@node9/proxy 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -4
- package/dist/cli.js +2812 -584
- package/dist/cli.mjs +2775 -546
- package/dist/index.js +80 -17
- package/dist/index.mjs +80 -17
- package/dist/shields/builtin/bash-safe.json +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -147,8 +147,8 @@ function sanitizeConfig(raw) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
const lines = result.error.issues.map((issue) => {
|
|
150
|
-
const
|
|
151
|
-
return ` \u2022 ${
|
|
150
|
+
const path41 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
151
|
+
return ` \u2022 ${path41}: ${issue.message}`;
|
|
152
152
|
});
|
|
153
153
|
return {
|
|
154
154
|
sanitized,
|
|
@@ -232,7 +232,8 @@ var init_config_schema = __esm({
|
|
|
232
232
|
enableTrustSessions: z.boolean().optional(),
|
|
233
233
|
allowGlobalPause: z.boolean().optional(),
|
|
234
234
|
auditHashArgs: z.boolean().optional(),
|
|
235
|
-
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional()
|
|
235
|
+
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
236
|
+
cloudSyncIntervalHours: z.number().positive().optional()
|
|
236
237
|
}).optional(),
|
|
237
238
|
policy: z.object({
|
|
238
239
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -253,6 +254,11 @@ var init_config_schema = __esm({
|
|
|
253
254
|
enabled: z.boolean().optional(),
|
|
254
255
|
threshold: z.number().min(2).optional(),
|
|
255
256
|
windowSeconds: z.number().min(10).optional()
|
|
257
|
+
}).optional(),
|
|
258
|
+
skillPinning: z.object({
|
|
259
|
+
enabled: z.boolean().optional(),
|
|
260
|
+
mode: z.enum(["warn", "block"]).optional(),
|
|
261
|
+
roots: z.array(z.string()).optional()
|
|
256
262
|
}).optional()
|
|
257
263
|
}).optional(),
|
|
258
264
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -495,11 +501,11 @@ function getGlobalSettings() {
|
|
|
495
501
|
};
|
|
496
502
|
}
|
|
497
503
|
function getCredentials() {
|
|
498
|
-
const
|
|
504
|
+
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
499
505
|
if (process.env.NODE9_API_KEY) {
|
|
500
506
|
return {
|
|
501
507
|
apiKey: process.env.NODE9_API_KEY,
|
|
502
|
-
apiUrl: process.env.NODE9_API_URL ||
|
|
508
|
+
apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL2
|
|
503
509
|
};
|
|
504
510
|
}
|
|
505
511
|
try {
|
|
@@ -511,13 +517,13 @@ function getCredentials() {
|
|
|
511
517
|
if (profile?.apiKey) {
|
|
512
518
|
return {
|
|
513
519
|
apiKey: profile.apiKey,
|
|
514
|
-
apiUrl: profile.apiUrl ||
|
|
520
|
+
apiUrl: profile.apiUrl || DEFAULT_API_URL2
|
|
515
521
|
};
|
|
516
522
|
}
|
|
517
523
|
if (creds.apiKey) {
|
|
518
524
|
return {
|
|
519
525
|
apiKey: creds.apiKey,
|
|
520
|
-
apiUrl: creds.apiUrl ||
|
|
526
|
+
apiUrl: creds.apiUrl || DEFAULT_API_URL2
|
|
521
527
|
};
|
|
522
528
|
}
|
|
523
529
|
}
|
|
@@ -551,7 +557,11 @@ function getConfig(cwd) {
|
|
|
551
557
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
552
558
|
},
|
|
553
559
|
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
554
|
-
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
560
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
561
|
+
skillPinning: {
|
|
562
|
+
...DEFAULT_CONFIG.policy.skillPinning,
|
|
563
|
+
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
564
|
+
}
|
|
555
565
|
};
|
|
556
566
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
557
567
|
const applyLayer = (source) => {
|
|
@@ -568,6 +578,8 @@ function getConfig(cwd) {
|
|
|
568
578
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
569
579
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
570
580
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
581
|
+
if (s.cloudSyncIntervalHours !== void 0)
|
|
582
|
+
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
571
583
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
572
584
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
573
585
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -577,7 +589,12 @@ function getConfig(cwd) {
|
|
|
577
589
|
if (p.smartRules) {
|
|
578
590
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
579
591
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
580
|
-
|
|
592
|
+
const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
|
|
593
|
+
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
594
|
+
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
595
|
+
(r) => !r.name || !userRuleNames.has(r.name)
|
|
596
|
+
);
|
|
597
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
|
|
581
598
|
}
|
|
582
599
|
if (p.snapshot) {
|
|
583
600
|
const s2 = p.snapshot;
|
|
@@ -597,6 +614,16 @@ function getConfig(cwd) {
|
|
|
597
614
|
if (ld.windowSeconds !== void 0)
|
|
598
615
|
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
599
616
|
}
|
|
617
|
+
if (p.skillPinning && typeof p.skillPinning === "object") {
|
|
618
|
+
const sp = p.skillPinning;
|
|
619
|
+
if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
|
|
620
|
+
if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
|
|
621
|
+
if (Array.isArray(sp.roots)) {
|
|
622
|
+
for (const r of sp.roots) {
|
|
623
|
+
if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
600
627
|
const envs = source.environments || {};
|
|
601
628
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
602
629
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -611,6 +638,16 @@ function getConfig(cwd) {
|
|
|
611
638
|
};
|
|
612
639
|
applyLayer(globalConfig);
|
|
613
640
|
applyLayer(projectConfig);
|
|
641
|
+
{
|
|
642
|
+
const cacheFile = path3.join(os3.homedir(), ".node9", "rules-cache.json");
|
|
643
|
+
try {
|
|
644
|
+
const raw = JSON.parse(fs3.readFileSync(cacheFile, "utf-8"));
|
|
645
|
+
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
646
|
+
applyLayer({ policy: { smartRules: raw.rules } });
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
614
651
|
const shieldOverrides = readShieldOverrides();
|
|
615
652
|
for (const shieldName of readActiveShields()) {
|
|
616
653
|
const shield = getShield(shieldName);
|
|
@@ -638,6 +675,7 @@ function getConfig(cwd) {
|
|
|
638
675
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
639
676
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
640
677
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
678
|
+
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
641
679
|
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
642
680
|
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
643
681
|
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
@@ -726,7 +764,8 @@ var init_config = __esm({
|
|
|
726
764
|
// 120-second auto-deny timeout
|
|
727
765
|
flightRecorder: true,
|
|
728
766
|
auditHashArgs: true,
|
|
729
|
-
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
767
|
+
approvers: { native: true, browser: true, cloud: false, terminal: true },
|
|
768
|
+
cloudSyncIntervalHours: 5
|
|
730
769
|
},
|
|
731
770
|
policy: {
|
|
732
771
|
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
@@ -901,7 +940,8 @@ var init_config = __esm({
|
|
|
901
940
|
}
|
|
902
941
|
],
|
|
903
942
|
dlp: { enabled: true, scanIgnoredTools: true },
|
|
904
|
-
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
943
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
944
|
+
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
905
945
|
},
|
|
906
946
|
environments: {}
|
|
907
947
|
};
|
|
@@ -1710,9 +1750,9 @@ function matchesPattern(text, patterns) {
|
|
|
1710
1750
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1711
1751
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1712
1752
|
}
|
|
1713
|
-
function getNestedValue(obj,
|
|
1753
|
+
function getNestedValue(obj, path41) {
|
|
1714
1754
|
if (!obj || typeof obj !== "object") return null;
|
|
1715
|
-
return
|
|
1755
|
+
return path41.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1716
1756
|
}
|
|
1717
1757
|
function shouldSnapshot(toolName, args, config) {
|
|
1718
1758
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2187,8 +2227,8 @@ async function explainPolicy(toolName, args) {
|
|
|
2187
2227
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
2188
2228
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
2189
2229
|
allTokens.push(...extraTokens);
|
|
2190
|
-
const
|
|
2191
|
-
detail += ` + deep scan of args: [${
|
|
2230
|
+
const preview2 = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
|
|
2231
|
+
detail += ` + deep scan of args: [${preview2}]`;
|
|
2192
2232
|
}
|
|
2193
2233
|
steps.push({ name: "Input parsing", outcome: "checked", detail });
|
|
2194
2234
|
}
|
|
@@ -2305,6 +2345,15 @@ var init_policy = __esm({
|
|
|
2305
2345
|
import fs8 from "fs";
|
|
2306
2346
|
import path9 from "path";
|
|
2307
2347
|
import os7 from "os";
|
|
2348
|
+
function extractCommandPattern(toolName, args) {
|
|
2349
|
+
const lower = toolName.toLowerCase();
|
|
2350
|
+
if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
|
|
2351
|
+
const a = args;
|
|
2352
|
+
const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
|
|
2353
|
+
if (!cmd) return void 0;
|
|
2354
|
+
const words = cmd.split(/\s+/);
|
|
2355
|
+
return words.slice(0, 2).join(" ");
|
|
2356
|
+
}
|
|
2308
2357
|
function checkPause() {
|
|
2309
2358
|
try {
|
|
2310
2359
|
if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -2338,7 +2387,7 @@ function resumeNode9() {
|
|
|
2338
2387
|
} catch {
|
|
2339
2388
|
}
|
|
2340
2389
|
}
|
|
2341
|
-
function getActiveTrustSession(toolName) {
|
|
2390
|
+
function getActiveTrustSession(toolName, args) {
|
|
2342
2391
|
try {
|
|
2343
2392
|
if (!fs8.existsSync(TRUST_FILE)) return false;
|
|
2344
2393
|
const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
@@ -2347,12 +2396,20 @@ function getActiveTrustSession(toolName) {
|
|
|
2347
2396
|
if (active.length !== trust.entries.length) {
|
|
2348
2397
|
fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
2349
2398
|
}
|
|
2350
|
-
return active.some((e) =>
|
|
2399
|
+
return active.some((e) => {
|
|
2400
|
+
if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
|
|
2401
|
+
if (e.commandPattern) {
|
|
2402
|
+
const actual = extractCommandPattern(toolName, args) ?? "";
|
|
2403
|
+
return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
|
|
2404
|
+
}
|
|
2405
|
+
return true;
|
|
2406
|
+
});
|
|
2351
2407
|
} catch {
|
|
2352
2408
|
return false;
|
|
2353
2409
|
}
|
|
2354
2410
|
}
|
|
2355
|
-
function writeTrustSession(toolName, durationMs) {
|
|
2411
|
+
function writeTrustSession(toolName, durationMs, args) {
|
|
2412
|
+
const commandPattern = extractCommandPattern(toolName, args);
|
|
2356
2413
|
try {
|
|
2357
2414
|
let trust = { entries: [] };
|
|
2358
2415
|
try {
|
|
@@ -2362,8 +2419,14 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2362
2419
|
} catch {
|
|
2363
2420
|
}
|
|
2364
2421
|
const now = Date.now();
|
|
2365
|
-
trust.entries = trust.entries.filter(
|
|
2366
|
-
|
|
2422
|
+
trust.entries = trust.entries.filter(
|
|
2423
|
+
(e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
|
|
2424
|
+
);
|
|
2425
|
+
trust.entries.push({
|
|
2426
|
+
tool: toolName,
|
|
2427
|
+
...commandPattern && { commandPattern },
|
|
2428
|
+
expiry: now + durationMs
|
|
2429
|
+
});
|
|
2367
2430
|
atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
|
|
2368
2431
|
} catch (err2) {
|
|
2369
2432
|
if (process.env.NODE9_DEBUG === "1") {
|
|
@@ -2452,8 +2515,8 @@ function isDaemonRunning() {
|
|
|
2452
2515
|
if (port !== DAEMON_PORT) {
|
|
2453
2516
|
return false;
|
|
2454
2517
|
}
|
|
2455
|
-
const
|
|
2456
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid >
|
|
2518
|
+
const MAX_PID2 = 4194304;
|
|
2519
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
|
|
2457
2520
|
return false;
|
|
2458
2521
|
}
|
|
2459
2522
|
try {
|
|
@@ -3359,12 +3422,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3359
3422
|
};
|
|
3360
3423
|
}
|
|
3361
3424
|
}
|
|
3362
|
-
if (getActiveTrustSession(toolName)) {
|
|
3363
|
-
if (approvers.cloud && creds?.apiKey)
|
|
3364
|
-
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3365
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3366
|
-
return { approved: true, checkedBy: "trust" };
|
|
3367
|
-
}
|
|
3368
3425
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3369
3426
|
if (policyResult.decision === "allow") {
|
|
3370
3427
|
if (approvers.cloud && creds?.apiKey)
|
|
@@ -3446,6 +3503,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3446
3503
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
3447
3504
|
return { approved: true };
|
|
3448
3505
|
}
|
|
3506
|
+
if (!taintWarning && getActiveTrustSession(toolName, args)) {
|
|
3507
|
+
if (approvers.cloud && creds?.apiKey)
|
|
3508
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3509
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3510
|
+
return { approved: true, checkedBy: "trust" };
|
|
3511
|
+
}
|
|
3449
3512
|
if (taintWarning) {
|
|
3450
3513
|
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
3451
3514
|
riskMetadata = computeRiskMetadata(
|
|
@@ -3578,7 +3641,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3578
3641
|
riskMetadata?.ruleDescription
|
|
3579
3642
|
);
|
|
3580
3643
|
if (decision === "always_allow") {
|
|
3581
|
-
writeTrustSession(toolName, 36e5);
|
|
3644
|
+
writeTrustSession(toolName, 36e5, args);
|
|
3582
3645
|
return { approved: true, checkedBy: "trust" };
|
|
3583
3646
|
}
|
|
3584
3647
|
const isApproved = decision === "allow";
|
|
@@ -5756,7 +5819,7 @@ function writeGlobalSetting(key, value) {
|
|
|
5756
5819
|
config.settings[key] = value;
|
|
5757
5820
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5758
5821
|
}
|
|
5759
|
-
function writeTrustEntry(toolName, durationMs) {
|
|
5822
|
+
function writeTrustEntry(toolName, durationMs, commandPattern) {
|
|
5760
5823
|
try {
|
|
5761
5824
|
let trust = { entries: [] };
|
|
5762
5825
|
try {
|
|
@@ -5764,8 +5827,14 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5764
5827
|
trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5765
5828
|
} catch {
|
|
5766
5829
|
}
|
|
5767
|
-
trust.entries = trust.entries.filter(
|
|
5768
|
-
|
|
5830
|
+
trust.entries = trust.entries.filter(
|
|
5831
|
+
(e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > Date.now()
|
|
5832
|
+
);
|
|
5833
|
+
trust.entries.push({
|
|
5834
|
+
tool: toolName,
|
|
5835
|
+
...commandPattern && { commandPattern },
|
|
5836
|
+
expiry: Date.now() + durationMs
|
|
5837
|
+
});
|
|
5769
5838
|
atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
|
|
5770
5839
|
} catch {
|
|
5771
5840
|
}
|
|
@@ -6243,15 +6312,152 @@ var init_costSync = __esm({
|
|
|
6243
6312
|
}
|
|
6244
6313
|
});
|
|
6245
6314
|
|
|
6246
|
-
// src/daemon/
|
|
6247
|
-
import http from "http";
|
|
6315
|
+
// src/daemon/sync.ts
|
|
6248
6316
|
import fs17 from "fs";
|
|
6317
|
+
import https from "https";
|
|
6318
|
+
import os15 from "os";
|
|
6249
6319
|
import path20 from "path";
|
|
6320
|
+
function readCredentials() {
|
|
6321
|
+
if (process.env.NODE9_API_KEY) {
|
|
6322
|
+
return {
|
|
6323
|
+
apiKey: process.env.NODE9_API_KEY,
|
|
6324
|
+
apiUrl: process.env.NODE9_API_URL ?? DEFAULT_API_URL
|
|
6325
|
+
};
|
|
6326
|
+
}
|
|
6327
|
+
try {
|
|
6328
|
+
const credPath = path20.join(os15.homedir(), ".node9", "credentials.json");
|
|
6329
|
+
const creds = JSON.parse(fs17.readFileSync(credPath, "utf-8"));
|
|
6330
|
+
const profileName = process.env.NODE9_PROFILE ?? "default";
|
|
6331
|
+
const profile = creds[profileName];
|
|
6332
|
+
if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
|
|
6333
|
+
return {
|
|
6334
|
+
apiKey: profile.apiKey,
|
|
6335
|
+
apiUrl: typeof profile.apiUrl === "string" ? profile.apiUrl.replace(/\/intercept$/, "/policy") : DEFAULT_API_URL
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6338
|
+
if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
|
|
6339
|
+
return { apiKey: creds.apiKey, apiUrl: DEFAULT_API_URL };
|
|
6340
|
+
}
|
|
6341
|
+
} catch {
|
|
6342
|
+
}
|
|
6343
|
+
return null;
|
|
6344
|
+
}
|
|
6345
|
+
function fetchCloudRules(apiKey, apiUrl) {
|
|
6346
|
+
const parsed = new URL(apiUrl);
|
|
6347
|
+
return new Promise((resolve, reject) => {
|
|
6348
|
+
const req = https.request(
|
|
6349
|
+
{
|
|
6350
|
+
hostname: parsed.hostname,
|
|
6351
|
+
port: parsed.port ? parseInt(parsed.port, 10) : void 0,
|
|
6352
|
+
path: parsed.pathname + parsed.search,
|
|
6353
|
+
method: "GET",
|
|
6354
|
+
headers: {
|
|
6355
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6356
|
+
"Content-Type": "application/json"
|
|
6357
|
+
},
|
|
6358
|
+
timeout: 1e4
|
|
6359
|
+
},
|
|
6360
|
+
(res) => {
|
|
6361
|
+
const chunks = [];
|
|
6362
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
6363
|
+
res.on("end", () => {
|
|
6364
|
+
if (res.statusCode !== 200) {
|
|
6365
|
+
reject(new Error(`API returned ${res.statusCode ?? "unknown"}`));
|
|
6366
|
+
return;
|
|
6367
|
+
}
|
|
6368
|
+
try {
|
|
6369
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
6370
|
+
const rules = Array.isArray(body) ? body : Array.isArray(body.rules) ? body.rules : [];
|
|
6371
|
+
resolve(rules);
|
|
6372
|
+
} catch (e) {
|
|
6373
|
+
reject(e);
|
|
6374
|
+
}
|
|
6375
|
+
});
|
|
6376
|
+
}
|
|
6377
|
+
);
|
|
6378
|
+
req.on("error", reject);
|
|
6379
|
+
req.on("timeout", () => {
|
|
6380
|
+
req.destroy(new Error("Cloud policy fetch timed out"));
|
|
6381
|
+
});
|
|
6382
|
+
req.end();
|
|
6383
|
+
});
|
|
6384
|
+
}
|
|
6385
|
+
async function syncOnce() {
|
|
6386
|
+
const creds = readCredentials();
|
|
6387
|
+
if (!creds) return;
|
|
6388
|
+
try {
|
|
6389
|
+
const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
|
|
6390
|
+
const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
|
|
6391
|
+
const dir = path20.dirname(rulesCacheFile());
|
|
6392
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
6393
|
+
fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
6394
|
+
} catch {
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
async function runCloudSync() {
|
|
6398
|
+
const creds = readCredentials();
|
|
6399
|
+
if (!creds) {
|
|
6400
|
+
return { ok: false, reason: "No API key configured. Add credentials with: node9 login" };
|
|
6401
|
+
}
|
|
6402
|
+
try {
|
|
6403
|
+
const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
|
|
6404
|
+
const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
|
|
6405
|
+
const dir = path20.dirname(rulesCacheFile());
|
|
6406
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
6407
|
+
fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
6408
|
+
return { ok: true, rules: rules.length, fetchedAt: cache.fetchedAt };
|
|
6409
|
+
} catch (err2) {
|
|
6410
|
+
return { ok: false, reason: err2 instanceof Error ? err2.message : String(err2) };
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
function getCloudSyncStatus() {
|
|
6414
|
+
try {
|
|
6415
|
+
const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
|
|
6416
|
+
if (!Array.isArray(raw.rules) || typeof raw.fetchedAt !== "string") return { cached: false };
|
|
6417
|
+
return { cached: true, rules: raw.rules.length, fetchedAt: raw.fetchedAt };
|
|
6418
|
+
} catch {
|
|
6419
|
+
return { cached: false };
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
function getCloudRules() {
|
|
6423
|
+
try {
|
|
6424
|
+
const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
|
|
6425
|
+
return Array.isArray(raw.rules) ? raw.rules : null;
|
|
6426
|
+
} catch {
|
|
6427
|
+
return null;
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
function startCloudSync() {
|
|
6431
|
+
const rawHours = getConfig().settings.cloudSyncIntervalHours ?? DEFAULT_INTERVAL_HOURS;
|
|
6432
|
+
const intervalHours = Math.max(rawHours, MIN_INTERVAL_HOURS);
|
|
6433
|
+
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
6434
|
+
const initial = setTimeout(() => void syncOnce(), 3e4);
|
|
6435
|
+
initial.unref();
|
|
6436
|
+
const recurring = setInterval(() => void syncOnce(), intervalMs);
|
|
6437
|
+
recurring.unref();
|
|
6438
|
+
}
|
|
6439
|
+
var rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
|
|
6440
|
+
var init_sync = __esm({
|
|
6441
|
+
"src/daemon/sync.ts"() {
|
|
6442
|
+
"use strict";
|
|
6443
|
+
init_config();
|
|
6444
|
+
rulesCacheFile = () => path20.join(os15.homedir(), ".node9", "rules-cache.json");
|
|
6445
|
+
DEFAULT_API_URL = "https://api.node9.ai/api/v1/policy";
|
|
6446
|
+
DEFAULT_INTERVAL_HOURS = 5;
|
|
6447
|
+
MIN_INTERVAL_HOURS = 1;
|
|
6448
|
+
}
|
|
6449
|
+
});
|
|
6450
|
+
|
|
6451
|
+
// src/daemon/server.ts
|
|
6452
|
+
import http from "http";
|
|
6453
|
+
import fs18 from "fs";
|
|
6454
|
+
import path21 from "path";
|
|
6250
6455
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6251
6456
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6252
6457
|
import chalk2 from "chalk";
|
|
6253
6458
|
function startDaemon() {
|
|
6254
6459
|
startCostSync();
|
|
6460
|
+
startCloudSync();
|
|
6255
6461
|
loadInsightCounts();
|
|
6256
6462
|
const csrfToken = randomUUID4();
|
|
6257
6463
|
const internalToken = randomUUID4();
|
|
@@ -6267,7 +6473,7 @@ function startDaemon() {
|
|
|
6267
6473
|
idleTimer = setTimeout(() => {
|
|
6268
6474
|
if (autoStarted) {
|
|
6269
6475
|
try {
|
|
6270
|
-
|
|
6476
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6271
6477
|
} catch {
|
|
6272
6478
|
}
|
|
6273
6479
|
}
|
|
@@ -6430,7 +6636,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6430
6636
|
status: "pending"
|
|
6431
6637
|
});
|
|
6432
6638
|
}
|
|
6433
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6639
|
+
const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
|
|
6434
6640
|
const projectConfig = getConfig(projectCwd);
|
|
6435
6641
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6436
6642
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6557,7 +6763,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6557
6763
|
);
|
|
6558
6764
|
if (decision === "trust" && trustDuration) {
|
|
6559
6765
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
6560
|
-
|
|
6766
|
+
const commandPattern = extractCommandPattern(entry.toolName, entry.args);
|
|
6767
|
+
writeTrustEntry(entry.toolName, ms, commandPattern);
|
|
6561
6768
|
appendAuditLog({
|
|
6562
6769
|
toolName: entry.toolName,
|
|
6563
6770
|
args: entry.args,
|
|
@@ -6820,8 +7027,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6820
7027
|
const body = await readBody(req);
|
|
6821
7028
|
const data = body ? JSON.parse(body) : {};
|
|
6822
7029
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6823
|
-
const node9Dir =
|
|
6824
|
-
if (!
|
|
7030
|
+
const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
|
|
7031
|
+
if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
|
|
6825
7032
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6826
7033
|
return res.end(
|
|
6827
7034
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6932,14 +7139,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6932
7139
|
server.on("error", (e) => {
|
|
6933
7140
|
if (e.code === "EADDRINUSE") {
|
|
6934
7141
|
try {
|
|
6935
|
-
if (
|
|
6936
|
-
const { pid } = JSON.parse(
|
|
7142
|
+
if (fs18.existsSync(DAEMON_PID_FILE)) {
|
|
7143
|
+
const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6937
7144
|
process.kill(pid, 0);
|
|
6938
7145
|
return process.exit(0);
|
|
6939
7146
|
}
|
|
6940
7147
|
} catch {
|
|
6941
7148
|
try {
|
|
6942
|
-
|
|
7149
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6943
7150
|
} catch {
|
|
6944
7151
|
}
|
|
6945
7152
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -7005,59 +7212,331 @@ var init_server = __esm({
|
|
|
7005
7212
|
init_shields();
|
|
7006
7213
|
init_ui2();
|
|
7007
7214
|
init_state2();
|
|
7215
|
+
init_state();
|
|
7008
7216
|
init_patch();
|
|
7009
7217
|
init_config_schema();
|
|
7010
7218
|
init_costSync();
|
|
7219
|
+
init_sync();
|
|
7220
|
+
}
|
|
7221
|
+
});
|
|
7222
|
+
|
|
7223
|
+
// src/daemon/service.ts
|
|
7224
|
+
import fs19 from "fs";
|
|
7225
|
+
import path22 from "path";
|
|
7226
|
+
import os16 from "os";
|
|
7227
|
+
import { spawnSync as spawnSync3, execFileSync } from "child_process";
|
|
7228
|
+
function resolveNode9Binary() {
|
|
7229
|
+
try {
|
|
7230
|
+
const script = process.argv[1];
|
|
7231
|
+
if (typeof script === "string" && path22.isAbsolute(script) && fs19.existsSync(script)) {
|
|
7232
|
+
return fs19.realpathSync(script);
|
|
7233
|
+
}
|
|
7234
|
+
} catch {
|
|
7235
|
+
}
|
|
7236
|
+
try {
|
|
7237
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
7238
|
+
const r = spawnSync3(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
|
|
7239
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
7240
|
+
return r.stdout.trim().split("\n")[0].trim();
|
|
7241
|
+
}
|
|
7242
|
+
} catch {
|
|
7243
|
+
}
|
|
7244
|
+
return null;
|
|
7245
|
+
}
|
|
7246
|
+
function xmlEscape(s) {
|
|
7247
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7248
|
+
}
|
|
7249
|
+
function launchdPlist(binaryPath) {
|
|
7250
|
+
const logDir = path22.join(os16.homedir(), ".node9");
|
|
7251
|
+
const nodePath = xmlEscape(process.execPath);
|
|
7252
|
+
const scriptPath = xmlEscape(binaryPath);
|
|
7253
|
+
const outLog = xmlEscape(path22.join(logDir, "daemon.log"));
|
|
7254
|
+
const errLog = xmlEscape(path22.join(logDir, "daemon-error.log"));
|
|
7255
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7256
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7257
|
+
<plist version="1.0">
|
|
7258
|
+
<dict>
|
|
7259
|
+
<key>Label</key>
|
|
7260
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
7261
|
+
<key>ProgramArguments</key>
|
|
7262
|
+
<array>
|
|
7263
|
+
<string>${nodePath}</string>
|
|
7264
|
+
<string>${scriptPath}</string>
|
|
7265
|
+
<string>daemon</string>
|
|
7266
|
+
</array>
|
|
7267
|
+
<key>RunAtLoad</key>
|
|
7268
|
+
<true/>
|
|
7269
|
+
<key>KeepAlive</key>
|
|
7270
|
+
<true/>
|
|
7271
|
+
<key>ThrottleInterval</key>
|
|
7272
|
+
<integer>10</integer>
|
|
7273
|
+
<key>StandardOutPath</key>
|
|
7274
|
+
<string>${outLog}</string>
|
|
7275
|
+
<key>StandardErrorPath</key>
|
|
7276
|
+
<string>${errLog}</string>
|
|
7277
|
+
<key>EnvironmentVariables</key>
|
|
7278
|
+
<dict>
|
|
7279
|
+
<key>NODE9_AUTO_STARTED</key>
|
|
7280
|
+
<string>1</string>
|
|
7281
|
+
<key>NODE9_BROWSER_OPENED</key>
|
|
7282
|
+
<string>1</string>
|
|
7283
|
+
</dict>
|
|
7284
|
+
</dict>
|
|
7285
|
+
</plist>
|
|
7286
|
+
`;
|
|
7287
|
+
}
|
|
7288
|
+
function installLaunchd(binaryPath) {
|
|
7289
|
+
const dir = path22.dirname(LAUNCHD_PLIST);
|
|
7290
|
+
if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
|
|
7291
|
+
fs19.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
|
|
7292
|
+
spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
|
|
7293
|
+
const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
|
|
7294
|
+
encoding: "utf8",
|
|
7295
|
+
timeout: 5e3
|
|
7296
|
+
});
|
|
7297
|
+
if (r.status !== 0) {
|
|
7298
|
+
throw new Error(`launchctl load failed: ${r.stderr || r.stdout || "unknown error"}`);
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
function uninstallLaunchd() {
|
|
7302
|
+
if (fs19.existsSync(LAUNCHD_PLIST)) {
|
|
7303
|
+
spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
|
|
7304
|
+
fs19.unlinkSync(LAUNCHD_PLIST);
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
function isLaunchdInstalled() {
|
|
7308
|
+
return fs19.existsSync(LAUNCHD_PLIST);
|
|
7309
|
+
}
|
|
7310
|
+
function systemdUnit(binaryPath) {
|
|
7311
|
+
return `[Unit]
|
|
7312
|
+
Description=node9 approval daemon
|
|
7313
|
+
After=network.target
|
|
7314
|
+
|
|
7315
|
+
[Service]
|
|
7316
|
+
Type=simple
|
|
7317
|
+
ExecStart=${process.execPath} ${binaryPath} daemon
|
|
7318
|
+
Restart=on-failure
|
|
7319
|
+
RestartSec=10s
|
|
7320
|
+
Environment=NODE9_AUTO_STARTED=1
|
|
7321
|
+
Environment=NODE9_BROWSER_OPENED=1
|
|
7322
|
+
|
|
7323
|
+
[Install]
|
|
7324
|
+
WantedBy=default.target
|
|
7325
|
+
`;
|
|
7326
|
+
}
|
|
7327
|
+
function installSystemd(binaryPath) {
|
|
7328
|
+
if (!fs19.existsSync(SYSTEMD_UNIT_DIR)) {
|
|
7329
|
+
fs19.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
|
|
7330
|
+
}
|
|
7331
|
+
fs19.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
|
|
7332
|
+
try {
|
|
7333
|
+
execFileSync("loginctl", ["enable-linger", os16.userInfo().username], { timeout: 3e3 });
|
|
7334
|
+
} catch {
|
|
7335
|
+
}
|
|
7336
|
+
const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
|
|
7337
|
+
encoding: "utf8",
|
|
7338
|
+
timeout: 5e3
|
|
7339
|
+
});
|
|
7340
|
+
if (reload.status !== 0) {
|
|
7341
|
+
throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
|
|
7342
|
+
}
|
|
7343
|
+
spawnSync3("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
|
|
7344
|
+
const enable = spawnSync3("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
|
|
7345
|
+
encoding: "utf8",
|
|
7346
|
+
timeout: 5e3
|
|
7347
|
+
});
|
|
7348
|
+
if (enable.status !== 0) {
|
|
7349
|
+
throw new Error(`systemctl enable failed: ${enable.stderr}`);
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
function uninstallSystemd() {
|
|
7353
|
+
if (fs19.existsSync(SYSTEMD_UNIT)) {
|
|
7354
|
+
spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
|
|
7355
|
+
encoding: "utf8",
|
|
7356
|
+
timeout: 5e3
|
|
7357
|
+
});
|
|
7358
|
+
spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
|
|
7359
|
+
fs19.unlinkSync(SYSTEMD_UNIT);
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
function isSystemdInstalled() {
|
|
7363
|
+
return fs19.existsSync(SYSTEMD_UNIT);
|
|
7364
|
+
}
|
|
7365
|
+
function stopRunningDaemon() {
|
|
7366
|
+
const pidFile = path22.join(os16.homedir(), ".node9", "daemon.pid");
|
|
7367
|
+
if (!fs19.existsSync(pidFile)) return;
|
|
7368
|
+
try {
|
|
7369
|
+
const data = JSON.parse(fs19.readFileSync(pidFile, "utf-8"));
|
|
7370
|
+
const pid = data.pid;
|
|
7371
|
+
const MAX_PID2 = 4194304;
|
|
7372
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
|
|
7373
|
+
try {
|
|
7374
|
+
process.kill(pid, "SIGTERM");
|
|
7375
|
+
const deadline = Date.now() + 3e3;
|
|
7376
|
+
const pollStop = spawnSync3(
|
|
7377
|
+
"sh",
|
|
7378
|
+
["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
|
|
7379
|
+
{
|
|
7380
|
+
timeout: 3100
|
|
7381
|
+
}
|
|
7382
|
+
);
|
|
7383
|
+
void pollStop;
|
|
7384
|
+
void deadline;
|
|
7385
|
+
} catch {
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
try {
|
|
7389
|
+
fs19.unlinkSync(pidFile);
|
|
7390
|
+
} catch {
|
|
7391
|
+
}
|
|
7392
|
+
} catch {
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
function installDaemonService() {
|
|
7396
|
+
const binary = resolveNode9Binary();
|
|
7397
|
+
if (!binary) {
|
|
7398
|
+
return { ok: false, reason: "Could not locate the node9 binary. Is it in your PATH?" };
|
|
7399
|
+
}
|
|
7400
|
+
stopRunningDaemon();
|
|
7401
|
+
try {
|
|
7402
|
+
if (process.platform === "darwin") {
|
|
7403
|
+
const alreadyInstalled = isLaunchdInstalled();
|
|
7404
|
+
installLaunchd(binary);
|
|
7405
|
+
return { ok: true, platform: "launchd", alreadyInstalled };
|
|
7406
|
+
}
|
|
7407
|
+
if (process.platform === "linux") {
|
|
7408
|
+
const check = spawnSync3("systemctl", ["--user", "--version"], {
|
|
7409
|
+
encoding: "utf8",
|
|
7410
|
+
timeout: 2e3
|
|
7411
|
+
});
|
|
7412
|
+
if (check.status !== 0) {
|
|
7413
|
+
return {
|
|
7414
|
+
ok: false,
|
|
7415
|
+
reason: "systemd not available. Start the daemon manually with: node9 daemon start"
|
|
7416
|
+
};
|
|
7417
|
+
}
|
|
7418
|
+
const alreadyInstalled = isSystemdInstalled();
|
|
7419
|
+
installSystemd(binary);
|
|
7420
|
+
return { ok: true, platform: "systemd", alreadyInstalled };
|
|
7421
|
+
}
|
|
7422
|
+
return {
|
|
7423
|
+
ok: false,
|
|
7424
|
+
reason: `Automatic service install is not supported on ${process.platform}. Start the daemon manually with: node9 daemon start`
|
|
7425
|
+
};
|
|
7426
|
+
} catch (err2) {
|
|
7427
|
+
return {
|
|
7428
|
+
ok: false,
|
|
7429
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
7430
|
+
};
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
function uninstallDaemonService() {
|
|
7434
|
+
try {
|
|
7435
|
+
if (process.platform === "darwin") {
|
|
7436
|
+
uninstallLaunchd();
|
|
7437
|
+
return { ok: true, platform: "launchd", alreadyInstalled: false };
|
|
7438
|
+
}
|
|
7439
|
+
if (process.platform === "linux") {
|
|
7440
|
+
uninstallSystemd();
|
|
7441
|
+
return { ok: true, platform: "systemd", alreadyInstalled: false };
|
|
7442
|
+
}
|
|
7443
|
+
return {
|
|
7444
|
+
ok: false,
|
|
7445
|
+
reason: `Service management not supported on ${process.platform}.`
|
|
7446
|
+
};
|
|
7447
|
+
} catch (err2) {
|
|
7448
|
+
return {
|
|
7449
|
+
ok: false,
|
|
7450
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
function isDaemonServiceInstalled() {
|
|
7455
|
+
if (process.platform === "darwin") return isLaunchdInstalled();
|
|
7456
|
+
if (process.platform === "linux") return isSystemdInstalled();
|
|
7457
|
+
return false;
|
|
7458
|
+
}
|
|
7459
|
+
var LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
|
|
7460
|
+
var init_service = __esm({
|
|
7461
|
+
"src/daemon/service.ts"() {
|
|
7462
|
+
"use strict";
|
|
7463
|
+
LAUNCHD_LABEL = "ai.node9.daemon";
|
|
7464
|
+
LAUNCHD_PLIST = path22.join(os16.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
7465
|
+
SYSTEMD_UNIT_DIR = path22.join(os16.homedir(), ".config", "systemd", "user");
|
|
7466
|
+
SYSTEMD_UNIT = path22.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
|
|
7011
7467
|
}
|
|
7012
7468
|
});
|
|
7013
7469
|
|
|
7014
7470
|
// src/daemon/index.ts
|
|
7015
|
-
import
|
|
7471
|
+
import fs20 from "fs";
|
|
7016
7472
|
import chalk3 from "chalk";
|
|
7017
|
-
import { spawnSync as
|
|
7473
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
7018
7474
|
function stopDaemon() {
|
|
7019
|
-
if (!
|
|
7475
|
+
if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
7020
7476
|
try {
|
|
7021
|
-
const
|
|
7477
|
+
const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7478
|
+
const pid = data.pid;
|
|
7479
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
7480
|
+
console.log(chalk3.gray("Cleaned up invalid PID file."));
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7022
7483
|
process.kill(pid, "SIGTERM");
|
|
7023
7484
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
7024
7485
|
} catch {
|
|
7025
7486
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
7026
7487
|
} finally {
|
|
7027
7488
|
try {
|
|
7028
|
-
|
|
7489
|
+
fs20.unlinkSync(DAEMON_PID_FILE);
|
|
7029
7490
|
} catch {
|
|
7030
7491
|
}
|
|
7031
7492
|
}
|
|
7032
7493
|
}
|
|
7033
7494
|
function daemonStatus() {
|
|
7034
|
-
|
|
7495
|
+
const serviceInstalled = isDaemonServiceInstalled();
|
|
7496
|
+
const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
|
|
7497
|
+
let processStatus;
|
|
7498
|
+
if (fs20.existsSync(DAEMON_PID_FILE)) {
|
|
7035
7499
|
try {
|
|
7036
|
-
const
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7500
|
+
const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7501
|
+
const pid = data.pid;
|
|
7502
|
+
const port = data.port;
|
|
7503
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
7504
|
+
processStatus = chalk3.yellow("not running (invalid PID file)");
|
|
7505
|
+
} else {
|
|
7506
|
+
process.kill(pid, 0);
|
|
7507
|
+
processStatus = chalk3.green(
|
|
7508
|
+
`running (PID ${pid}, port ${typeof port === "number" ? port : DAEMON_PORT})`
|
|
7509
|
+
);
|
|
7510
|
+
}
|
|
7040
7511
|
} catch {
|
|
7041
|
-
|
|
7042
|
-
return;
|
|
7512
|
+
processStatus = chalk3.yellow("not running (stale PID file)");
|
|
7043
7513
|
}
|
|
7044
|
-
}
|
|
7045
|
-
const r = spawnSync3("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
7046
|
-
encoding: "utf8",
|
|
7047
|
-
timeout: 500
|
|
7048
|
-
});
|
|
7049
|
-
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
|
|
7050
|
-
console.log(chalk3.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
7051
7514
|
} else {
|
|
7052
|
-
|
|
7515
|
+
const r = spawnSync4("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
7516
|
+
encoding: "utf8",
|
|
7517
|
+
timeout: 500
|
|
7518
|
+
});
|
|
7519
|
+
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
|
|
7520
|
+
processStatus = chalk3.yellow(`running (orphaned \u2014 no PID file)`);
|
|
7521
|
+
} else {
|
|
7522
|
+
processStatus = chalk3.yellow("not running");
|
|
7523
|
+
}
|
|
7053
7524
|
}
|
|
7525
|
+
console.log(`
|
|
7526
|
+
Process : ${processStatus}`);
|
|
7527
|
+
console.log(` Service : ${serviceLabel}
|
|
7528
|
+
`);
|
|
7054
7529
|
}
|
|
7530
|
+
var MAX_PID;
|
|
7055
7531
|
var init_daemon2 = __esm({
|
|
7056
7532
|
"src/daemon/index.ts"() {
|
|
7057
7533
|
"use strict";
|
|
7058
7534
|
init_server();
|
|
7059
7535
|
init_state2();
|
|
7536
|
+
init_service();
|
|
7060
7537
|
init_state2();
|
|
7538
|
+
init_service();
|
|
7539
|
+
MAX_PID = 4194304;
|
|
7061
7540
|
}
|
|
7062
7541
|
});
|
|
7063
7542
|
|
|
@@ -7067,10 +7546,10 @@ __export(tail_exports, {
|
|
|
7067
7546
|
startTail: () => startTail
|
|
7068
7547
|
});
|
|
7069
7548
|
import http2 from "http";
|
|
7070
|
-
import
|
|
7071
|
-
import
|
|
7072
|
-
import
|
|
7073
|
-
import
|
|
7549
|
+
import chalk24 from "chalk";
|
|
7550
|
+
import fs35 from "fs";
|
|
7551
|
+
import os31 from "os";
|
|
7552
|
+
import path38 from "path";
|
|
7074
7553
|
import readline5 from "readline";
|
|
7075
7554
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
7076
7555
|
function getIcon(tool) {
|
|
@@ -7093,22 +7572,22 @@ function formatBase(activity) {
|
|
|
7093
7572
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
7094
7573
|
const icon = getIcon(activity.tool);
|
|
7095
7574
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
7096
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
7575
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os31.homedir(), "~");
|
|
7097
7576
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
7098
|
-
return `${
|
|
7577
|
+
return `${chalk24.gray(time)} ${icon} ${chalk24.white.bold(toolName)} ${chalk24.dim(argsPreview)}`;
|
|
7099
7578
|
}
|
|
7100
7579
|
function renderResult(activity, result) {
|
|
7101
7580
|
const base = formatBase(activity);
|
|
7102
7581
|
let status;
|
|
7103
7582
|
if (result.status === "allow") {
|
|
7104
|
-
status =
|
|
7583
|
+
status = chalk24.green("\u2713 ALLOW");
|
|
7105
7584
|
} else if (result.status === "dlp") {
|
|
7106
|
-
status =
|
|
7585
|
+
status = chalk24.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
7107
7586
|
} else {
|
|
7108
|
-
status =
|
|
7587
|
+
status = chalk24.red("\u2717 BLOCK");
|
|
7109
7588
|
}
|
|
7110
7589
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
7111
|
-
const costSuffix = cost == null ? "" :
|
|
7590
|
+
const costSuffix = cost == null ? "" : chalk24.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
7112
7591
|
if (process.stdout.isTTY) {
|
|
7113
7592
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
7114
7593
|
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -7125,19 +7604,19 @@ function renderResult(activity, result) {
|
|
|
7125
7604
|
}
|
|
7126
7605
|
function renderPending(activity) {
|
|
7127
7606
|
if (!process.stdout.isTTY) return;
|
|
7128
|
-
const line = `${formatBase(activity)} ${
|
|
7607
|
+
const line = `${formatBase(activity)} ${chalk24.yellow("\u25CF \u2026")}`;
|
|
7129
7608
|
pendingShownForId = activity.id;
|
|
7130
7609
|
pendingWrappedLines = wrappedLineCount(line);
|
|
7131
7610
|
process.stdout.write(`${line}\r`);
|
|
7132
7611
|
}
|
|
7133
7612
|
async function ensureDaemon() {
|
|
7134
7613
|
let pidPort = null;
|
|
7135
|
-
if (
|
|
7614
|
+
if (fs35.existsSync(PID_FILE)) {
|
|
7136
7615
|
try {
|
|
7137
|
-
const { port } = JSON.parse(
|
|
7616
|
+
const { port } = JSON.parse(fs35.readFileSync(PID_FILE, "utf-8"));
|
|
7138
7617
|
pidPort = port;
|
|
7139
7618
|
} catch {
|
|
7140
|
-
console.error(
|
|
7619
|
+
console.error(chalk24.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
7141
7620
|
}
|
|
7142
7621
|
}
|
|
7143
7622
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -7148,7 +7627,7 @@ async function ensureDaemon() {
|
|
|
7148
7627
|
if (res.ok) return checkPort;
|
|
7149
7628
|
} catch {
|
|
7150
7629
|
}
|
|
7151
|
-
console.log(
|
|
7630
|
+
console.log(chalk24.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
7152
7631
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
7153
7632
|
detached: true,
|
|
7154
7633
|
stdio: "ignore",
|
|
@@ -7165,7 +7644,7 @@ async function ensureDaemon() {
|
|
|
7165
7644
|
} catch {
|
|
7166
7645
|
}
|
|
7167
7646
|
}
|
|
7168
|
-
console.error(
|
|
7647
|
+
console.error(chalk24.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7169
7648
|
process.exit(1);
|
|
7170
7649
|
}
|
|
7171
7650
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -7197,27 +7676,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
|
7197
7676
|
req.end(body);
|
|
7198
7677
|
});
|
|
7199
7678
|
}
|
|
7679
|
+
function extractArgsSummary(toolName, args) {
|
|
7680
|
+
const a = args;
|
|
7681
|
+
if (!a) return "";
|
|
7682
|
+
const cmd = a["command"] ?? a["cmd"];
|
|
7683
|
+
if (typeof cmd === "string") return cmd.replace(/\s+/g, " ").trim();
|
|
7684
|
+
const fp = a["file_path"] ?? a["path"] ?? a["filepath"];
|
|
7685
|
+
if (typeof fp === "string") return fp;
|
|
7686
|
+
const q = a["query"] ?? a["sql"];
|
|
7687
|
+
if (typeof q === "string") return q.replace(/\s+/g, " ").trim();
|
|
7688
|
+
const s = JSON.stringify(a).replace(/\s+/g, " ");
|
|
7689
|
+
return s.length > 80 ? s.slice(0, 80) + "\u2026" : s;
|
|
7690
|
+
}
|
|
7691
|
+
function cleanReason(raw) {
|
|
7692
|
+
return raw.replace(/\s*[—–-]+\s*(blocked by|requires human approval)[^)]*(\([^)]*\))?\.?\s*$/i, "").trim();
|
|
7693
|
+
}
|
|
7694
|
+
function cleanBlockedBy(raw) {
|
|
7695
|
+
const shieldMatch = raw.match(/shield:([^:]+):/i);
|
|
7696
|
+
if (shieldMatch) return shieldMatch[1];
|
|
7697
|
+
const smartMatch = raw.match(/^Smart Rule:\s*(.+)$/i);
|
|
7698
|
+
if (smartMatch) return smartMatch[1];
|
|
7699
|
+
return raw;
|
|
7700
|
+
}
|
|
7200
7701
|
function buildCardLines(req, localCount = 0) {
|
|
7201
7702
|
if (req.recoveryCommand) {
|
|
7202
7703
|
return buildRecoveryCardLines(req);
|
|
7203
7704
|
}
|
|
7204
|
-
const
|
|
7205
|
-
const argsPreview =
|
|
7206
|
-
const
|
|
7207
|
-
const blockedBy =
|
|
7705
|
+
const argsSummary = extractArgsSummary(req.toolName, req.args);
|
|
7706
|
+
const argsPreview = argsSummary.length > 72 ? argsSummary.slice(0, 72) + "\u2026" : argsSummary;
|
|
7707
|
+
const rawBlockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
7708
|
+
const blockedBy = cleanBlockedBy(rawBlockedBy);
|
|
7709
|
+
const isBlock = req.riskMetadata?.tier != null && req.riskMetadata.tier <= 2;
|
|
7710
|
+
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
7711
|
+
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
7712
|
+
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
7208
7713
|
const lines = [
|
|
7209
7714
|
``,
|
|
7210
7715
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
7211
7716
|
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
7212
|
-
`${CYAN}\u2551${RESET2}
|
|
7717
|
+
`${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
|
|
7213
7718
|
];
|
|
7214
|
-
if (
|
|
7215
|
-
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}
|
|
7719
|
+
if (description) {
|
|
7720
|
+
lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
|
|
7216
7721
|
}
|
|
7217
|
-
if (req.riskMetadata?.ruleName &&
|
|
7722
|
+
if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
|
|
7218
7723
|
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
7219
7724
|
}
|
|
7220
|
-
|
|
7725
|
+
if (argsPreview) {
|
|
7726
|
+
const argLabel = req.toolName.toLowerCase().includes("bash") ? "Command" : "Args ";
|
|
7727
|
+
lines.push(`${CYAN}\u2551${RESET2} ${argLabel}: ${GRAY}${argsPreview}${RESET2}`);
|
|
7728
|
+
}
|
|
7221
7729
|
if (localCount >= 2) {
|
|
7222
7730
|
lines.push(
|
|
7223
7731
|
`${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
|
|
@@ -7257,9 +7765,9 @@ function buildRecoveryCardLines(req) {
|
|
|
7257
7765
|
];
|
|
7258
7766
|
}
|
|
7259
7767
|
function readApproversFromDisk() {
|
|
7260
|
-
const configPath =
|
|
7768
|
+
const configPath = path38.join(os31.homedir(), ".node9", "config.json");
|
|
7261
7769
|
try {
|
|
7262
|
-
const raw = JSON.parse(
|
|
7770
|
+
const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
7263
7771
|
const settings = raw.settings ?? {};
|
|
7264
7772
|
return settings.approvers ?? {};
|
|
7265
7773
|
} catch {
|
|
@@ -7270,20 +7778,20 @@ function approverStatusLine() {
|
|
|
7270
7778
|
const a = readApproversFromDisk();
|
|
7271
7779
|
const fmt = (label, key) => {
|
|
7272
7780
|
const on = a[key] !== false;
|
|
7273
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7781
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk24.green("\u2713") : chalk24.dim("\u2717")}`;
|
|
7274
7782
|
};
|
|
7275
7783
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7276
7784
|
}
|
|
7277
7785
|
function toggleApprover(channel) {
|
|
7278
|
-
const configPath =
|
|
7786
|
+
const configPath = path38.join(os31.homedir(), ".node9", "config.json");
|
|
7279
7787
|
try {
|
|
7280
|
-
const raw = JSON.parse(
|
|
7788
|
+
const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
7281
7789
|
const settings = raw.settings ?? {};
|
|
7282
7790
|
const approvers = settings.approvers ?? {};
|
|
7283
7791
|
approvers[channel] = approvers[channel] === false;
|
|
7284
7792
|
settings.approvers = approvers;
|
|
7285
7793
|
raw.settings = settings;
|
|
7286
|
-
|
|
7794
|
+
fs35.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7287
7795
|
} catch (err2) {
|
|
7288
7796
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7289
7797
|
`);
|
|
@@ -7315,7 +7823,7 @@ async function startTail(options = {}) {
|
|
|
7315
7823
|
req2.end();
|
|
7316
7824
|
});
|
|
7317
7825
|
if (result.ok) {
|
|
7318
|
-
console.log(
|
|
7826
|
+
console.log(chalk24.green("\u2713 Flight Recorder buffer cleared."));
|
|
7319
7827
|
} else if (result.code === "ECONNREFUSED") {
|
|
7320
7828
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
7321
7829
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7359,7 +7867,7 @@ async function startTail(options = {}) {
|
|
|
7359
7867
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7360
7868
|
if (channel) {
|
|
7361
7869
|
toggleApprover(channel);
|
|
7362
|
-
console.log(
|
|
7870
|
+
console.log(chalk24.dim(` Approvers: ${approverStatusLine()}`));
|
|
7363
7871
|
}
|
|
7364
7872
|
};
|
|
7365
7873
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7425,7 +7933,7 @@ async function startTail(options = {}) {
|
|
|
7425
7933
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7426
7934
|
)
|
|
7427
7935
|
);
|
|
7428
|
-
const decisionStamp = action === "always-allow" ?
|
|
7936
|
+
const decisionStamp = action === "always-allow" ? chalk24.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk24.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk24.green("\u2713 ALLOWED") : action === "redirect" ? chalk24.yellow("\u21A9 REDIRECT AI") : chalk24.red("\u2717 DENIED");
|
|
7429
7937
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7430
7938
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7431
7939
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7453,8 +7961,8 @@ async function startTail(options = {}) {
|
|
|
7453
7961
|
}
|
|
7454
7962
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7455
7963
|
try {
|
|
7456
|
-
|
|
7457
|
-
|
|
7964
|
+
fs35.appendFileSync(
|
|
7965
|
+
path38.join(os31.homedir(), ".node9", "hook-debug.log"),
|
|
7458
7966
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7459
7967
|
`
|
|
7460
7968
|
);
|
|
@@ -7476,7 +7984,7 @@ async function startTail(options = {}) {
|
|
|
7476
7984
|
);
|
|
7477
7985
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7478
7986
|
if (externalDecision) {
|
|
7479
|
-
const source = externalDecision === "allow" ?
|
|
7987
|
+
const source = externalDecision === "allow" ? chalk24.green("\u2713 ALLOWED") : chalk24.red("\u2717 DENIED");
|
|
7480
7988
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7481
7989
|
}
|
|
7482
7990
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7535,16 +8043,16 @@ async function startTail(options = {}) {
|
|
|
7535
8043
|
}
|
|
7536
8044
|
} catch {
|
|
7537
8045
|
}
|
|
7538
|
-
console.log(
|
|
7539
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
8046
|
+
console.log(chalk24.cyan.bold(`
|
|
8047
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk24.dim(`\u2192 ${dashboardUrl}`));
|
|
7540
8048
|
if (canApprove) {
|
|
7541
|
-
console.log(
|
|
7542
|
-
console.log(
|
|
8049
|
+
console.log(chalk24.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
8050
|
+
console.log(chalk24.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7543
8051
|
}
|
|
7544
8052
|
if (options.history) {
|
|
7545
|
-
console.log(
|
|
8053
|
+
console.log(chalk24.dim("Showing history + live events.\n"));
|
|
7546
8054
|
} else {
|
|
7547
|
-
console.log(
|
|
8055
|
+
console.log(chalk24.dim("Showing live events only. Use --history to include past.\n"));
|
|
7548
8056
|
}
|
|
7549
8057
|
process.on("SIGINT", () => {
|
|
7550
8058
|
exitIdleMode();
|
|
@@ -7554,13 +8062,13 @@ async function startTail(options = {}) {
|
|
|
7554
8062
|
readline5.clearLine(process.stdout, 0);
|
|
7555
8063
|
readline5.cursorTo(process.stdout, 0);
|
|
7556
8064
|
}
|
|
7557
|
-
console.log(
|
|
8065
|
+
console.log(chalk24.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7558
8066
|
process.exit(0);
|
|
7559
8067
|
});
|
|
7560
8068
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7561
8069
|
const req = http2.get(sseUrl, (res) => {
|
|
7562
8070
|
if (res.statusCode !== 200) {
|
|
7563
|
-
console.error(
|
|
8071
|
+
console.error(chalk24.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7564
8072
|
process.exit(1);
|
|
7565
8073
|
}
|
|
7566
8074
|
if (canApprove) enterIdleMode();
|
|
@@ -7591,7 +8099,7 @@ async function startTail(options = {}) {
|
|
|
7591
8099
|
readline5.clearLine(process.stdout, 0);
|
|
7592
8100
|
readline5.cursorTo(process.stdout, 0);
|
|
7593
8101
|
}
|
|
7594
|
-
console.log(
|
|
8102
|
+
console.log(chalk24.red("\n\u274C Daemon disconnected."));
|
|
7595
8103
|
process.exit(1);
|
|
7596
8104
|
});
|
|
7597
8105
|
});
|
|
@@ -7683,9 +8191,9 @@ async function startTail(options = {}) {
|
|
|
7683
8191
|
const hash = data.hash ?? "";
|
|
7684
8192
|
const summary = data.argsSummary ?? data.tool;
|
|
7685
8193
|
const fileCount = data.fileCount ?? 0;
|
|
7686
|
-
const files = fileCount > 0 ?
|
|
8194
|
+
const files = fileCount > 0 ? chalk24.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7687
8195
|
process.stdout.write(
|
|
7688
|
-
`${
|
|
8196
|
+
`${chalk24.dim(time)} ${chalk24.cyan("\u{1F4F8} snapshot")} ${chalk24.dim(hash)} ${summary}${files}
|
|
7689
8197
|
`
|
|
7690
8198
|
);
|
|
7691
8199
|
return;
|
|
@@ -7702,7 +8210,7 @@ async function startTail(options = {}) {
|
|
|
7702
8210
|
}
|
|
7703
8211
|
req.on("error", (err2) => {
|
|
7704
8212
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7705
|
-
console.error(
|
|
8213
|
+
console.error(chalk24.red(`
|
|
7706
8214
|
\u274C ${msg}`));
|
|
7707
8215
|
process.exit(1);
|
|
7708
8216
|
});
|
|
@@ -7714,7 +8222,7 @@ var init_tail = __esm({
|
|
|
7714
8222
|
init_daemon2();
|
|
7715
8223
|
init_daemon();
|
|
7716
8224
|
init_core();
|
|
7717
|
-
PID_FILE =
|
|
8225
|
+
PID_FILE = path38.join(os31.homedir(), ".node9", "daemon.pid");
|
|
7718
8226
|
ICONS = {
|
|
7719
8227
|
bash: "\u{1F4BB}",
|
|
7720
8228
|
shell: "\u{1F4BB}",
|
|
@@ -7755,9 +8263,9 @@ __export(hud_exports, {
|
|
|
7755
8263
|
main: () => main,
|
|
7756
8264
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7757
8265
|
});
|
|
7758
|
-
import
|
|
7759
|
-
import
|
|
7760
|
-
import
|
|
8266
|
+
import fs36 from "fs";
|
|
8267
|
+
import path39 from "path";
|
|
8268
|
+
import os32 from "os";
|
|
7761
8269
|
import http3 from "http";
|
|
7762
8270
|
async function readStdin() {
|
|
7763
8271
|
const chunks = [];
|
|
@@ -7833,9 +8341,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7833
8341
|
return ` (${m}m left)`;
|
|
7834
8342
|
}
|
|
7835
8343
|
function safeReadJson(filePath) {
|
|
7836
|
-
if (!
|
|
8344
|
+
if (!fs36.existsSync(filePath)) return null;
|
|
7837
8345
|
try {
|
|
7838
|
-
return JSON.parse(
|
|
8346
|
+
return JSON.parse(fs36.readFileSync(filePath, "utf-8"));
|
|
7839
8347
|
} catch {
|
|
7840
8348
|
return null;
|
|
7841
8349
|
}
|
|
@@ -7856,12 +8364,12 @@ function countHooksInFile(filePath) {
|
|
|
7856
8364
|
return Object.keys(cfg.hooks).length;
|
|
7857
8365
|
}
|
|
7858
8366
|
function countRulesInDir(rulesDir) {
|
|
7859
|
-
if (!
|
|
8367
|
+
if (!fs36.existsSync(rulesDir)) return 0;
|
|
7860
8368
|
let count = 0;
|
|
7861
8369
|
try {
|
|
7862
|
-
for (const entry of
|
|
8370
|
+
for (const entry of fs36.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7863
8371
|
if (entry.isDirectory()) {
|
|
7864
|
-
count += countRulesInDir(
|
|
8372
|
+
count += countRulesInDir(path39.join(rulesDir, entry.name));
|
|
7865
8373
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7866
8374
|
count++;
|
|
7867
8375
|
}
|
|
@@ -7872,46 +8380,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7872
8380
|
}
|
|
7873
8381
|
function isSamePath(a, b) {
|
|
7874
8382
|
try {
|
|
7875
|
-
return
|
|
8383
|
+
return path39.resolve(a) === path39.resolve(b);
|
|
7876
8384
|
} catch {
|
|
7877
8385
|
return false;
|
|
7878
8386
|
}
|
|
7879
8387
|
}
|
|
7880
8388
|
function countConfigs(cwd) {
|
|
7881
|
-
const homeDir2 =
|
|
7882
|
-
const claudeDir =
|
|
8389
|
+
const homeDir2 = os32.homedir();
|
|
8390
|
+
const claudeDir = path39.join(homeDir2, ".claude");
|
|
7883
8391
|
let claudeMdCount = 0;
|
|
7884
8392
|
let rulesCount = 0;
|
|
7885
8393
|
let hooksCount = 0;
|
|
7886
8394
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7887
8395
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7888
|
-
if (
|
|
7889
|
-
rulesCount += countRulesInDir(
|
|
7890
|
-
const userSettings =
|
|
8396
|
+
if (fs36.existsSync(path39.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8397
|
+
rulesCount += countRulesInDir(path39.join(claudeDir, "rules"));
|
|
8398
|
+
const userSettings = path39.join(claudeDir, "settings.json");
|
|
7891
8399
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7892
8400
|
hooksCount += countHooksInFile(userSettings);
|
|
7893
|
-
const userClaudeJson =
|
|
8401
|
+
const userClaudeJson = path39.join(homeDir2, ".claude.json");
|
|
7894
8402
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7895
8403
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7896
8404
|
userMcpServers.delete(name);
|
|
7897
8405
|
}
|
|
7898
8406
|
if (cwd) {
|
|
7899
|
-
if (
|
|
7900
|
-
if (
|
|
7901
|
-
const projectClaudeDir =
|
|
8407
|
+
if (fs36.existsSync(path39.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
8408
|
+
if (fs36.existsSync(path39.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8409
|
+
const projectClaudeDir = path39.join(cwd, ".claude");
|
|
7902
8410
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7903
8411
|
if (!overlapsUserScope) {
|
|
7904
|
-
if (
|
|
7905
|
-
rulesCount += countRulesInDir(
|
|
7906
|
-
const projSettings =
|
|
8412
|
+
if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8413
|
+
rulesCount += countRulesInDir(path39.join(projectClaudeDir, "rules"));
|
|
8414
|
+
const projSettings = path39.join(projectClaudeDir, "settings.json");
|
|
7907
8415
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7908
8416
|
hooksCount += countHooksInFile(projSettings);
|
|
7909
8417
|
}
|
|
7910
|
-
if (
|
|
7911
|
-
const localSettings =
|
|
8418
|
+
if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8419
|
+
const localSettings = path39.join(projectClaudeDir, "settings.local.json");
|
|
7912
8420
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7913
8421
|
hooksCount += countHooksInFile(localSettings);
|
|
7914
|
-
const mcpJsonServers = getMcpServerNames(
|
|
8422
|
+
const mcpJsonServers = getMcpServerNames(path39.join(cwd, ".mcp.json"));
|
|
7915
8423
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7916
8424
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7917
8425
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7944,12 +8452,12 @@ function readActiveShieldsHud() {
|
|
|
7944
8452
|
return shieldsCache.value;
|
|
7945
8453
|
}
|
|
7946
8454
|
try {
|
|
7947
|
-
const shieldsPath =
|
|
7948
|
-
if (!
|
|
8455
|
+
const shieldsPath = path39.join(os32.homedir(), ".node9", "shields.json");
|
|
8456
|
+
if (!fs36.existsSync(shieldsPath)) {
|
|
7949
8457
|
shieldsCache = { value: [], ts: now };
|
|
7950
8458
|
return [];
|
|
7951
8459
|
}
|
|
7952
|
-
const parsed = JSON.parse(
|
|
8460
|
+
const parsed = JSON.parse(fs36.readFileSync(shieldsPath, "utf-8"));
|
|
7953
8461
|
if (!Array.isArray(parsed.active)) {
|
|
7954
8462
|
shieldsCache = { value: [], ts: now };
|
|
7955
8463
|
return [];
|
|
@@ -8051,17 +8559,17 @@ function renderContextLine(stdin) {
|
|
|
8051
8559
|
async function main() {
|
|
8052
8560
|
try {
|
|
8053
8561
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
8054
|
-
if (
|
|
8562
|
+
if (fs36.existsSync(path39.join(os32.homedir(), ".node9", "hud-debug"))) {
|
|
8055
8563
|
try {
|
|
8056
|
-
const logPath =
|
|
8564
|
+
const logPath = path39.join(os32.homedir(), ".node9", "hud-debug.log");
|
|
8057
8565
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8058
8566
|
let size = 0;
|
|
8059
8567
|
try {
|
|
8060
|
-
size =
|
|
8568
|
+
size = fs36.statSync(logPath).size;
|
|
8061
8569
|
} catch {
|
|
8062
8570
|
}
|
|
8063
8571
|
if (size < MAX_LOG_SIZE) {
|
|
8064
|
-
|
|
8572
|
+
fs36.appendFileSync(
|
|
8065
8573
|
logPath,
|
|
8066
8574
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
8067
8575
|
);
|
|
@@ -8082,11 +8590,11 @@ async function main() {
|
|
|
8082
8590
|
try {
|
|
8083
8591
|
const cwd = stdin.cwd ?? process.cwd();
|
|
8084
8592
|
for (const configPath of [
|
|
8085
|
-
|
|
8086
|
-
|
|
8593
|
+
path39.join(cwd, "node9.config.json"),
|
|
8594
|
+
path39.join(os32.homedir(), ".node9", "config.json")
|
|
8087
8595
|
]) {
|
|
8088
|
-
if (!
|
|
8089
|
-
const cfg = JSON.parse(
|
|
8596
|
+
if (!fs36.existsSync(configPath)) continue;
|
|
8597
|
+
const cfg = JSON.parse(fs36.readFileSync(configPath, "utf-8"));
|
|
8090
8598
|
const hud = cfg.settings?.hud;
|
|
8091
8599
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
8092
8600
|
}
|
|
@@ -8517,7 +9025,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
8517
9025
|
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
8518
9026
|
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
8519
9027
|
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
8520
|
-
codex: exists(path15.join(homeDir2, ".codex"))
|
|
9028
|
+
codex: exists(path15.join(homeDir2, ".codex")),
|
|
9029
|
+
windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
|
|
9030
|
+
vscode: exists(path15.join(homeDir2, ".vscode"))
|
|
8521
9031
|
};
|
|
8522
9032
|
}
|
|
8523
9033
|
async function setupCursor() {
|
|
@@ -8666,6 +9176,38 @@ async function setupCodex() {
|
|
|
8666
9176
|
printDaemonTip();
|
|
8667
9177
|
}
|
|
8668
9178
|
}
|
|
9179
|
+
function teardownCodex() {
|
|
9180
|
+
const homeDir2 = os11.homedir();
|
|
9181
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
9182
|
+
const config = readToml(configPath);
|
|
9183
|
+
if (!config?.mcp_servers) {
|
|
9184
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
|
|
9185
|
+
return;
|
|
9186
|
+
}
|
|
9187
|
+
let changed = false;
|
|
9188
|
+
if (removeNode9McpServer(config.mcp_servers)) {
|
|
9189
|
+
changed = true;
|
|
9190
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codex/config.toml"));
|
|
9191
|
+
}
|
|
9192
|
+
for (const [name, server] of Object.entries(config.mcp_servers)) {
|
|
9193
|
+
const args = server.args;
|
|
9194
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9195
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9196
|
+
config.mcp_servers[name] = {
|
|
9197
|
+
...server,
|
|
9198
|
+
command: originalCmd,
|
|
9199
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9200
|
+
};
|
|
9201
|
+
changed = true;
|
|
9202
|
+
}
|
|
9203
|
+
}
|
|
9204
|
+
if (changed) {
|
|
9205
|
+
writeToml(configPath, config);
|
|
9206
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
|
|
9207
|
+
} else {
|
|
9208
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
|
|
9209
|
+
}
|
|
9210
|
+
}
|
|
8669
9211
|
function setupHud() {
|
|
8670
9212
|
const homeDir2 = os11.homedir();
|
|
8671
9213
|
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8712,31 +9254,303 @@ function teardownHud() {
|
|
|
8712
9254
|
console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
|
|
8713
9255
|
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
8714
9256
|
}
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
const
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
9257
|
+
async function setupWindsurf() {
|
|
9258
|
+
const homeDir2 = os11.homedir();
|
|
9259
|
+
const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
|
|
9260
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
9261
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
9262
|
+
let anythingChanged = false;
|
|
9263
|
+
if (!hasNode9McpServer(servers)) {
|
|
9264
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
9265
|
+
mcpConfig.mcpServers = servers;
|
|
9266
|
+
writeJson(mcpPath, mcpConfig);
|
|
9267
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9268
|
+
anythingChanged = true;
|
|
9269
|
+
}
|
|
9270
|
+
const serversToWrap = [];
|
|
9271
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
9272
|
+
if (!server.command || server.command === "node9") continue;
|
|
9273
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
9274
|
+
}
|
|
9275
|
+
if (serversToWrap.length > 0) {
|
|
9276
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
9277
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
9278
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9279
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
9280
|
+
}
|
|
9281
|
+
console.log("");
|
|
9282
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
9283
|
+
if (proceed) {
|
|
9284
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9285
|
+
servers[name] = {
|
|
9286
|
+
...servers[name],
|
|
9287
|
+
command: "node9",
|
|
9288
|
+
args: ["mcp", "--upstream", upstream]
|
|
9289
|
+
};
|
|
9290
|
+
}
|
|
9291
|
+
mcpConfig.mcpServers = servers;
|
|
9292
|
+
writeJson(mcpPath, mcpConfig);
|
|
9293
|
+
console.log(chalk.green(`
|
|
9294
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
9295
|
+
anythingChanged = true;
|
|
9296
|
+
} else {
|
|
9297
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
9298
|
+
}
|
|
9299
|
+
console.log("");
|
|
9300
|
+
}
|
|
9301
|
+
console.log(
|
|
9302
|
+
chalk.yellow(
|
|
9303
|
+
" \u26A0\uFE0F Note: Windsurf does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Windsurf."
|
|
9304
|
+
)
|
|
9305
|
+
);
|
|
9306
|
+
console.log("");
|
|
9307
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
9308
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Windsurf."));
|
|
9309
|
+
printDaemonTip();
|
|
9310
|
+
return;
|
|
9311
|
+
}
|
|
9312
|
+
if (anythingChanged) {
|
|
9313
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Windsurf via MCP proxy!"));
|
|
9314
|
+
console.log(chalk.gray(" Restart Windsurf for changes to take effect."));
|
|
9315
|
+
printDaemonTip();
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
function teardownWindsurf() {
|
|
9319
|
+
const homeDir2 = os11.homedir();
|
|
9320
|
+
const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
|
|
9321
|
+
const mcpConfig = readJson(mcpPath);
|
|
9322
|
+
if (!mcpConfig?.mcpServers) {
|
|
9323
|
+
console.log(
|
|
9324
|
+
chalk.blue(" \u2139\uFE0F ~/.codeium/windsurf/mcp_config.json not found \u2014 nothing to remove")
|
|
9325
|
+
);
|
|
9326
|
+
return;
|
|
9327
|
+
}
|
|
9328
|
+
let changed = false;
|
|
9329
|
+
if (removeNode9McpServer(mcpConfig.mcpServers)) {
|
|
9330
|
+
changed = true;
|
|
9331
|
+
console.log(
|
|
9332
|
+
chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codeium/windsurf/mcp_config.json")
|
|
9333
|
+
);
|
|
9334
|
+
}
|
|
9335
|
+
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
9336
|
+
const args = server.args;
|
|
9337
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9338
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9339
|
+
mcpConfig.mcpServers[name] = {
|
|
9340
|
+
...server,
|
|
9341
|
+
command: originalCmd,
|
|
9342
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9343
|
+
};
|
|
9344
|
+
changed = true;
|
|
9345
|
+
}
|
|
9346
|
+
}
|
|
9347
|
+
if (changed) {
|
|
9348
|
+
writeJson(mcpPath, mcpConfig);
|
|
9349
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codeium/windsurf/mcp_config.json"));
|
|
9350
|
+
} else {
|
|
9351
|
+
console.log(
|
|
9352
|
+
chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codeium/windsurf/mcp_config.json")
|
|
9353
|
+
);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
function hasNode9McpServerVSCode(servers) {
|
|
9357
|
+
const entry = servers["node9"];
|
|
9358
|
+
return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
|
|
9359
|
+
}
|
|
9360
|
+
async function setupVSCode() {
|
|
9361
|
+
const homeDir2 = os11.homedir();
|
|
9362
|
+
const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
|
|
9363
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
9364
|
+
const servers = mcpConfig.servers ?? {};
|
|
9365
|
+
let anythingChanged = false;
|
|
9366
|
+
if (!hasNode9McpServerVSCode(servers)) {
|
|
9367
|
+
servers["node9"] = { type: "stdio", command: "node9", args: ["mcp-server"] };
|
|
9368
|
+
mcpConfig.servers = servers;
|
|
9369
|
+
writeJson(mcpPath, mcpConfig);
|
|
9370
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9371
|
+
anythingChanged = true;
|
|
9372
|
+
}
|
|
9373
|
+
const serversToWrap = [];
|
|
9374
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
9375
|
+
if (!server.command || server.command === "node9") continue;
|
|
9376
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
9377
|
+
}
|
|
9378
|
+
if (serversToWrap.length > 0) {
|
|
9379
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
9380
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
9381
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9382
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
9383
|
+
}
|
|
9384
|
+
console.log("");
|
|
9385
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
9386
|
+
if (proceed) {
|
|
9387
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9388
|
+
servers[name] = {
|
|
9389
|
+
...servers[name],
|
|
9390
|
+
type: "stdio",
|
|
9391
|
+
command: "node9",
|
|
9392
|
+
args: ["mcp", "--upstream", upstream]
|
|
9393
|
+
};
|
|
9394
|
+
}
|
|
9395
|
+
mcpConfig.servers = servers;
|
|
9396
|
+
writeJson(mcpPath, mcpConfig);
|
|
9397
|
+
console.log(chalk.green(`
|
|
9398
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
9399
|
+
anythingChanged = true;
|
|
9400
|
+
} else {
|
|
9401
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
9402
|
+
}
|
|
9403
|
+
console.log("");
|
|
9404
|
+
}
|
|
9405
|
+
console.log(
|
|
9406
|
+
chalk.yellow(
|
|
9407
|
+
" \u26A0\uFE0F Note: VSCode MCP support requires the GitHub Copilot extension (v1.99+).\n Pre-execution hooks are not supported \u2014 MCP proxy wrapping only."
|
|
9408
|
+
)
|
|
9409
|
+
);
|
|
9410
|
+
console.log("");
|
|
9411
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
9412
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for VSCode."));
|
|
9413
|
+
printDaemonTip();
|
|
9414
|
+
return;
|
|
9415
|
+
}
|
|
9416
|
+
if (anythingChanged) {
|
|
9417
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting VSCode via MCP proxy!"));
|
|
9418
|
+
console.log(chalk.gray(" Restart VSCode for changes to take effect."));
|
|
9419
|
+
printDaemonTip();
|
|
9420
|
+
}
|
|
9421
|
+
}
|
|
9422
|
+
function teardownVSCode() {
|
|
9423
|
+
const homeDir2 = os11.homedir();
|
|
9424
|
+
const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
|
|
9425
|
+
const mcpConfig = readJson(mcpPath);
|
|
9426
|
+
if (!mcpConfig?.servers) {
|
|
9427
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.vscode/mcp.json not found \u2014 nothing to remove"));
|
|
9428
|
+
return;
|
|
9429
|
+
}
|
|
9430
|
+
let changed = false;
|
|
9431
|
+
if (hasNode9McpServerVSCode(mcpConfig.servers)) {
|
|
9432
|
+
delete mcpConfig.servers["node9"];
|
|
9433
|
+
changed = true;
|
|
9434
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.vscode/mcp.json"));
|
|
9435
|
+
}
|
|
9436
|
+
for (const [name, server] of Object.entries(mcpConfig.servers)) {
|
|
9437
|
+
const args = server.args;
|
|
9438
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9439
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9440
|
+
mcpConfig.servers[name] = {
|
|
9441
|
+
...server,
|
|
9442
|
+
type: "stdio",
|
|
9443
|
+
command: originalCmd,
|
|
9444
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9445
|
+
};
|
|
9446
|
+
changed = true;
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9449
|
+
if (changed) {
|
|
9450
|
+
writeJson(mcpPath, mcpConfig);
|
|
9451
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.vscode/mcp.json"));
|
|
9452
|
+
} else {
|
|
9453
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9456
|
+
function getAgentsStatus(homeDir2 = os11.homedir()) {
|
|
9457
|
+
const detected = detectAgents(homeDir2);
|
|
9458
|
+
const claudeWired = (() => {
|
|
9459
|
+
const settings = readJson(path15.join(homeDir2, ".claude", "settings.json"));
|
|
9460
|
+
return !!settings?.hooks?.PreToolUse?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
|
|
9461
|
+
})();
|
|
9462
|
+
const geminiWired = (() => {
|
|
9463
|
+
const settings = readJson(path15.join(homeDir2, ".gemini", "settings.json"));
|
|
9464
|
+
return !!settings?.hooks?.BeforeTool?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
|
|
9465
|
+
})();
|
|
9466
|
+
const cursorWired = (() => {
|
|
9467
|
+
const cfg = readJson(path15.join(homeDir2, ".cursor", "mcp.json"));
|
|
9468
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
9469
|
+
})();
|
|
9470
|
+
const codexWired = (() => {
|
|
9471
|
+
const cfg = readToml(path15.join(homeDir2, ".codex", "config.toml"));
|
|
9472
|
+
return !!(cfg?.mcp_servers && hasNode9McpServer(cfg.mcp_servers));
|
|
9473
|
+
})();
|
|
9474
|
+
const windsurfWired = (() => {
|
|
9475
|
+
const cfg = readJson(
|
|
9476
|
+
path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json")
|
|
9477
|
+
);
|
|
9478
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
9479
|
+
})();
|
|
9480
|
+
const vscodeWired = (() => {
|
|
9481
|
+
const cfg = readJson(path15.join(homeDir2, ".vscode", "mcp.json"));
|
|
9482
|
+
return !!(cfg?.servers && hasNode9McpServerVSCode(cfg.servers));
|
|
9483
|
+
})();
|
|
9484
|
+
return [
|
|
9485
|
+
{
|
|
9486
|
+
name: "claude",
|
|
9487
|
+
label: "Claude Code",
|
|
9488
|
+
installed: detected.claude,
|
|
9489
|
+
wired: claudeWired,
|
|
9490
|
+
mode: detected.claude ? "hooks" : null
|
|
9491
|
+
},
|
|
9492
|
+
{
|
|
9493
|
+
name: "gemini",
|
|
9494
|
+
label: "Gemini CLI",
|
|
9495
|
+
installed: detected.gemini,
|
|
9496
|
+
wired: geminiWired,
|
|
9497
|
+
mode: detected.gemini ? "hooks" : null
|
|
9498
|
+
},
|
|
9499
|
+
{
|
|
9500
|
+
name: "cursor",
|
|
9501
|
+
label: "Cursor",
|
|
9502
|
+
installed: detected.cursor,
|
|
9503
|
+
wired: cursorWired,
|
|
9504
|
+
mode: detected.cursor ? "mcp" : null
|
|
9505
|
+
},
|
|
9506
|
+
{
|
|
9507
|
+
name: "windsurf",
|
|
9508
|
+
label: "Windsurf",
|
|
9509
|
+
installed: detected.windsurf,
|
|
9510
|
+
wired: windsurfWired,
|
|
9511
|
+
mode: detected.windsurf ? "mcp" : null
|
|
9512
|
+
},
|
|
9513
|
+
{
|
|
9514
|
+
name: "vscode",
|
|
9515
|
+
label: "VSCode",
|
|
9516
|
+
installed: detected.vscode,
|
|
9517
|
+
wired: vscodeWired,
|
|
9518
|
+
mode: detected.vscode ? "mcp" : null
|
|
9519
|
+
},
|
|
9520
|
+
{
|
|
9521
|
+
name: "codex",
|
|
9522
|
+
label: "Codex",
|
|
9523
|
+
installed: detected.codex,
|
|
9524
|
+
wired: codexWired,
|
|
9525
|
+
mode: detected.codex ? "mcp" : null
|
|
9526
|
+
}
|
|
9527
|
+
];
|
|
9528
|
+
}
|
|
9529
|
+
|
|
9530
|
+
// src/cli.ts
|
|
9531
|
+
init_daemon2();
|
|
9532
|
+
import chalk25 from "chalk";
|
|
9533
|
+
import fs37 from "fs";
|
|
9534
|
+
import path40 from "path";
|
|
9535
|
+
import os33 from "os";
|
|
9536
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
9537
|
+
|
|
9538
|
+
// src/utils/duration.ts
|
|
9539
|
+
function parseDuration(str) {
|
|
9540
|
+
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
9541
|
+
if (!m) return null;
|
|
9542
|
+
const n = parseFloat(m[1]);
|
|
9543
|
+
switch ((m[2] ?? "m").toLowerCase()) {
|
|
9544
|
+
case "s":
|
|
9545
|
+
return Math.round(n * 1e3);
|
|
9546
|
+
case "m":
|
|
9547
|
+
return Math.round(n * 6e4);
|
|
9548
|
+
case "h":
|
|
9549
|
+
return Math.round(n * 36e5);
|
|
9550
|
+
case "d":
|
|
9551
|
+
return Math.round(n * 864e5);
|
|
9552
|
+
default:
|
|
9553
|
+
return null;
|
|
8740
9554
|
}
|
|
8741
9555
|
}
|
|
8742
9556
|
|
|
@@ -8947,19 +9761,19 @@ init_daemon();
|
|
|
8947
9761
|
init_config();
|
|
8948
9762
|
init_policy();
|
|
8949
9763
|
import chalk5 from "chalk";
|
|
8950
|
-
import
|
|
9764
|
+
import fs23 from "fs";
|
|
8951
9765
|
import { spawn as spawn6 } from "child_process";
|
|
8952
|
-
import
|
|
8953
|
-
import
|
|
9766
|
+
import path25 from "path";
|
|
9767
|
+
import os19 from "os";
|
|
8954
9768
|
|
|
8955
9769
|
// src/undo.ts
|
|
8956
|
-
import { spawnSync as
|
|
9770
|
+
import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
|
|
8957
9771
|
import crypto3 from "crypto";
|
|
8958
|
-
import
|
|
9772
|
+
import fs21 from "fs";
|
|
8959
9773
|
import net3 from "net";
|
|
8960
|
-
import
|
|
8961
|
-
import
|
|
8962
|
-
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
9774
|
+
import path23 from "path";
|
|
9775
|
+
import os17 from "os";
|
|
9776
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path23.join(os17.tmpdir(), "node9-activity.sock");
|
|
8963
9777
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8964
9778
|
try {
|
|
8965
9779
|
const payload = JSON.stringify({
|
|
@@ -8979,22 +9793,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8979
9793
|
} catch {
|
|
8980
9794
|
}
|
|
8981
9795
|
}
|
|
8982
|
-
var SNAPSHOT_STACK_PATH =
|
|
8983
|
-
var UNDO_LATEST_PATH =
|
|
9796
|
+
var SNAPSHOT_STACK_PATH = path23.join(os17.homedir(), ".node9", "snapshots.json");
|
|
9797
|
+
var UNDO_LATEST_PATH = path23.join(os17.homedir(), ".node9", "undo_latest.txt");
|
|
8984
9798
|
var MAX_SNAPSHOTS = 10;
|
|
8985
9799
|
var GIT_TIMEOUT = 15e3;
|
|
8986
9800
|
function readStack() {
|
|
8987
9801
|
try {
|
|
8988
|
-
if (
|
|
8989
|
-
return JSON.parse(
|
|
9802
|
+
if (fs21.existsSync(SNAPSHOT_STACK_PATH))
|
|
9803
|
+
return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8990
9804
|
} catch {
|
|
8991
9805
|
}
|
|
8992
9806
|
return [];
|
|
8993
9807
|
}
|
|
8994
9808
|
function writeStack(stack) {
|
|
8995
|
-
const dir =
|
|
8996
|
-
if (!
|
|
8997
|
-
|
|
9809
|
+
const dir = path23.dirname(SNAPSHOT_STACK_PATH);
|
|
9810
|
+
if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
|
|
9811
|
+
fs21.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
8998
9812
|
}
|
|
8999
9813
|
function extractFilePath(args) {
|
|
9000
9814
|
if (!args || typeof args !== "object") return null;
|
|
@@ -9014,12 +9828,12 @@ function buildArgsSummary(tool, args) {
|
|
|
9014
9828
|
return "";
|
|
9015
9829
|
}
|
|
9016
9830
|
function findProjectRoot(filePath) {
|
|
9017
|
-
let dir =
|
|
9831
|
+
let dir = path23.dirname(filePath);
|
|
9018
9832
|
while (true) {
|
|
9019
|
-
if (
|
|
9833
|
+
if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
|
|
9020
9834
|
return dir;
|
|
9021
9835
|
}
|
|
9022
|
-
const parent =
|
|
9836
|
+
const parent = path23.dirname(dir);
|
|
9023
9837
|
if (parent === dir) return process.cwd();
|
|
9024
9838
|
dir = parent;
|
|
9025
9839
|
}
|
|
@@ -9027,7 +9841,7 @@ function findProjectRoot(filePath) {
|
|
|
9027
9841
|
function normalizeCwdForHash(cwd) {
|
|
9028
9842
|
let normalized;
|
|
9029
9843
|
try {
|
|
9030
|
-
normalized =
|
|
9844
|
+
normalized = fs21.realpathSync(cwd);
|
|
9031
9845
|
} catch {
|
|
9032
9846
|
normalized = cwd;
|
|
9033
9847
|
}
|
|
@@ -9037,16 +9851,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
9037
9851
|
}
|
|
9038
9852
|
function getShadowRepoDir(cwd) {
|
|
9039
9853
|
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
9040
|
-
return
|
|
9854
|
+
return path23.join(os17.homedir(), ".node9", "snapshots", hash);
|
|
9041
9855
|
}
|
|
9042
9856
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
9043
9857
|
try {
|
|
9044
9858
|
const cutoff = Date.now() - 6e4;
|
|
9045
|
-
for (const f of
|
|
9859
|
+
for (const f of fs21.readdirSync(shadowDir)) {
|
|
9046
9860
|
if (f.startsWith("index_")) {
|
|
9047
|
-
const fp =
|
|
9861
|
+
const fp = path23.join(shadowDir, f);
|
|
9048
9862
|
try {
|
|
9049
|
-
if (
|
|
9863
|
+
if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
|
|
9050
9864
|
} catch {
|
|
9051
9865
|
}
|
|
9052
9866
|
}
|
|
@@ -9058,7 +9872,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
9058
9872
|
const hardcoded = [".git", ".node9"];
|
|
9059
9873
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
9060
9874
|
try {
|
|
9061
|
-
|
|
9875
|
+
fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
9062
9876
|
} catch {
|
|
9063
9877
|
}
|
|
9064
9878
|
}
|
|
@@ -9066,54 +9880,54 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9066
9880
|
cleanOrphanedIndexFiles(shadowDir);
|
|
9067
9881
|
const normalizedCwd = normalizeCwdForHash(cwd);
|
|
9068
9882
|
const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
9069
|
-
const check =
|
|
9883
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9070
9884
|
env: shadowEnvBase,
|
|
9071
9885
|
timeout: 3e3
|
|
9072
9886
|
});
|
|
9073
9887
|
if (check.status === 0) {
|
|
9074
|
-
const ptPath =
|
|
9888
|
+
const ptPath = path23.join(shadowDir, "project-path.txt");
|
|
9075
9889
|
try {
|
|
9076
|
-
const stored =
|
|
9890
|
+
const stored = fs21.readFileSync(ptPath, "utf8").trim();
|
|
9077
9891
|
if (stored === normalizedCwd) return true;
|
|
9078
9892
|
if (process.env.NODE9_DEBUG === "1")
|
|
9079
9893
|
console.error(
|
|
9080
9894
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
9081
9895
|
);
|
|
9082
|
-
|
|
9896
|
+
fs21.rmSync(shadowDir, { recursive: true, force: true });
|
|
9083
9897
|
} catch {
|
|
9084
9898
|
try {
|
|
9085
|
-
|
|
9899
|
+
fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
9086
9900
|
} catch {
|
|
9087
9901
|
}
|
|
9088
9902
|
return true;
|
|
9089
9903
|
}
|
|
9090
9904
|
}
|
|
9091
9905
|
try {
|
|
9092
|
-
|
|
9906
|
+
fs21.mkdirSync(shadowDir, { recursive: true });
|
|
9093
9907
|
} catch {
|
|
9094
9908
|
}
|
|
9095
|
-
const init =
|
|
9909
|
+
const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
9096
9910
|
if (init.status !== 0 || init.error) {
|
|
9097
9911
|
const reason = init.error ? init.error.message : init.stderr?.toString();
|
|
9098
9912
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
9099
9913
|
return false;
|
|
9100
9914
|
}
|
|
9101
|
-
const configFile =
|
|
9102
|
-
|
|
9915
|
+
const configFile = path23.join(shadowDir, "config");
|
|
9916
|
+
spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
9103
9917
|
timeout: 3e3
|
|
9104
9918
|
});
|
|
9105
|
-
|
|
9919
|
+
spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
|
|
9106
9920
|
timeout: 3e3
|
|
9107
9921
|
});
|
|
9108
9922
|
try {
|
|
9109
|
-
|
|
9923
|
+
fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
9110
9924
|
} catch {
|
|
9111
9925
|
}
|
|
9112
9926
|
return true;
|
|
9113
9927
|
}
|
|
9114
9928
|
function buildGitEnv(cwd) {
|
|
9115
9929
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9116
|
-
const check =
|
|
9930
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9117
9931
|
env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
|
|
9118
9932
|
timeout: 2e3
|
|
9119
9933
|
});
|
|
@@ -9126,23 +9940,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9126
9940
|
let indexFile = null;
|
|
9127
9941
|
try {
|
|
9128
9942
|
const rawFilePath = extractFilePath(args);
|
|
9129
|
-
const absFilePath = rawFilePath &&
|
|
9943
|
+
const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
9130
9944
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
9131
9945
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9132
9946
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
9133
9947
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
9134
|
-
indexFile =
|
|
9948
|
+
indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
9135
9949
|
const shadowEnv = {
|
|
9136
9950
|
...process.env,
|
|
9137
9951
|
GIT_DIR: shadowDir,
|
|
9138
9952
|
GIT_WORK_TREE: cwd,
|
|
9139
9953
|
GIT_INDEX_FILE: indexFile
|
|
9140
9954
|
};
|
|
9141
|
-
|
|
9142
|
-
const treeRes =
|
|
9955
|
+
spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9956
|
+
const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9143
9957
|
const treeHash = treeRes.stdout?.toString().trim();
|
|
9144
9958
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
9145
|
-
const commitRes =
|
|
9959
|
+
const commitRes = spawnSync5(
|
|
9146
9960
|
"git",
|
|
9147
9961
|
["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
|
|
9148
9962
|
{ env: shadowEnv, timeout: GIT_TIMEOUT }
|
|
@@ -9154,7 +9968,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9154
9968
|
let capturedFiles = [];
|
|
9155
9969
|
let capturedDiff = null;
|
|
9156
9970
|
if (prevEntry) {
|
|
9157
|
-
const filesRes =
|
|
9971
|
+
const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
|
|
9158
9972
|
env: shadowEnv,
|
|
9159
9973
|
timeout: GIT_TIMEOUT
|
|
9160
9974
|
});
|
|
@@ -9164,7 +9978,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9164
9978
|
if (capturedFiles.length === 0) {
|
|
9165
9979
|
return prevEntry.hash;
|
|
9166
9980
|
}
|
|
9167
|
-
const diffRes =
|
|
9981
|
+
const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
|
|
9168
9982
|
env: shadowEnv,
|
|
9169
9983
|
timeout: GIT_TIMEOUT
|
|
9170
9984
|
});
|
|
@@ -9172,7 +9986,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9172
9986
|
capturedDiff = diffRes.stdout?.toString() || null;
|
|
9173
9987
|
}
|
|
9174
9988
|
} else {
|
|
9175
|
-
const filesRes =
|
|
9989
|
+
const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
|
|
9176
9990
|
env: shadowEnv,
|
|
9177
9991
|
timeout: GIT_TIMEOUT
|
|
9178
9992
|
});
|
|
@@ -9203,7 +10017,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9203
10017
|
writeStack(stack);
|
|
9204
10018
|
const entry = stack[stack.length - 1];
|
|
9205
10019
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
9206
|
-
|
|
10020
|
+
fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
9207
10021
|
if (shouldGc) {
|
|
9208
10022
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
9209
10023
|
}
|
|
@@ -9214,7 +10028,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9214
10028
|
} finally {
|
|
9215
10029
|
if (indexFile) {
|
|
9216
10030
|
try {
|
|
9217
|
-
|
|
10031
|
+
fs21.unlinkSync(indexFile);
|
|
9218
10032
|
} catch {
|
|
9219
10033
|
}
|
|
9220
10034
|
}
|
|
@@ -9226,14 +10040,14 @@ function getSnapshotHistory() {
|
|
|
9226
10040
|
function computeUndoDiff(hash, cwd) {
|
|
9227
10041
|
try {
|
|
9228
10042
|
const env = buildGitEnv(cwd);
|
|
9229
|
-
const statRes =
|
|
10043
|
+
const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
|
|
9230
10044
|
cwd,
|
|
9231
10045
|
env,
|
|
9232
10046
|
timeout: GIT_TIMEOUT
|
|
9233
10047
|
});
|
|
9234
10048
|
const stat = statRes.stdout?.toString().trim();
|
|
9235
10049
|
if (!stat || statRes.status !== 0) return null;
|
|
9236
|
-
const diffRes =
|
|
10050
|
+
const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
|
|
9237
10051
|
cwd,
|
|
9238
10052
|
env,
|
|
9239
10053
|
timeout: GIT_TIMEOUT
|
|
@@ -9252,7 +10066,7 @@ function applyUndo(hash, cwd) {
|
|
|
9252
10066
|
try {
|
|
9253
10067
|
const dir = cwd ?? process.cwd();
|
|
9254
10068
|
const env = buildGitEnv(dir);
|
|
9255
|
-
const restore =
|
|
10069
|
+
const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
9256
10070
|
cwd: dir,
|
|
9257
10071
|
env,
|
|
9258
10072
|
timeout: GIT_TIMEOUT
|
|
@@ -9264,7 +10078,7 @@ function applyUndo(hash, cwd) {
|
|
|
9264
10078
|
}
|
|
9265
10079
|
return false;
|
|
9266
10080
|
}
|
|
9267
|
-
const lsTree =
|
|
10081
|
+
const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
9268
10082
|
cwd: dir,
|
|
9269
10083
|
env,
|
|
9270
10084
|
timeout: GIT_TIMEOUT
|
|
@@ -9283,16 +10097,16 @@ function applyUndo(hash, cwd) {
|
|
|
9283
10097
|
`);
|
|
9284
10098
|
return false;
|
|
9285
10099
|
}
|
|
9286
|
-
const tracked =
|
|
9287
|
-
const untracked =
|
|
10100
|
+
const tracked = spawnSync5("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
10101
|
+
const untracked = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
9288
10102
|
cwd: dir,
|
|
9289
10103
|
env,
|
|
9290
10104
|
timeout: GIT_TIMEOUT
|
|
9291
10105
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
9292
10106
|
for (const file of [...tracked, ...untracked]) {
|
|
9293
|
-
const fullPath =
|
|
9294
|
-
if (!snapshotFiles.has(file) &&
|
|
9295
|
-
|
|
10107
|
+
const fullPath = path23.join(dir, file);
|
|
10108
|
+
if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
|
|
10109
|
+
fs21.unlinkSync(fullPath);
|
|
9296
10110
|
}
|
|
9297
10111
|
}
|
|
9298
10112
|
return true;
|
|
@@ -9301,52 +10115,233 @@ function applyUndo(hash, cwd) {
|
|
|
9301
10115
|
}
|
|
9302
10116
|
}
|
|
9303
10117
|
|
|
9304
|
-
// src/
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
10118
|
+
// src/skill-pin.ts
|
|
10119
|
+
import fs22 from "fs";
|
|
10120
|
+
import path24 from "path";
|
|
10121
|
+
import os18 from "os";
|
|
10122
|
+
import crypto4 from "crypto";
|
|
10123
|
+
function getPinsFilePath() {
|
|
10124
|
+
return path24.join(os18.homedir(), ".node9", "skill-pins.json");
|
|
10125
|
+
}
|
|
10126
|
+
var MAX_FILES = 5e3;
|
|
10127
|
+
var MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
10128
|
+
function sha256Bytes(buf) {
|
|
10129
|
+
return crypto4.createHash("sha256").update(buf).digest("hex");
|
|
10130
|
+
}
|
|
10131
|
+
function walkDir(root) {
|
|
10132
|
+
const out = [];
|
|
10133
|
+
let totalBytes = 0;
|
|
10134
|
+
const visit = (dir, relDir) => {
|
|
10135
|
+
if (out.length >= MAX_FILES) return;
|
|
10136
|
+
let entries;
|
|
10137
|
+
try {
|
|
10138
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
10139
|
+
} catch {
|
|
10140
|
+
return;
|
|
10141
|
+
}
|
|
10142
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
10143
|
+
for (const entry of entries) {
|
|
10144
|
+
if (out.length >= MAX_FILES) return;
|
|
10145
|
+
const full = path24.join(dir, entry.name);
|
|
10146
|
+
const rel = relDir ? path24.posix.join(relDir, entry.name) : entry.name;
|
|
10147
|
+
let lst;
|
|
9311
10148
|
try {
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
10149
|
+
lst = fs22.lstatSync(full);
|
|
10150
|
+
} catch {
|
|
10151
|
+
continue;
|
|
10152
|
+
}
|
|
10153
|
+
if (lst.isSymbolicLink()) continue;
|
|
10154
|
+
if (lst.isDirectory()) {
|
|
10155
|
+
visit(full, rel);
|
|
10156
|
+
continue;
|
|
10157
|
+
}
|
|
10158
|
+
if (!lst.isFile()) continue;
|
|
10159
|
+
if (totalBytes + lst.size > MAX_TOTAL_BYTES) continue;
|
|
10160
|
+
try {
|
|
10161
|
+
const buf = fs22.readFileSync(full);
|
|
10162
|
+
totalBytes += buf.length;
|
|
10163
|
+
out.push({ rel, hash: sha256Bytes(buf) });
|
|
10164
|
+
} catch {
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
10167
|
+
};
|
|
10168
|
+
visit(root, "");
|
|
10169
|
+
out.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
10170
|
+
return out.map((e) => `${e.rel}\0${e.hash}`);
|
|
10171
|
+
}
|
|
10172
|
+
function hashSkillRoot(absPath) {
|
|
10173
|
+
let lst;
|
|
10174
|
+
try {
|
|
10175
|
+
lst = fs22.lstatSync(absPath);
|
|
10176
|
+
} catch {
|
|
10177
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10178
|
+
}
|
|
10179
|
+
if (lst.isSymbolicLink()) return { exists: false, contentHash: "", fileCount: 0 };
|
|
10180
|
+
if (lst.isFile()) {
|
|
10181
|
+
try {
|
|
10182
|
+
return { exists: true, contentHash: sha256Bytes(fs22.readFileSync(absPath)), fileCount: 1 };
|
|
10183
|
+
} catch {
|
|
10184
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10185
|
+
}
|
|
10186
|
+
}
|
|
10187
|
+
if (lst.isDirectory()) {
|
|
10188
|
+
const entries = walkDir(absPath);
|
|
10189
|
+
const contentHash = crypto4.createHash("sha256").update(entries.join("\n")).digest("hex");
|
|
10190
|
+
return { exists: true, contentHash, fileCount: entries.length };
|
|
10191
|
+
}
|
|
10192
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10193
|
+
}
|
|
10194
|
+
function getRootKey(absPath) {
|
|
10195
|
+
return crypto4.createHash("sha256").update(absPath).digest("hex").slice(0, 16);
|
|
10196
|
+
}
|
|
10197
|
+
function readSkillPinsSafe() {
|
|
10198
|
+
const filePath = getPinsFilePath();
|
|
10199
|
+
try {
|
|
10200
|
+
const raw = fs22.readFileSync(filePath, "utf-8");
|
|
10201
|
+
if (!raw.trim()) return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
10202
|
+
const parsed = JSON.parse(raw);
|
|
10203
|
+
if (!parsed.roots || typeof parsed.roots !== "object" || Array.isArray(parsed.roots)) {
|
|
10204
|
+
return { ok: false, reason: "corrupt", detail: "invalid structure: missing roots object" };
|
|
10205
|
+
}
|
|
10206
|
+
return { ok: true, pins: { roots: parsed.roots } };
|
|
10207
|
+
} catch (err2) {
|
|
10208
|
+
if (err2.code === "ENOENT") return { ok: false, reason: "missing" };
|
|
10209
|
+
return { ok: false, reason: "corrupt", detail: String(err2) };
|
|
10210
|
+
}
|
|
10211
|
+
}
|
|
10212
|
+
function readSkillPins() {
|
|
10213
|
+
const result = readSkillPinsSafe();
|
|
10214
|
+
if (result.ok) return result.pins;
|
|
10215
|
+
if (result.reason === "missing") return { roots: {} };
|
|
10216
|
+
throw new Error(`[node9] skill pin file is corrupt: ${result.detail}`);
|
|
10217
|
+
}
|
|
10218
|
+
function writeSkillPins(data) {
|
|
10219
|
+
const filePath = getPinsFilePath();
|
|
10220
|
+
fs22.mkdirSync(path24.dirname(filePath), { recursive: true });
|
|
10221
|
+
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
10222
|
+
fs22.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
10223
|
+
fs22.renameSync(tmp, filePath);
|
|
10224
|
+
}
|
|
10225
|
+
function removePin(rootKey) {
|
|
10226
|
+
const pins = readSkillPins();
|
|
10227
|
+
delete pins.roots[rootKey];
|
|
10228
|
+
writeSkillPins(pins);
|
|
10229
|
+
}
|
|
10230
|
+
function clearAllPins() {
|
|
10231
|
+
writeSkillPins({ roots: {} });
|
|
10232
|
+
}
|
|
10233
|
+
function verifyAndPinRoots(roots) {
|
|
10234
|
+
const pinsRead = readSkillPinsSafe();
|
|
10235
|
+
if (!pinsRead.ok && pinsRead.reason === "corrupt") {
|
|
10236
|
+
return { kind: "corrupt", detail: pinsRead.detail };
|
|
10237
|
+
}
|
|
10238
|
+
const pins = pinsRead.ok ? pinsRead.pins : { roots: {} };
|
|
10239
|
+
let mutated = false;
|
|
10240
|
+
for (const rootPath of new Set(roots)) {
|
|
10241
|
+
const rootKey = getRootKey(rootPath);
|
|
10242
|
+
const current = hashSkillRoot(rootPath);
|
|
10243
|
+
const existing = pins.roots[rootKey];
|
|
10244
|
+
if (!existing) {
|
|
10245
|
+
pins.roots[rootKey] = {
|
|
10246
|
+
rootPath,
|
|
10247
|
+
exists: current.exists,
|
|
10248
|
+
contentHash: current.contentHash,
|
|
10249
|
+
fileCount: current.fileCount,
|
|
10250
|
+
pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10251
|
+
};
|
|
10252
|
+
mutated = true;
|
|
10253
|
+
continue;
|
|
10254
|
+
}
|
|
10255
|
+
if (existing.exists !== current.exists || existing.contentHash !== current.contentHash) {
|
|
10256
|
+
let summary;
|
|
10257
|
+
if (existing.exists && !current.exists) summary = `vanished: ${rootPath}`;
|
|
10258
|
+
else if (!existing.exists && current.exists) summary = `appeared: ${rootPath}`;
|
|
10259
|
+
else summary = `changed: ${rootPath}`;
|
|
10260
|
+
return { kind: "drift", changedRootKey: rootKey, changedRootPath: rootPath, summary };
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10263
|
+
if (mutated) writeSkillPins(pins);
|
|
10264
|
+
return { kind: "verified" };
|
|
10265
|
+
}
|
|
10266
|
+
function defaultSkillRoots(_cwd) {
|
|
10267
|
+
const marketplaces = path24.join(os18.homedir(), ".claude", "plugins", "marketplaces");
|
|
10268
|
+
const roots = [];
|
|
10269
|
+
let registries;
|
|
10270
|
+
try {
|
|
10271
|
+
registries = fs22.readdirSync(marketplaces, { withFileTypes: true });
|
|
10272
|
+
} catch {
|
|
10273
|
+
return [];
|
|
10274
|
+
}
|
|
10275
|
+
for (const registry of registries) {
|
|
10276
|
+
if (!registry.isDirectory()) continue;
|
|
10277
|
+
const pluginsDir = path24.join(marketplaces, registry.name, "plugins");
|
|
10278
|
+
let plugins;
|
|
10279
|
+
try {
|
|
10280
|
+
plugins = fs22.readdirSync(pluginsDir, { withFileTypes: true });
|
|
10281
|
+
} catch {
|
|
10282
|
+
continue;
|
|
10283
|
+
}
|
|
10284
|
+
for (const plugin of plugins) {
|
|
10285
|
+
if (!plugin.isDirectory()) continue;
|
|
10286
|
+
roots.push(path24.join(pluginsDir, plugin.name));
|
|
10287
|
+
}
|
|
10288
|
+
}
|
|
10289
|
+
return roots;
|
|
10290
|
+
}
|
|
10291
|
+
function resolveUserSkillRoot(entry, cwd) {
|
|
10292
|
+
if (!entry) return null;
|
|
10293
|
+
if (entry.startsWith("~/") || entry === "~") return path24.join(os18.homedir(), entry.slice(1));
|
|
10294
|
+
if (path24.isAbsolute(entry)) return entry;
|
|
10295
|
+
if (!cwd || !path24.isAbsolute(cwd)) return null;
|
|
10296
|
+
return path24.join(cwd, entry);
|
|
10297
|
+
}
|
|
10298
|
+
|
|
10299
|
+
// src/cli/commands/check.ts
|
|
10300
|
+
function sanitize2(value) {
|
|
10301
|
+
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
10302
|
+
}
|
|
10303
|
+
function registerCheckCommand(program2) {
|
|
10304
|
+
program2.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
10305
|
+
const processPayload = async (raw) => {
|
|
10306
|
+
try {
|
|
10307
|
+
if (!raw || raw.trim() === "") process.exit(0);
|
|
10308
|
+
let payload = JSON.parse(raw);
|
|
10309
|
+
try {
|
|
10310
|
+
payload = JSON.parse(raw);
|
|
10311
|
+
} catch (err2) {
|
|
10312
|
+
const tempConfig = getConfig();
|
|
10313
|
+
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
10314
|
+
const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
10315
|
+
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
10316
|
+
fs23.appendFileSync(
|
|
10317
|
+
logPath,
|
|
10318
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
10319
|
+
RAW: ${raw}
|
|
10320
|
+
`
|
|
10321
|
+
);
|
|
10322
|
+
}
|
|
10323
|
+
process.exit(0);
|
|
10324
|
+
}
|
|
10325
|
+
const config = getConfig(payload.cwd || void 0);
|
|
10326
|
+
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
10327
|
+
try {
|
|
10328
|
+
const scriptPath = process.argv[1];
|
|
10329
|
+
if (typeof scriptPath !== "string" || !path25.isAbsolute(scriptPath))
|
|
10330
|
+
throw new Error("node9: argv[1] is not an absolute path");
|
|
10331
|
+
const resolvedScript = fs23.realpathSync(scriptPath);
|
|
10332
|
+
const packageDist = fs23.realpathSync(path25.resolve(__dirname, "../.."));
|
|
10333
|
+
if (!resolvedScript.startsWith(packageDist + path25.sep) && resolvedScript !== packageDist)
|
|
10334
|
+
throw new Error(
|
|
10335
|
+
`node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
|
|
10336
|
+
);
|
|
10337
|
+
const safeEnv = { ...process.env };
|
|
10338
|
+
for (const key of [
|
|
10339
|
+
"NODE_OPTIONS",
|
|
10340
|
+
"LD_PRELOAD",
|
|
10341
|
+
"LD_LIBRARY_PATH",
|
|
10342
|
+
"DYLD_INSERT_LIBRARIES",
|
|
10343
|
+
"NODE_PATH",
|
|
10344
|
+
"ELECTRON_RUN_AS_NODE"
|
|
9350
10345
|
]) {
|
|
9351
10346
|
delete safeEnv[key];
|
|
9352
10347
|
}
|
|
@@ -9357,10 +10352,10 @@ RAW: ${raw}
|
|
|
9357
10352
|
});
|
|
9358
10353
|
d.unref();
|
|
9359
10354
|
} catch (spawnErr) {
|
|
9360
|
-
const logPath =
|
|
10355
|
+
const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
9361
10356
|
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
9362
10357
|
try {
|
|
9363
|
-
|
|
10358
|
+
fs23.appendFileSync(
|
|
9364
10359
|
logPath,
|
|
9365
10360
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
|
|
9366
10361
|
`
|
|
@@ -9370,10 +10365,10 @@ RAW: ${raw}
|
|
|
9370
10365
|
}
|
|
9371
10366
|
}
|
|
9372
10367
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9373
|
-
const logPath =
|
|
9374
|
-
if (!
|
|
9375
|
-
|
|
9376
|
-
|
|
10368
|
+
const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
10369
|
+
if (!fs23.existsSync(path25.dirname(logPath)))
|
|
10370
|
+
fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
|
|
10371
|
+
fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
9377
10372
|
`);
|
|
9378
10373
|
}
|
|
9379
10374
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9386,8 +10381,8 @@ RAW: ${raw}
|
|
|
9386
10381
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9387
10382
|
let ttyFd = null;
|
|
9388
10383
|
try {
|
|
9389
|
-
ttyFd =
|
|
9390
|
-
const writeTty = (line) =>
|
|
10384
|
+
ttyFd = fs23.openSync("/dev/tty", "w");
|
|
10385
|
+
const writeTty = (line) => fs23.writeSync(ttyFd, line + "\n");
|
|
9391
10386
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9392
10387
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9393
10388
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9406,7 +10401,7 @@ RAW: ${raw}
|
|
|
9406
10401
|
} finally {
|
|
9407
10402
|
if (ttyFd !== null)
|
|
9408
10403
|
try {
|
|
9409
|
-
|
|
10404
|
+
fs23.closeSync(ttyFd);
|
|
9410
10405
|
} catch {
|
|
9411
10406
|
}
|
|
9412
10407
|
}
|
|
@@ -9435,10 +10430,131 @@ RAW: ${raw}
|
|
|
9435
10430
|
return;
|
|
9436
10431
|
}
|
|
9437
10432
|
const meta = { agent, mcpServer };
|
|
10433
|
+
const skillPinCfg = config.policy.skillPinning;
|
|
10434
|
+
const rawSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
10435
|
+
const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
|
|
10436
|
+
if (skillPinCfg.enabled && safeSessionId) {
|
|
10437
|
+
try {
|
|
10438
|
+
const sessionsDir = path25.join(os19.homedir(), ".node9", "skill-sessions");
|
|
10439
|
+
const flagPath = path25.join(sessionsDir, `${safeSessionId}.json`);
|
|
10440
|
+
let flag = null;
|
|
10441
|
+
try {
|
|
10442
|
+
flag = JSON.parse(fs23.readFileSync(flagPath, "utf-8"));
|
|
10443
|
+
} catch {
|
|
10444
|
+
}
|
|
10445
|
+
const writeFlag = (data2) => {
|
|
10446
|
+
try {
|
|
10447
|
+
fs23.mkdirSync(sessionsDir, { recursive: true });
|
|
10448
|
+
fs23.writeFileSync(
|
|
10449
|
+
flagPath,
|
|
10450
|
+
JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
10451
|
+
{ mode: 384 }
|
|
10452
|
+
);
|
|
10453
|
+
} catch {
|
|
10454
|
+
}
|
|
10455
|
+
};
|
|
10456
|
+
const sendSkillWarn = (detail, recoveryCmd) => {
|
|
10457
|
+
let ttyFd = null;
|
|
10458
|
+
try {
|
|
10459
|
+
ttyFd = fs23.openSync("/dev/tty", "w");
|
|
10460
|
+
const w = (line) => fs23.writeSync(ttyFd, line + "\n");
|
|
10461
|
+
w(chalk5.yellow(`
|
|
10462
|
+
\u26A0\uFE0F Node9: installed skill drift detected`));
|
|
10463
|
+
w(chalk5.gray(` ${detail}`));
|
|
10464
|
+
w(
|
|
10465
|
+
chalk5.gray(
|
|
10466
|
+
` If you updated a plugin, acknowledge the change to clear this warning.`
|
|
10467
|
+
)
|
|
10468
|
+
);
|
|
10469
|
+
if (recoveryCmd) w(chalk5.green(` \u{1F4A1} Run: ${recoveryCmd}`));
|
|
10470
|
+
w("");
|
|
10471
|
+
} catch {
|
|
10472
|
+
} finally {
|
|
10473
|
+
if (ttyFd !== null)
|
|
10474
|
+
try {
|
|
10475
|
+
fs23.closeSync(ttyFd);
|
|
10476
|
+
} catch {
|
|
10477
|
+
}
|
|
10478
|
+
}
|
|
10479
|
+
};
|
|
10480
|
+
if (flag && flag.state === "quarantined" && skillPinCfg.mode === "block") {
|
|
10481
|
+
sendBlock(
|
|
10482
|
+
`Node9: session quarantined \u2014 installed skill changed. Open a separate terminal and run: node9 skill pin list (to see what changed) then: node9 skill pin update <rootKey> (to acknowledge). If you updated a plugin intentionally, this is expected.`,
|
|
10483
|
+
{
|
|
10484
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10485
|
+
recoveryCommand: "node9 skill pin list"
|
|
10486
|
+
}
|
|
10487
|
+
);
|
|
10488
|
+
return;
|
|
10489
|
+
}
|
|
10490
|
+
if (!flag || flag.state !== "verified" && flag.state !== "warned") {
|
|
10491
|
+
const absoluteCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
10492
|
+
const extraRoots = skillPinCfg.roots;
|
|
10493
|
+
const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
|
|
10494
|
+
const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
|
|
10495
|
+
const result2 = verifyAndPinRoots(roots);
|
|
10496
|
+
if (result2.kind === "corrupt") {
|
|
10497
|
+
if (skillPinCfg.mode === "block") {
|
|
10498
|
+
writeFlag({
|
|
10499
|
+
state: "quarantined",
|
|
10500
|
+
detail: `pin file corrupt: ${result2.detail}`
|
|
10501
|
+
});
|
|
10502
|
+
sendBlock("Node9: skill pin file is corrupt \u2014 fail-closed.", {
|
|
10503
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10504
|
+
recoveryCommand: "node9 skill pin reset"
|
|
10505
|
+
});
|
|
10506
|
+
return;
|
|
10507
|
+
}
|
|
10508
|
+
writeFlag({ state: "warned", detail: `pin file corrupt: ${result2.detail}` });
|
|
10509
|
+
sendSkillWarn(
|
|
10510
|
+
`Skill pin file is corrupt: ${result2.detail}`,
|
|
10511
|
+
"node9 skill pin reset"
|
|
10512
|
+
);
|
|
10513
|
+
} else if (result2.kind === "drift") {
|
|
10514
|
+
if (skillPinCfg.mode === "block") {
|
|
10515
|
+
writeFlag({ state: "quarantined", detail: result2.summary });
|
|
10516
|
+
sendBlock(
|
|
10517
|
+
`Node9: installed skill changed \u2014 ${result2.summary}. If you updated a plugin, open a separate terminal and run: node9 skill pin update ${result2.changedRootKey}`,
|
|
10518
|
+
{
|
|
10519
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10520
|
+
recoveryCommand: `node9 skill pin update ${result2.changedRootKey}`
|
|
10521
|
+
}
|
|
10522
|
+
);
|
|
10523
|
+
return;
|
|
10524
|
+
}
|
|
10525
|
+
writeFlag({ state: "warned", detail: result2.summary });
|
|
10526
|
+
sendSkillWarn(result2.summary, `node9 skill pin update ${result2.changedRootKey}`);
|
|
10527
|
+
} else {
|
|
10528
|
+
writeFlag({ state: "verified" });
|
|
10529
|
+
}
|
|
10530
|
+
try {
|
|
10531
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
10532
|
+
for (const name of fs23.readdirSync(sessionsDir)) {
|
|
10533
|
+
const p = path25.join(sessionsDir, name);
|
|
10534
|
+
try {
|
|
10535
|
+
if (fs23.statSync(p).mtimeMs < cutoff) fs23.unlinkSync(p);
|
|
10536
|
+
} catch {
|
|
10537
|
+
}
|
|
10538
|
+
}
|
|
10539
|
+
} catch {
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10542
|
+
} catch (err2) {
|
|
10543
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
10544
|
+
try {
|
|
10545
|
+
const dbg = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
10546
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
10547
|
+
fs23.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
|
|
10548
|
+
`);
|
|
10549
|
+
} catch {
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
}
|
|
10553
|
+
}
|
|
9438
10554
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9439
10555
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9440
10556
|
}
|
|
9441
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
10557
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9442
10558
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9443
10559
|
cwd: safeCwdForAuth
|
|
9444
10560
|
});
|
|
@@ -9450,12 +10566,12 @@ RAW: ${raw}
|
|
|
9450
10566
|
}
|
|
9451
10567
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9452
10568
|
try {
|
|
9453
|
-
const tty =
|
|
9454
|
-
|
|
10569
|
+
const tty = fs23.openSync("/dev/tty", "w");
|
|
10570
|
+
fs23.writeSync(
|
|
9455
10571
|
tty,
|
|
9456
10572
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9457
10573
|
);
|
|
9458
|
-
|
|
10574
|
+
fs23.closeSync(tty);
|
|
9459
10575
|
} catch {
|
|
9460
10576
|
}
|
|
9461
10577
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9482,9 +10598,9 @@ RAW: ${raw}
|
|
|
9482
10598
|
});
|
|
9483
10599
|
} catch (err2) {
|
|
9484
10600
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9485
|
-
const logPath =
|
|
10601
|
+
const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
9486
10602
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9487
|
-
|
|
10603
|
+
fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9488
10604
|
`);
|
|
9489
10605
|
}
|
|
9490
10606
|
process.exit(0);
|
|
@@ -9521,9 +10637,9 @@ RAW: ${raw}
|
|
|
9521
10637
|
init_audit();
|
|
9522
10638
|
init_config();
|
|
9523
10639
|
init_policy();
|
|
9524
|
-
import
|
|
9525
|
-
import
|
|
9526
|
-
import
|
|
10640
|
+
import fs24 from "fs";
|
|
10641
|
+
import path26 from "path";
|
|
10642
|
+
import os20 from "os";
|
|
9527
10643
|
init_daemon();
|
|
9528
10644
|
|
|
9529
10645
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9596,10 +10712,10 @@ function registerLogCommand(program2) {
|
|
|
9596
10712
|
decision: "allowed",
|
|
9597
10713
|
source: "post-hook"
|
|
9598
10714
|
};
|
|
9599
|
-
const logPath =
|
|
9600
|
-
if (!
|
|
9601
|
-
|
|
9602
|
-
|
|
10715
|
+
const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
|
|
10716
|
+
if (!fs24.existsSync(path26.dirname(logPath)))
|
|
10717
|
+
fs24.mkdirSync(path26.dirname(logPath), { recursive: true });
|
|
10718
|
+
fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
9603
10719
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9604
10720
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9605
10721
|
if (command) {
|
|
@@ -9632,7 +10748,7 @@ function registerLogCommand(program2) {
|
|
|
9632
10748
|
}
|
|
9633
10749
|
}
|
|
9634
10750
|
}
|
|
9635
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
10751
|
+
const safeCwd = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9636
10752
|
const config = getConfig(safeCwd);
|
|
9637
10753
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9638
10754
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9641,9 +10757,9 @@ function registerLogCommand(program2) {
|
|
|
9641
10757
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9642
10758
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9643
10759
|
`);
|
|
9644
|
-
const debugPath =
|
|
10760
|
+
const debugPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
9645
10761
|
try {
|
|
9646
|
-
|
|
10762
|
+
fs24.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9647
10763
|
`);
|
|
9648
10764
|
} catch {
|
|
9649
10765
|
}
|
|
@@ -9673,10 +10789,10 @@ init_config();
|
|
|
9673
10789
|
import chalk6 from "chalk";
|
|
9674
10790
|
|
|
9675
10791
|
// src/utils/https-fetch.ts
|
|
9676
|
-
import
|
|
10792
|
+
import https2 from "https";
|
|
9677
10793
|
function httpsFetch(url) {
|
|
9678
10794
|
return new Promise((resolve, reject) => {
|
|
9679
|
-
|
|
10795
|
+
https2.get(url, (res) => {
|
|
9680
10796
|
if (res.statusCode !== 200) {
|
|
9681
10797
|
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9682
10798
|
res.resume();
|
|
@@ -10044,13 +11160,13 @@ function registerConfigShowCommand(program2) {
|
|
|
10044
11160
|
// src/cli/commands/doctor.ts
|
|
10045
11161
|
init_daemon();
|
|
10046
11162
|
import chalk7 from "chalk";
|
|
10047
|
-
import
|
|
10048
|
-
import
|
|
10049
|
-
import
|
|
11163
|
+
import fs25 from "fs";
|
|
11164
|
+
import path27 from "path";
|
|
11165
|
+
import os21 from "os";
|
|
10050
11166
|
import { execSync as execSync2 } from "child_process";
|
|
10051
11167
|
function registerDoctorCommand(program2, version2) {
|
|
10052
11168
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
10053
|
-
const homeDir2 =
|
|
11169
|
+
const homeDir2 = os21.homedir();
|
|
10054
11170
|
let failures = 0;
|
|
10055
11171
|
function pass(msg) {
|
|
10056
11172
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -10099,10 +11215,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10099
11215
|
);
|
|
10100
11216
|
}
|
|
10101
11217
|
section("Configuration");
|
|
10102
|
-
const globalConfigPath =
|
|
10103
|
-
if (
|
|
11218
|
+
const globalConfigPath = path27.join(homeDir2, ".node9", "config.json");
|
|
11219
|
+
if (fs25.existsSync(globalConfigPath)) {
|
|
10104
11220
|
try {
|
|
10105
|
-
JSON.parse(
|
|
11221
|
+
JSON.parse(fs25.readFileSync(globalConfigPath, "utf-8"));
|
|
10106
11222
|
pass("~/.node9/config.json found and valid");
|
|
10107
11223
|
} catch {
|
|
10108
11224
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -10110,10 +11226,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10110
11226
|
} else {
|
|
10111
11227
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
10112
11228
|
}
|
|
10113
|
-
const projectConfigPath =
|
|
10114
|
-
if (
|
|
11229
|
+
const projectConfigPath = path27.join(process.cwd(), "node9.config.json");
|
|
11230
|
+
if (fs25.existsSync(projectConfigPath)) {
|
|
10115
11231
|
try {
|
|
10116
|
-
JSON.parse(
|
|
11232
|
+
JSON.parse(fs25.readFileSync(projectConfigPath, "utf-8"));
|
|
10117
11233
|
pass("node9.config.json found and valid (project)");
|
|
10118
11234
|
} catch {
|
|
10119
11235
|
fail(
|
|
@@ -10122,8 +11238,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10122
11238
|
);
|
|
10123
11239
|
}
|
|
10124
11240
|
}
|
|
10125
|
-
const credsPath =
|
|
10126
|
-
if (
|
|
11241
|
+
const credsPath = path27.join(homeDir2, ".node9", "credentials.json");
|
|
11242
|
+
if (fs25.existsSync(credsPath)) {
|
|
10127
11243
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
10128
11244
|
} else {
|
|
10129
11245
|
warn(
|
|
@@ -10132,10 +11248,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10132
11248
|
);
|
|
10133
11249
|
}
|
|
10134
11250
|
section("Agent Hooks");
|
|
10135
|
-
const claudeSettingsPath =
|
|
10136
|
-
if (
|
|
11251
|
+
const claudeSettingsPath = path27.join(homeDir2, ".claude", "settings.json");
|
|
11252
|
+
if (fs25.existsSync(claudeSettingsPath)) {
|
|
10137
11253
|
try {
|
|
10138
|
-
const cs = JSON.parse(
|
|
11254
|
+
const cs = JSON.parse(fs25.readFileSync(claudeSettingsPath, "utf-8"));
|
|
10139
11255
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
10140
11256
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10141
11257
|
);
|
|
@@ -10151,10 +11267,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10151
11267
|
} else {
|
|
10152
11268
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
10153
11269
|
}
|
|
10154
|
-
const geminiSettingsPath =
|
|
10155
|
-
if (
|
|
11270
|
+
const geminiSettingsPath = path27.join(homeDir2, ".gemini", "settings.json");
|
|
11271
|
+
if (fs25.existsSync(geminiSettingsPath)) {
|
|
10156
11272
|
try {
|
|
10157
|
-
const gs = JSON.parse(
|
|
11273
|
+
const gs = JSON.parse(fs25.readFileSync(geminiSettingsPath, "utf-8"));
|
|
10158
11274
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
10159
11275
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10160
11276
|
);
|
|
@@ -10170,10 +11286,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10170
11286
|
} else {
|
|
10171
11287
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
10172
11288
|
}
|
|
10173
|
-
const cursorHooksPath =
|
|
10174
|
-
if (
|
|
11289
|
+
const cursorHooksPath = path27.join(homeDir2, ".cursor", "hooks.json");
|
|
11290
|
+
if (fs25.existsSync(cursorHooksPath)) {
|
|
10175
11291
|
try {
|
|
10176
|
-
const cur = JSON.parse(
|
|
11292
|
+
const cur = JSON.parse(fs25.readFileSync(cursorHooksPath, "utf-8"));
|
|
10177
11293
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
10178
11294
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
10179
11295
|
);
|
|
@@ -10211,9 +11327,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10211
11327
|
|
|
10212
11328
|
// src/cli/commands/audit.ts
|
|
10213
11329
|
import chalk8 from "chalk";
|
|
10214
|
-
import
|
|
10215
|
-
import
|
|
10216
|
-
import
|
|
11330
|
+
import fs26 from "fs";
|
|
11331
|
+
import path28 from "path";
|
|
11332
|
+
import os22 from "os";
|
|
10217
11333
|
function formatRelativeTime(timestamp) {
|
|
10218
11334
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
10219
11335
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -10226,14 +11342,14 @@ function formatRelativeTime(timestamp) {
|
|
|
10226
11342
|
}
|
|
10227
11343
|
function registerAuditCommand(program2) {
|
|
10228
11344
|
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) => {
|
|
10229
|
-
const logPath =
|
|
10230
|
-
if (!
|
|
11345
|
+
const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
|
|
11346
|
+
if (!fs26.existsSync(logPath)) {
|
|
10231
11347
|
console.log(
|
|
10232
11348
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
10233
11349
|
);
|
|
10234
11350
|
return;
|
|
10235
11351
|
}
|
|
10236
|
-
const raw =
|
|
11352
|
+
const raw = fs26.readFileSync(logPath, "utf-8");
|
|
10237
11353
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
10238
11354
|
let entries = lines.flatMap((line) => {
|
|
10239
11355
|
try {
|
|
@@ -10287,9 +11403,9 @@ function registerAuditCommand(program2) {
|
|
|
10287
11403
|
|
|
10288
11404
|
// src/cli/commands/report.ts
|
|
10289
11405
|
import chalk9 from "chalk";
|
|
10290
|
-
import
|
|
10291
|
-
import
|
|
10292
|
-
import
|
|
11406
|
+
import fs27 from "fs";
|
|
11407
|
+
import path29 from "path";
|
|
11408
|
+
import os23 from "os";
|
|
10293
11409
|
var TEST_COMMAND_RE3 = /(?:^|\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;
|
|
10294
11410
|
function buildTestTimestamps(allEntries) {
|
|
10295
11411
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -10336,8 +11452,8 @@ function getDateRange(period) {
|
|
|
10336
11452
|
}
|
|
10337
11453
|
}
|
|
10338
11454
|
function parseAuditLog(logPath) {
|
|
10339
|
-
if (!
|
|
10340
|
-
const raw =
|
|
11455
|
+
if (!fs27.existsSync(logPath)) return [];
|
|
11456
|
+
const raw = fs27.readFileSync(logPath, "utf-8");
|
|
10341
11457
|
return raw.split("\n").flatMap((line) => {
|
|
10342
11458
|
if (!line.trim()) return [];
|
|
10343
11459
|
try {
|
|
@@ -10363,9 +11479,9 @@ function colorBar(value, max, width) {
|
|
|
10363
11479
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
10364
11480
|
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
10365
11481
|
}
|
|
10366
|
-
function pct(
|
|
11482
|
+
function pct(num3, total) {
|
|
10367
11483
|
if (total === 0) return "\u2013";
|
|
10368
|
-
return Math.round(
|
|
11484
|
+
return Math.round(num3 / total * 100) + "%";
|
|
10369
11485
|
}
|
|
10370
11486
|
function fmtDate(d) {
|
|
10371
11487
|
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
@@ -10406,11 +11522,11 @@ function loadClaudeCost(start, end) {
|
|
|
10406
11522
|
inputTokens: 0,
|
|
10407
11523
|
cacheReadTokens: 0
|
|
10408
11524
|
};
|
|
10409
|
-
const projectsDir =
|
|
10410
|
-
if (!
|
|
11525
|
+
const projectsDir = path29.join(os23.homedir(), ".claude", "projects");
|
|
11526
|
+
if (!fs27.existsSync(projectsDir)) return empty;
|
|
10411
11527
|
let dirs;
|
|
10412
11528
|
try {
|
|
10413
|
-
dirs =
|
|
11529
|
+
dirs = fs27.readdirSync(projectsDir);
|
|
10414
11530
|
} catch {
|
|
10415
11531
|
return empty;
|
|
10416
11532
|
}
|
|
@@ -10420,18 +11536,18 @@ function loadClaudeCost(start, end) {
|
|
|
10420
11536
|
const byDay = /* @__PURE__ */ new Map();
|
|
10421
11537
|
const byModel = /* @__PURE__ */ new Map();
|
|
10422
11538
|
for (const proj of dirs) {
|
|
10423
|
-
const projPath =
|
|
11539
|
+
const projPath = path29.join(projectsDir, proj);
|
|
10424
11540
|
let files;
|
|
10425
11541
|
try {
|
|
10426
|
-
const stat =
|
|
11542
|
+
const stat = fs27.statSync(projPath);
|
|
10427
11543
|
if (!stat.isDirectory()) continue;
|
|
10428
|
-
files =
|
|
11544
|
+
files = fs27.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
10429
11545
|
} catch {
|
|
10430
11546
|
continue;
|
|
10431
11547
|
}
|
|
10432
11548
|
for (const file of files) {
|
|
10433
11549
|
try {
|
|
10434
|
-
const raw =
|
|
11550
|
+
const raw = fs27.readFileSync(path29.join(projPath, file), "utf-8");
|
|
10435
11551
|
for (const line of raw.split("\n")) {
|
|
10436
11552
|
if (!line.trim()) continue;
|
|
10437
11553
|
let entry;
|
|
@@ -10474,7 +11590,7 @@ function registerReportCommand(program2) {
|
|
|
10474
11590
|
const period = ["today", "7d", "30d", "month"].includes(
|
|
10475
11591
|
options.period
|
|
10476
11592
|
) ? options.period : "7d";
|
|
10477
|
-
const logPath =
|
|
11593
|
+
const logPath = path29.join(os23.homedir(), ".node9", "audit.log");
|
|
10478
11594
|
const allEntries = parseAuditLog(logPath);
|
|
10479
11595
|
if (allEntries.length === 0) {
|
|
10480
11596
|
console.log(
|
|
@@ -10725,19 +11841,60 @@ init_daemon2();
|
|
|
10725
11841
|
init_daemon();
|
|
10726
11842
|
import chalk10 from "chalk";
|
|
10727
11843
|
import { spawn as spawn7 } from "child_process";
|
|
11844
|
+
var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
|
|
10728
11845
|
function registerDaemonCommand(program2) {
|
|
10729
|
-
program2.command("daemon").description("
|
|
11846
|
+
program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
10730
11847
|
"-w, --watch",
|
|
10731
11848
|
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
10732
11849
|
).action(
|
|
10733
11850
|
async (action, options) => {
|
|
10734
11851
|
const cmd = (action ?? "start").toLowerCase();
|
|
11852
|
+
if (cmd === "install") {
|
|
11853
|
+
const result = installDaemonService();
|
|
11854
|
+
if (!result.ok) {
|
|
11855
|
+
console.error(chalk10.red(`\u2717 ${result.reason}`));
|
|
11856
|
+
process.exit(1);
|
|
11857
|
+
}
|
|
11858
|
+
if (result.alreadyInstalled) {
|
|
11859
|
+
console.log(chalk10.green(`\u2713 Daemon service reinstalled (${result.platform})`));
|
|
11860
|
+
} else {
|
|
11861
|
+
console.log(chalk10.green(`\u2713 Daemon installed as login service (${result.platform})`));
|
|
11862
|
+
console.log(chalk10.gray(" The daemon will now start automatically on login."));
|
|
11863
|
+
}
|
|
11864
|
+
process.exit(0);
|
|
11865
|
+
}
|
|
11866
|
+
if (cmd === "uninstall") {
|
|
11867
|
+
const result = uninstallDaemonService();
|
|
11868
|
+
if (!result.ok) {
|
|
11869
|
+
console.error(chalk10.red(`\u2717 ${result.reason}`));
|
|
11870
|
+
process.exit(1);
|
|
11871
|
+
}
|
|
11872
|
+
console.log(chalk10.green(`\u2713 Daemon service removed (${result.platform})`));
|
|
11873
|
+
console.log(chalk10.gray(" The daemon will no longer start automatically on login."));
|
|
11874
|
+
console.log(chalk10.gray(" To stop the running daemon: node9 daemon stop"));
|
|
11875
|
+
process.exit(0);
|
|
11876
|
+
}
|
|
10735
11877
|
if (cmd === "stop") return stopDaemon();
|
|
11878
|
+
if (cmd === "restart") {
|
|
11879
|
+
stopDaemon();
|
|
11880
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11881
|
+
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
11882
|
+
detached: true,
|
|
11883
|
+
stdio: "ignore",
|
|
11884
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
11885
|
+
});
|
|
11886
|
+
child.unref();
|
|
11887
|
+
if (child.pid) {
|
|
11888
|
+
console.log(chalk10.green(`\u2713 Daemon restarted (PID ${child.pid})`));
|
|
11889
|
+
} else {
|
|
11890
|
+
console.error(chalk10.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
|
|
11891
|
+
process.exit(1);
|
|
11892
|
+
}
|
|
11893
|
+
process.exit(0);
|
|
11894
|
+
}
|
|
10736
11895
|
if (cmd === "status") return daemonStatus();
|
|
10737
11896
|
if (cmd !== "start" && action !== void 0) {
|
|
10738
|
-
console.error(
|
|
10739
|
-
chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
|
|
10740
|
-
);
|
|
11897
|
+
console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
|
|
10741
11898
|
process.exit(1);
|
|
10742
11899
|
}
|
|
10743
11900
|
if (options.watch) {
|
|
@@ -10788,12 +11945,12 @@ function registerDaemonCommand(program2) {
|
|
|
10788
11945
|
init_core();
|
|
10789
11946
|
init_daemon();
|
|
10790
11947
|
import chalk11 from "chalk";
|
|
10791
|
-
import
|
|
10792
|
-
import
|
|
10793
|
-
import
|
|
11948
|
+
import fs28 from "fs";
|
|
11949
|
+
import path30 from "path";
|
|
11950
|
+
import os24 from "os";
|
|
10794
11951
|
function readJson2(filePath) {
|
|
10795
11952
|
try {
|
|
10796
|
-
if (
|
|
11953
|
+
if (fs28.existsSync(filePath)) return JSON.parse(fs28.readFileSync(filePath, "utf-8"));
|
|
10797
11954
|
} catch {
|
|
10798
11955
|
}
|
|
10799
11956
|
return null;
|
|
@@ -10858,28 +12015,28 @@ function registerStatusCommand(program2) {
|
|
|
10858
12015
|
console.log("");
|
|
10859
12016
|
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10860
12017
|
console.log(` Mode: ${modeLabel}`);
|
|
10861
|
-
const projectConfig =
|
|
10862
|
-
const globalConfig =
|
|
12018
|
+
const projectConfig = path30.join(process.cwd(), "node9.config.json");
|
|
12019
|
+
const globalConfig = path30.join(os24.homedir(), ".node9", "config.json");
|
|
10863
12020
|
console.log(
|
|
10864
|
-
` Local: ${
|
|
12021
|
+
` Local: ${fs28.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10865
12022
|
);
|
|
10866
12023
|
console.log(
|
|
10867
|
-
` Global: ${
|
|
12024
|
+
` Global: ${fs28.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10868
12025
|
);
|
|
10869
12026
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10870
12027
|
console.log(
|
|
10871
12028
|
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10872
12029
|
);
|
|
10873
12030
|
}
|
|
10874
|
-
const homeDir2 =
|
|
12031
|
+
const homeDir2 = os24.homedir();
|
|
10875
12032
|
const claudeSettings = readJson2(
|
|
10876
|
-
|
|
12033
|
+
path30.join(homeDir2, ".claude", "settings.json")
|
|
10877
12034
|
);
|
|
10878
|
-
const claudeConfig = readJson2(
|
|
12035
|
+
const claudeConfig = readJson2(path30.join(homeDir2, ".claude.json"));
|
|
10879
12036
|
const geminiSettings = readJson2(
|
|
10880
|
-
|
|
12037
|
+
path30.join(homeDir2, ".gemini", "settings.json")
|
|
10881
12038
|
);
|
|
10882
|
-
const cursorConfig = readJson2(
|
|
12039
|
+
const cursorConfig = readJson2(path30.join(homeDir2, ".cursor", "mcp.json"));
|
|
10883
12040
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10884
12041
|
if (agentFound) {
|
|
10885
12042
|
console.log("");
|
|
@@ -10939,11 +12096,12 @@ function registerStatusCommand(program2) {
|
|
|
10939
12096
|
// src/cli/commands/init.ts
|
|
10940
12097
|
init_core();
|
|
10941
12098
|
import chalk12 from "chalk";
|
|
10942
|
-
import
|
|
10943
|
-
import
|
|
10944
|
-
import
|
|
10945
|
-
import
|
|
12099
|
+
import fs29 from "fs";
|
|
12100
|
+
import path31 from "path";
|
|
12101
|
+
import os25 from "os";
|
|
12102
|
+
import https3 from "https";
|
|
10946
12103
|
init_shields();
|
|
12104
|
+
init_service();
|
|
10947
12105
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10948
12106
|
function fireTelemetryPing(agents) {
|
|
10949
12107
|
try {
|
|
@@ -10953,7 +12111,7 @@ function fireTelemetryPing(agents) {
|
|
|
10953
12111
|
os: process.platform,
|
|
10954
12112
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10955
12113
|
});
|
|
10956
|
-
const req =
|
|
12114
|
+
const req = https3.request(
|
|
10957
12115
|
{
|
|
10958
12116
|
hostname: "api.node9.ai",
|
|
10959
12117
|
path: "/api/v1/telemetry",
|
|
@@ -11000,15 +12158,15 @@ function registerInitCommand(program2) {
|
|
|
11000
12158
|
}
|
|
11001
12159
|
console.log("");
|
|
11002
12160
|
}
|
|
11003
|
-
const configPath =
|
|
11004
|
-
if (
|
|
12161
|
+
const configPath = path31.join(os25.homedir(), ".node9", "config.json");
|
|
12162
|
+
if (fs29.existsSync(configPath) && !options.force) {
|
|
11005
12163
|
try {
|
|
11006
|
-
const existing = JSON.parse(
|
|
12164
|
+
const existing = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
|
|
11007
12165
|
const settings = existing.settings ?? {};
|
|
11008
12166
|
if (settings.mode !== chosenMode) {
|
|
11009
12167
|
settings.mode = chosenMode;
|
|
11010
12168
|
existing.settings = settings;
|
|
11011
|
-
|
|
12169
|
+
fs29.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
11012
12170
|
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
11013
12171
|
} else {
|
|
11014
12172
|
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
@@ -11021,9 +12179,9 @@ function registerInitCommand(program2) {
|
|
|
11021
12179
|
...DEFAULT_CONFIG,
|
|
11022
12180
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
11023
12181
|
};
|
|
11024
|
-
const dir =
|
|
11025
|
-
if (!
|
|
11026
|
-
|
|
12182
|
+
const dir = path31.dirname(configPath);
|
|
12183
|
+
if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
|
|
12184
|
+
fs29.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
11027
12185
|
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
11028
12186
|
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
11029
12187
|
}
|
|
@@ -11035,9 +12193,13 @@ function registerInitCommand(program2) {
|
|
|
11035
12193
|
);
|
|
11036
12194
|
if (found.length === 0) {
|
|
11037
12195
|
console.log(
|
|
11038
|
-
chalk12.gray(
|
|
12196
|
+
chalk12.gray(
|
|
12197
|
+
"No AI agents detected. Install Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
|
|
12198
|
+
)
|
|
12199
|
+
);
|
|
12200
|
+
console.log(
|
|
12201
|
+
chalk12.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
|
|
11039
12202
|
);
|
|
11040
|
-
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
11041
12203
|
return;
|
|
11042
12204
|
}
|
|
11043
12205
|
console.log(chalk12.bold("Detected agents:"));
|
|
@@ -11051,6 +12213,32 @@ function registerInitCommand(program2) {
|
|
|
11051
12213
|
else if (agent === "gemini") await setupGemini();
|
|
11052
12214
|
else if (agent === "cursor") await setupCursor();
|
|
11053
12215
|
else if (agent === "codex") await setupCodex();
|
|
12216
|
+
else if (agent === "windsurf") await setupWindsurf();
|
|
12217
|
+
else if (agent === "vscode") await setupVSCode();
|
|
12218
|
+
console.log("");
|
|
12219
|
+
}
|
|
12220
|
+
if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
|
|
12221
|
+
const alreadyInstalled = isDaemonServiceInstalled();
|
|
12222
|
+
if (!alreadyInstalled) {
|
|
12223
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
12224
|
+
const installService = await confirm3({
|
|
12225
|
+
message: "Install daemon as a login service? (starts automatically on login)",
|
|
12226
|
+
default: true
|
|
12227
|
+
});
|
|
12228
|
+
if (installService) {
|
|
12229
|
+
const result = installDaemonService();
|
|
12230
|
+
if (result.ok) {
|
|
12231
|
+
console.log(
|
|
12232
|
+
chalk12.green(` \u2713 Daemon installed as login service (${result.platform})`)
|
|
12233
|
+
);
|
|
12234
|
+
} else {
|
|
12235
|
+
console.log(chalk12.yellow(` \u26A0\uFE0F Could not install service: ${result.reason}`));
|
|
12236
|
+
console.log(chalk12.gray(" You can try again later with: node9 daemon install"));
|
|
12237
|
+
}
|
|
12238
|
+
}
|
|
12239
|
+
} else {
|
|
12240
|
+
console.log(chalk12.green(" \u2713 Daemon login service already installed"));
|
|
12241
|
+
}
|
|
11054
12242
|
console.log("");
|
|
11055
12243
|
}
|
|
11056
12244
|
{
|
|
@@ -11077,7 +12265,7 @@ function registerInitCommand(program2) {
|
|
|
11077
12265
|
}
|
|
11078
12266
|
|
|
11079
12267
|
// src/cli/commands/undo.ts
|
|
11080
|
-
import
|
|
12268
|
+
import path32 from "path";
|
|
11081
12269
|
import chalk14 from "chalk";
|
|
11082
12270
|
|
|
11083
12271
|
// src/tui/undo-navigator.ts
|
|
@@ -11236,7 +12424,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
11236
12424
|
let dir = startDir;
|
|
11237
12425
|
while (true) {
|
|
11238
12426
|
if (cwds.has(dir)) return dir;
|
|
11239
|
-
const parent =
|
|
12427
|
+
const parent = path32.dirname(dir);
|
|
11240
12428
|
if (parent === dir) return null;
|
|
11241
12429
|
dir = parent;
|
|
11242
12430
|
}
|
|
@@ -11365,7 +12553,7 @@ function registerUndoCommand(program2) {
|
|
|
11365
12553
|
// src/cli/commands/watch.ts
|
|
11366
12554
|
init_daemon();
|
|
11367
12555
|
import chalk15 from "chalk";
|
|
11368
|
-
import { spawn as spawn8, spawnSync as
|
|
12556
|
+
import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
|
|
11369
12557
|
function registerWatchCommand(program2) {
|
|
11370
12558
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
11371
12559
|
let port = DAEMON_PORT;
|
|
@@ -11411,7 +12599,7 @@ function registerWatchCommand(program2) {
|
|
|
11411
12599
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
11412
12600
|
)
|
|
11413
12601
|
);
|
|
11414
|
-
const result =
|
|
12602
|
+
const result = spawnSync6(cmd, args, {
|
|
11415
12603
|
stdio: "inherit",
|
|
11416
12604
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
11417
12605
|
});
|
|
@@ -11432,12 +12620,12 @@ import { execa as execa2 } from "execa";
|
|
|
11432
12620
|
init_provenance();
|
|
11433
12621
|
|
|
11434
12622
|
// src/mcp-pin.ts
|
|
11435
|
-
import
|
|
11436
|
-
import
|
|
11437
|
-
import
|
|
11438
|
-
import
|
|
11439
|
-
function
|
|
11440
|
-
return
|
|
12623
|
+
import fs30 from "fs";
|
|
12624
|
+
import path33 from "path";
|
|
12625
|
+
import os26 from "os";
|
|
12626
|
+
import crypto5 from "crypto";
|
|
12627
|
+
function getPinsFilePath2() {
|
|
12628
|
+
return path33.join(os26.homedir(), ".node9", "mcp-pins.json");
|
|
11441
12629
|
}
|
|
11442
12630
|
function hashToolDefinitions(tools) {
|
|
11443
12631
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -11446,15 +12634,15 @@ function hashToolDefinitions(tools) {
|
|
|
11446
12634
|
return nameA.localeCompare(nameB);
|
|
11447
12635
|
});
|
|
11448
12636
|
const canonical = JSON.stringify(sorted);
|
|
11449
|
-
return
|
|
12637
|
+
return crypto5.createHash("sha256").update(canonical).digest("hex");
|
|
11450
12638
|
}
|
|
11451
12639
|
function getServerKey(upstreamCommand) {
|
|
11452
|
-
return
|
|
12640
|
+
return crypto5.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
|
|
11453
12641
|
}
|
|
11454
12642
|
function readMcpPinsSafe() {
|
|
11455
|
-
const filePath =
|
|
12643
|
+
const filePath = getPinsFilePath2();
|
|
11456
12644
|
try {
|
|
11457
|
-
const raw =
|
|
12645
|
+
const raw = fs30.readFileSync(filePath, "utf-8");
|
|
11458
12646
|
if (!raw.trim()) {
|
|
11459
12647
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
11460
12648
|
}
|
|
@@ -11477,11 +12665,11 @@ function readMcpPins() {
|
|
|
11477
12665
|
throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
|
|
11478
12666
|
}
|
|
11479
12667
|
function writeMcpPins(data) {
|
|
11480
|
-
const filePath =
|
|
11481
|
-
|
|
11482
|
-
const tmp = `${filePath}.${
|
|
11483
|
-
|
|
11484
|
-
|
|
12668
|
+
const filePath = getPinsFilePath2();
|
|
12669
|
+
fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
|
|
12670
|
+
const tmp = `${filePath}.${crypto5.randomBytes(6).toString("hex")}.tmp`;
|
|
12671
|
+
fs30.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
12672
|
+
fs30.renameSync(tmp, filePath);
|
|
11485
12673
|
}
|
|
11486
12674
|
function checkPin(serverKey, currentHash) {
|
|
11487
12675
|
const result = readMcpPinsSafe();
|
|
@@ -11504,12 +12692,12 @@ function updatePin(serverKey, label, toolsHash, toolNames) {
|
|
|
11504
12692
|
};
|
|
11505
12693
|
writeMcpPins(pins);
|
|
11506
12694
|
}
|
|
11507
|
-
function
|
|
12695
|
+
function removePin2(serverKey) {
|
|
11508
12696
|
const pins = readMcpPins();
|
|
11509
12697
|
delete pins.servers[serverKey];
|
|
11510
12698
|
writeMcpPins(pins);
|
|
11511
12699
|
}
|
|
11512
|
-
function
|
|
12700
|
+
function clearAllPins2() {
|
|
11513
12701
|
writeMcpPins({ servers: {} });
|
|
11514
12702
|
}
|
|
11515
12703
|
|
|
@@ -11853,9 +13041,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11853
13041
|
|
|
11854
13042
|
// src/mcp-server/index.ts
|
|
11855
13043
|
import readline4 from "readline";
|
|
11856
|
-
import
|
|
11857
|
-
import
|
|
11858
|
-
import
|
|
13044
|
+
import fs31 from "fs";
|
|
13045
|
+
import os27 from "os";
|
|
13046
|
+
import path34 from "path";
|
|
11859
13047
|
init_core();
|
|
11860
13048
|
init_daemon();
|
|
11861
13049
|
init_shields();
|
|
@@ -12030,13 +13218,13 @@ function handleStatus() {
|
|
|
12030
13218
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
12031
13219
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
12032
13220
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
12033
|
-
const projectConfig =
|
|
12034
|
-
const globalConfig =
|
|
13221
|
+
const projectConfig = path34.join(process.cwd(), "node9.config.json");
|
|
13222
|
+
const globalConfig = path34.join(os27.homedir(), ".node9", "config.json");
|
|
12035
13223
|
lines.push(
|
|
12036
|
-
`Project config (node9.config.json): ${
|
|
13224
|
+
`Project config (node9.config.json): ${fs31.existsSync(projectConfig) ? "present" : "not found"}`
|
|
12037
13225
|
);
|
|
12038
13226
|
lines.push(
|
|
12039
|
-
`Global config (~/.node9/config.json): ${
|
|
13227
|
+
`Global config (~/.node9/config.json): ${fs31.existsSync(globalConfig) ? "present" : "not found"}`
|
|
12040
13228
|
);
|
|
12041
13229
|
return lines.join("\n");
|
|
12042
13230
|
}
|
|
@@ -12110,21 +13298,21 @@ function handleShieldDisable(args) {
|
|
|
12110
13298
|
writeActiveShields(active.filter((s) => s !== name));
|
|
12111
13299
|
return `Shield "${name}" disabled.`;
|
|
12112
13300
|
}
|
|
12113
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
13301
|
+
var GLOBAL_CONFIG_PATH2 = path34.join(os27.homedir(), ".node9", "config.json");
|
|
12114
13302
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
12115
13303
|
function readGlobalConfigRaw() {
|
|
12116
13304
|
try {
|
|
12117
|
-
if (
|
|
12118
|
-
return JSON.parse(
|
|
13305
|
+
if (fs31.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
13306
|
+
return JSON.parse(fs31.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
12119
13307
|
}
|
|
12120
13308
|
} catch {
|
|
12121
13309
|
}
|
|
12122
13310
|
return {};
|
|
12123
13311
|
}
|
|
12124
13312
|
function writeGlobalConfigRaw(data) {
|
|
12125
|
-
const dir =
|
|
12126
|
-
if (!
|
|
12127
|
-
|
|
13313
|
+
const dir = path34.dirname(GLOBAL_CONFIG_PATH2);
|
|
13314
|
+
if (!fs31.existsSync(dir)) fs31.mkdirSync(dir, { recursive: true });
|
|
13315
|
+
fs31.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
12128
13316
|
}
|
|
12129
13317
|
function handleApproverList() {
|
|
12130
13318
|
const config = getConfig();
|
|
@@ -12167,9 +13355,9 @@ function handleApproverSet(args) {
|
|
|
12167
13355
|
}
|
|
12168
13356
|
function handleAuditGet(args) {
|
|
12169
13357
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
12170
|
-
const auditPath =
|
|
12171
|
-
if (!
|
|
12172
|
-
const lines =
|
|
13358
|
+
const auditPath = path34.join(os27.homedir(), ".node9", "audit.log");
|
|
13359
|
+
if (!fs31.existsSync(auditPath)) return "No audit log found.";
|
|
13360
|
+
const lines = fs31.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
12173
13361
|
const recent = lines.slice(-limit);
|
|
12174
13362
|
const entries = recent.map((line) => {
|
|
12175
13363
|
try {
|
|
@@ -12467,7 +13655,7 @@ function registerMcpPinCommand(program2) {
|
|
|
12467
13655
|
process.exit(1);
|
|
12468
13656
|
}
|
|
12469
13657
|
const label = pins.servers[serverKey].label;
|
|
12470
|
-
|
|
13658
|
+
removePin2(serverKey);
|
|
12471
13659
|
console.log(chalk18.green(`
|
|
12472
13660
|
\u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
|
|
12473
13661
|
console.log(chalk18.gray(` Server: ${label}`));
|
|
@@ -12480,94 +13668,1115 @@ function registerMcpPinCommand(program2) {
|
|
|
12480
13668
|
return;
|
|
12481
13669
|
}
|
|
12482
13670
|
const count = result.ok ? Object.keys(result.pins.servers).length : "?";
|
|
12483
|
-
|
|
13671
|
+
clearAllPins2();
|
|
12484
13672
|
console.log(chalk18.green(`
|
|
12485
13673
|
\u{1F513} Cleared ${count} MCP pin(s).`));
|
|
12486
13674
|
console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
|
|
12487
13675
|
});
|
|
12488
13676
|
}
|
|
12489
13677
|
|
|
12490
|
-
// src/cli.ts
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
)
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
let existingCreds = {};
|
|
12503
|
-
try {
|
|
12504
|
-
if (fs31.existsSync(credPath)) {
|
|
12505
|
-
const raw = JSON.parse(fs31.readFileSync(credPath, "utf-8"));
|
|
12506
|
-
if (raw.apiKey) {
|
|
12507
|
-
existingCreds = {
|
|
12508
|
-
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
12509
|
-
};
|
|
12510
|
-
} else {
|
|
12511
|
-
existingCreds = raw;
|
|
12512
|
-
}
|
|
13678
|
+
// src/cli/commands/sync.ts
|
|
13679
|
+
init_sync();
|
|
13680
|
+
import chalk19 from "chalk";
|
|
13681
|
+
function registerSyncCommand(program2) {
|
|
13682
|
+
const policy = program2.command("policy").description("Manage cloud policy rules");
|
|
13683
|
+
policy.command("sync").description("Sync cloud policy rules to local cache (~/.node9/rules-cache.json)").action(async () => {
|
|
13684
|
+
process.stdout.write(chalk19.cyan("Syncing cloud policy rules\u2026"));
|
|
13685
|
+
const result = await runCloudSync();
|
|
13686
|
+
process.stdout.write("\n");
|
|
13687
|
+
if (!result.ok) {
|
|
13688
|
+
console.error(chalk19.red(`\u2717 ${result.reason}`));
|
|
13689
|
+
process.exit(1);
|
|
12513
13690
|
}
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
13691
|
+
console.log(
|
|
13692
|
+
chalk19.green(`\u2713 Synced ${result.rules} rule${result.rules === 1 ? "" : "s"} from cloud`)
|
|
13693
|
+
);
|
|
13694
|
+
console.log(chalk19.gray(` Cached at: ${result.fetchedAt}`));
|
|
13695
|
+
console.log(chalk19.gray(` File: ~/.node9/rules-cache.json`));
|
|
13696
|
+
});
|
|
13697
|
+
policy.command("show").description("List all cloud policy rules in the local cache").action(() => {
|
|
13698
|
+
const status = getCloudSyncStatus();
|
|
13699
|
+
if (!status.cached) {
|
|
13700
|
+
console.log(chalk19.yellow("\n No cloud rules cached \u2014 run: node9 policy sync\n"));
|
|
13701
|
+
return;
|
|
12525
13702
|
}
|
|
12526
|
-
|
|
12527
|
-
const
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
13703
|
+
const rules = getCloudRules() ?? [];
|
|
13704
|
+
const age = Math.round((Date.now() - new Date(status.fetchedAt).getTime()) / 6e4);
|
|
13705
|
+
console.log(
|
|
13706
|
+
chalk19.bold(`
|
|
13707
|
+
Cloud policy rules`) + chalk19.gray(
|
|
13708
|
+
` (${rules.length} rule${rules.length === 1 ? "" : "s"}, synced ${age}m ago)
|
|
13709
|
+
`
|
|
13710
|
+
)
|
|
13711
|
+
);
|
|
13712
|
+
if (rules.length === 0) {
|
|
13713
|
+
console.log(chalk19.gray(" No rules defined in cloud policy.\n"));
|
|
13714
|
+
return;
|
|
12536
13715
|
}
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
13716
|
+
for (const rule of rules) {
|
|
13717
|
+
const r = rule;
|
|
13718
|
+
const verdictColor = r.verdict === "block" ? chalk19.red : r.verdict === "allow" ? chalk19.green : chalk19.yellow;
|
|
13719
|
+
console.log(
|
|
13720
|
+
` ${verdictColor(
|
|
13721
|
+
String(r.verdict ?? "unknown").toUpperCase().padEnd(6)
|
|
13722
|
+
)} ${chalk19.white(String(r.name ?? "(unnamed)"))}`
|
|
13723
|
+
);
|
|
13724
|
+
if (r.reason) console.log(chalk19.gray(` ${String(r.reason)}`));
|
|
13725
|
+
}
|
|
13726
|
+
console.log("");
|
|
13727
|
+
});
|
|
13728
|
+
policy.command("status").description("Show current cloud policy cache status").action(() => {
|
|
13729
|
+
const s = getCloudSyncStatus();
|
|
13730
|
+
if (!s.cached) {
|
|
13731
|
+
console.log(chalk19.yellow("\n No cache yet \u2014 run: node9 policy sync\n"));
|
|
13732
|
+
} else {
|
|
13733
|
+
const age = Math.round((Date.now() - new Date(s.fetchedAt).getTime()) / 6e4);
|
|
13734
|
+
console.log(`
|
|
13735
|
+
Rules : ${chalk19.green(String(s.rules))} cloud rules loaded`);
|
|
13736
|
+
console.log(
|
|
13737
|
+
` Synced : ${chalk19.gray(`${age} minute${age === 1 ? "" : "s"} ago`)} (${s.fetchedAt})
|
|
13738
|
+
`
|
|
13739
|
+
);
|
|
13740
|
+
}
|
|
13741
|
+
});
|
|
13742
|
+
}
|
|
13743
|
+
|
|
13744
|
+
// src/cli/commands/agents.ts
|
|
13745
|
+
import chalk20 from "chalk";
|
|
13746
|
+
var SETUP_FN = {
|
|
13747
|
+
claude: setupClaude,
|
|
13748
|
+
gemini: setupGemini,
|
|
13749
|
+
cursor: setupCursor,
|
|
13750
|
+
codex: setupCodex,
|
|
13751
|
+
windsurf: setupWindsurf,
|
|
13752
|
+
vscode: setupVSCode
|
|
13753
|
+
};
|
|
13754
|
+
var TEARDOWN_FN = {
|
|
13755
|
+
claude: teardownClaude,
|
|
13756
|
+
gemini: teardownGemini,
|
|
13757
|
+
cursor: teardownCursor,
|
|
13758
|
+
codex: teardownCodex,
|
|
13759
|
+
windsurf: teardownWindsurf,
|
|
13760
|
+
vscode: teardownVSCode
|
|
13761
|
+
};
|
|
13762
|
+
var AGENT_NAMES = Object.keys(SETUP_FN);
|
|
13763
|
+
function registerAgentsCommand(program2) {
|
|
13764
|
+
const agents = program2.command("agents").description("List and manage AI agent integrations");
|
|
13765
|
+
agents.command("list").description("Show all supported agents and their Node9 status").action(() => {
|
|
13766
|
+
const statuses = getAgentsStatus();
|
|
13767
|
+
const anyInstalled = statuses.some((s) => s.installed);
|
|
13768
|
+
console.log("");
|
|
13769
|
+
console.log(` ${"Agent".padEnd(14)}${"Installed".padEnd(11)}${"Wired".padEnd(8)}Mode`);
|
|
13770
|
+
console.log(" " + "\u2500".repeat(44));
|
|
13771
|
+
for (const s of statuses) {
|
|
13772
|
+
const installed = s.installed ? chalk20.green("\u2713") : chalk20.gray("\u2717");
|
|
13773
|
+
const wired = !s.installed ? chalk20.gray("\u2014") : s.wired ? chalk20.green("\u2713") : chalk20.yellow("\u2717");
|
|
13774
|
+
const mode = s.mode ? chalk20.gray(s.mode) : chalk20.gray("\u2014");
|
|
13775
|
+
const hint = s.installed && !s.wired ? chalk20.gray(` \u2190 node9 agents add ${s.name}`) : "";
|
|
13776
|
+
console.log(` ${s.label.padEnd(14)}${installed} ${wired} ${mode}${hint}`);
|
|
13777
|
+
}
|
|
13778
|
+
console.log("");
|
|
13779
|
+
if (!anyInstalled) {
|
|
13780
|
+
console.log(
|
|
13781
|
+
chalk20.gray(" No AI agents detected. Install Claude Code, Gemini CLI, Cursor,\n") + chalk20.gray(" Windsurf, VSCode, or Codex then run: node9 agents list\n")
|
|
13782
|
+
);
|
|
13783
|
+
return;
|
|
13784
|
+
}
|
|
13785
|
+
const unwired = statuses.filter((s) => s.installed && !s.wired);
|
|
13786
|
+
if (unwired.length > 0) {
|
|
13787
|
+
console.log(
|
|
13788
|
+
chalk20.yellow(` ${unwired.length} agent(s) not yet wired. Run: `) + chalk20.white(`node9 agents add ${unwired[0].name}`) + "\n"
|
|
13789
|
+
);
|
|
13790
|
+
}
|
|
13791
|
+
});
|
|
13792
|
+
agents.command("add").description("Wire Node9 into an agent").argument("<agent>", `Agent to wire: ${AGENT_NAMES.join(" | ")}`).action(async (agent) => {
|
|
13793
|
+
const name = agent.toLowerCase();
|
|
13794
|
+
const fn = SETUP_FN[name];
|
|
13795
|
+
if (!fn) {
|
|
13796
|
+
console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
|
|
13797
|
+
process.exit(1);
|
|
13798
|
+
}
|
|
13799
|
+
await fn();
|
|
13800
|
+
});
|
|
13801
|
+
agents.command("remove").description("Remove Node9 from an agent").argument("<agent>", `Agent to unwire: ${AGENT_NAMES.join(" | ")}`).action((agent) => {
|
|
13802
|
+
const name = agent.toLowerCase();
|
|
13803
|
+
const fn = TEARDOWN_FN[name];
|
|
13804
|
+
if (!fn) {
|
|
13805
|
+
console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
|
|
13806
|
+
process.exit(1);
|
|
13807
|
+
}
|
|
13808
|
+
console.log(chalk20.cyan(`
|
|
13809
|
+
\u{1F6E1}\uFE0F Node9: removing from ${name}...
|
|
13810
|
+
`));
|
|
13811
|
+
fn();
|
|
13812
|
+
console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
|
|
13813
|
+
});
|
|
13814
|
+
}
|
|
13815
|
+
|
|
13816
|
+
// src/cli/commands/scan.ts
|
|
13817
|
+
init_shields();
|
|
13818
|
+
init_config();
|
|
13819
|
+
init_policy();
|
|
13820
|
+
init_dlp();
|
|
13821
|
+
import chalk21 from "chalk";
|
|
13822
|
+
import fs32 from "fs";
|
|
13823
|
+
import path35 from "path";
|
|
13824
|
+
import os28 from "os";
|
|
13825
|
+
var CLAUDE_PRICING2 = {
|
|
13826
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13827
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13828
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
13829
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13830
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13831
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13832
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13833
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13834
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
13835
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
13836
|
+
};
|
|
13837
|
+
function claudeModelPrice2(model) {
|
|
13838
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
13839
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
|
|
13840
|
+
if (base === key || base.startsWith(key)) return p;
|
|
13841
|
+
}
|
|
13842
|
+
return null;
|
|
13843
|
+
}
|
|
13844
|
+
function num2(n) {
|
|
13845
|
+
return n.toLocaleString();
|
|
13846
|
+
}
|
|
13847
|
+
function fmtCost2(usd) {
|
|
13848
|
+
if (usd < 1e-3) return "< $0.001";
|
|
13849
|
+
if (usd < 1) return "$" + usd.toFixed(4);
|
|
13850
|
+
return "$" + usd.toFixed(2);
|
|
13851
|
+
}
|
|
13852
|
+
function fmtTs(ts) {
|
|
13853
|
+
try {
|
|
13854
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
13855
|
+
month: "short",
|
|
13856
|
+
day: "numeric",
|
|
13857
|
+
year: "numeric"
|
|
13858
|
+
});
|
|
13859
|
+
} catch {
|
|
13860
|
+
return ts.slice(0, 10);
|
|
13861
|
+
}
|
|
13862
|
+
}
|
|
13863
|
+
function preview(input, max) {
|
|
13864
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
13865
|
+
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
13866
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
13867
|
+
}
|
|
13868
|
+
function buildRuleSources() {
|
|
13869
|
+
const sources = [];
|
|
13870
|
+
for (const [shieldName, shield] of Object.entries(SHIELDS)) {
|
|
13871
|
+
for (const rule of shield.smartRules) {
|
|
13872
|
+
sources.push({ shieldName, shieldLabel: shieldName, rule });
|
|
13873
|
+
}
|
|
13874
|
+
}
|
|
13875
|
+
try {
|
|
13876
|
+
const config = getConfig();
|
|
13877
|
+
for (const rule of config.policy.smartRules) {
|
|
13878
|
+
if (!rule.name) continue;
|
|
13879
|
+
if (rule.name.startsWith("shield:")) continue;
|
|
13880
|
+
const isCloud = rule.name.startsWith("cloud:");
|
|
13881
|
+
sources.push({
|
|
13882
|
+
shieldName: isCloud ? "cloud" : "custom",
|
|
13883
|
+
shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
|
|
13884
|
+
rule
|
|
13885
|
+
});
|
|
13886
|
+
}
|
|
13887
|
+
} catch {
|
|
13888
|
+
}
|
|
13889
|
+
return sources;
|
|
13890
|
+
}
|
|
13891
|
+
function scanClaudeHistory(startDate) {
|
|
13892
|
+
const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
|
|
13893
|
+
const result = {
|
|
13894
|
+
filesScanned: 0,
|
|
13895
|
+
sessions: 0,
|
|
13896
|
+
totalToolCalls: 0,
|
|
13897
|
+
bashCalls: 0,
|
|
13898
|
+
findings: [],
|
|
13899
|
+
dlpFindings: [],
|
|
13900
|
+
totalCostUSD: 0,
|
|
13901
|
+
firstDate: null,
|
|
13902
|
+
lastDate: null
|
|
13903
|
+
};
|
|
13904
|
+
if (!fs32.existsSync(projectsDir)) return result;
|
|
13905
|
+
let projDirs;
|
|
13906
|
+
try {
|
|
13907
|
+
projDirs = fs32.readdirSync(projectsDir);
|
|
13908
|
+
} catch {
|
|
13909
|
+
return result;
|
|
13910
|
+
}
|
|
13911
|
+
const ruleSources = buildRuleSources();
|
|
13912
|
+
for (const proj of projDirs) {
|
|
13913
|
+
const projPath = path35.join(projectsDir, proj);
|
|
13914
|
+
try {
|
|
13915
|
+
if (!fs32.statSync(projPath).isDirectory()) continue;
|
|
13916
|
+
} catch {
|
|
13917
|
+
continue;
|
|
13918
|
+
}
|
|
13919
|
+
const projLabel = decodeURIComponent(proj).replace(os28.homedir(), "~").slice(0, 40);
|
|
13920
|
+
let files;
|
|
13921
|
+
try {
|
|
13922
|
+
files = fs32.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
13923
|
+
} catch {
|
|
13924
|
+
continue;
|
|
13925
|
+
}
|
|
13926
|
+
for (const file of files) {
|
|
13927
|
+
result.filesScanned++;
|
|
13928
|
+
result.sessions++;
|
|
13929
|
+
let raw;
|
|
13930
|
+
try {
|
|
13931
|
+
raw = fs32.readFileSync(path35.join(projPath, file), "utf-8");
|
|
13932
|
+
} catch {
|
|
13933
|
+
continue;
|
|
13934
|
+
}
|
|
13935
|
+
for (const line of raw.split("\n")) {
|
|
13936
|
+
if (!line.trim()) continue;
|
|
13937
|
+
let entry;
|
|
13938
|
+
try {
|
|
13939
|
+
entry = JSON.parse(line);
|
|
13940
|
+
} catch {
|
|
13941
|
+
continue;
|
|
13942
|
+
}
|
|
13943
|
+
if (entry.type !== "assistant") continue;
|
|
13944
|
+
if (startDate && entry.timestamp) {
|
|
13945
|
+
if (new Date(entry.timestamp) < startDate) continue;
|
|
13946
|
+
}
|
|
13947
|
+
if (entry.timestamp) {
|
|
13948
|
+
if (!result.firstDate || entry.timestamp < result.firstDate)
|
|
13949
|
+
result.firstDate = entry.timestamp;
|
|
13950
|
+
if (!result.lastDate || entry.timestamp > result.lastDate)
|
|
13951
|
+
result.lastDate = entry.timestamp;
|
|
13952
|
+
}
|
|
13953
|
+
const usage = entry.message?.usage;
|
|
13954
|
+
const model = entry.message?.model;
|
|
13955
|
+
if (usage && model) {
|
|
13956
|
+
const p = claudeModelPrice2(model);
|
|
13957
|
+
if (p) {
|
|
13958
|
+
result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
|
|
13959
|
+
}
|
|
13960
|
+
}
|
|
13961
|
+
const content = entry.message?.content;
|
|
13962
|
+
if (!Array.isArray(content)) continue;
|
|
13963
|
+
for (const block of content) {
|
|
13964
|
+
if (block.type !== "tool_use") continue;
|
|
13965
|
+
result.totalToolCalls++;
|
|
13966
|
+
const toolName = block.name ?? "";
|
|
13967
|
+
const toolNameLower = toolName.toLowerCase();
|
|
13968
|
+
const input = block.input ?? {};
|
|
13969
|
+
if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
|
|
13970
|
+
result.bashCalls++;
|
|
13971
|
+
}
|
|
13972
|
+
const dlpMatch = scanArgs(input);
|
|
13973
|
+
if (dlpMatch) {
|
|
13974
|
+
const isDupe = result.dlpFindings.some(
|
|
13975
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
13976
|
+
);
|
|
13977
|
+
if (!isDupe) {
|
|
13978
|
+
result.dlpFindings.push({
|
|
13979
|
+
patternName: dlpMatch.patternName,
|
|
13980
|
+
redactedSample: dlpMatch.redactedSample,
|
|
13981
|
+
toolName,
|
|
13982
|
+
timestamp: entry.timestamp ?? "",
|
|
13983
|
+
project: projLabel
|
|
13984
|
+
});
|
|
13985
|
+
}
|
|
13986
|
+
}
|
|
13987
|
+
for (const source of ruleSources) {
|
|
13988
|
+
const { rule } = source;
|
|
13989
|
+
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
13990
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
13991
|
+
const inputPreview = preview(input, 120);
|
|
13992
|
+
const isDupe = result.findings.some(
|
|
13993
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
13994
|
+
);
|
|
13995
|
+
if (!isDupe) {
|
|
13996
|
+
result.findings.push({
|
|
13997
|
+
source,
|
|
13998
|
+
toolName,
|
|
13999
|
+
input,
|
|
14000
|
+
timestamp: entry.timestamp ?? "",
|
|
14001
|
+
project: projLabel
|
|
14002
|
+
});
|
|
14003
|
+
}
|
|
14004
|
+
break;
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
}
|
|
14008
|
+
}
|
|
14009
|
+
}
|
|
14010
|
+
return result;
|
|
14011
|
+
}
|
|
14012
|
+
function registerScanCommand(program2) {
|
|
14013
|
+
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per shield", "5").action((options) => {
|
|
14014
|
+
const topN = Math.max(1, parseInt(options.top, 10) || 5);
|
|
14015
|
+
const startDate = options.all ? null : (() => {
|
|
14016
|
+
const d = /* @__PURE__ */ new Date();
|
|
14017
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
14018
|
+
d.setHours(0, 0, 0, 0);
|
|
14019
|
+
return d;
|
|
14020
|
+
})();
|
|
14021
|
+
console.log("");
|
|
14022
|
+
console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
|
|
14023
|
+
console.log("");
|
|
14024
|
+
const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
|
|
14025
|
+
if (!fs32.existsSync(projectsDir)) {
|
|
14026
|
+
console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
|
|
14027
|
+
console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
14028
|
+
return;
|
|
14029
|
+
}
|
|
14030
|
+
process.stdout.write(chalk21.dim(" Scanning\u2026"));
|
|
14031
|
+
const scan = scanClaudeHistory(startDate);
|
|
14032
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
14033
|
+
if (scan.filesScanned === 0) {
|
|
14034
|
+
console.log(chalk21.yellow(" No JSONL session files found.\n"));
|
|
14035
|
+
return;
|
|
14036
|
+
}
|
|
14037
|
+
const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
|
|
14038
|
+
const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
14039
|
+
console.log(
|
|
14040
|
+
" " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
|
|
14041
|
+
);
|
|
14042
|
+
console.log("");
|
|
14043
|
+
const byShield = /* @__PURE__ */ new Map();
|
|
14044
|
+
for (const f of scan.findings) {
|
|
14045
|
+
const key = f.source.shieldName;
|
|
14046
|
+
const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
|
|
14047
|
+
entry.findings.push(f);
|
|
14048
|
+
byShield.set(key, entry);
|
|
14049
|
+
}
|
|
14050
|
+
const totalFindings = scan.findings.length;
|
|
14051
|
+
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
14052
|
+
console.log(chalk21.green(" \u2705 No findings across all shields and rules."));
|
|
14053
|
+
console.log(chalk21.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
|
|
14054
|
+
} else {
|
|
14055
|
+
if (totalFindings > 0) {
|
|
14056
|
+
console.log(
|
|
14057
|
+
" " + chalk21.bold("If node9 had been installed:") + " " + chalk21.yellow.bold(
|
|
14058
|
+
`${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
|
|
14059
|
+
)
|
|
14060
|
+
);
|
|
14061
|
+
console.log("");
|
|
14062
|
+
const sorted = [...byShield.entries()].sort(
|
|
14063
|
+
(a, b) => b[1].findings.length - a[1].findings.length
|
|
14064
|
+
);
|
|
14065
|
+
for (const [shieldName, { label, findings }] of sorted) {
|
|
14066
|
+
const count = findings.length;
|
|
14067
|
+
const isUserRule = shieldName === "custom" || shieldName === "cloud";
|
|
14068
|
+
const shieldBadge = isUserRule ? chalk21.magenta(label) : chalk21.cyan(label);
|
|
14069
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
14070
|
+
console.log(
|
|
14071
|
+
" " + shieldBadge + chalk21.dim(" \xB7 ") + chalk21.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : chalk21.dim(` \u2192 node9 shield enable ${shieldName}`))
|
|
14072
|
+
);
|
|
14073
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
14074
|
+
for (const f of findings) {
|
|
14075
|
+
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
14076
|
+
const arr = byRule.get(ruleKey) ?? [];
|
|
14077
|
+
arr.push(f);
|
|
14078
|
+
byRule.set(ruleKey, arr);
|
|
14079
|
+
}
|
|
14080
|
+
for (const [, ruleFindings] of byRule) {
|
|
14081
|
+
const rule = ruleFindings[0].source.rule;
|
|
14082
|
+
const ruleCount = ruleFindings.length;
|
|
14083
|
+
const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
|
|
14084
|
+
const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
14085
|
+
console.log(
|
|
14086
|
+
" " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
|
|
14087
|
+
);
|
|
14088
|
+
const shown = ruleFindings.slice(0, topN);
|
|
14089
|
+
for (const f of shown) {
|
|
14090
|
+
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
14091
|
+
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
14092
|
+
const cmd = chalk21.gray(preview(f.input, 55));
|
|
14093
|
+
console.log(` ${ts}${proj}${cmd}`);
|
|
14094
|
+
}
|
|
14095
|
+
if (ruleFindings.length > topN) {
|
|
14096
|
+
console.log(
|
|
14097
|
+
chalk21.dim(
|
|
14098
|
+
` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
|
|
14099
|
+
)
|
|
14100
|
+
);
|
|
14101
|
+
}
|
|
14102
|
+
}
|
|
14103
|
+
console.log("");
|
|
14104
|
+
}
|
|
14105
|
+
}
|
|
14106
|
+
if (scan.dlpFindings.length > 0) {
|
|
14107
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
14108
|
+
console.log(
|
|
14109
|
+
" " + chalk21.red.bold("Secrets / DLP") + chalk21.dim(" \xB7 ") + chalk21.red(
|
|
14110
|
+
`${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
|
|
14111
|
+
)
|
|
14112
|
+
);
|
|
14113
|
+
const shownDlp = scan.dlpFindings.slice(0, topN);
|
|
14114
|
+
for (const f of shownDlp) {
|
|
14115
|
+
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
14116
|
+
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
14117
|
+
console.log(
|
|
14118
|
+
` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
|
|
14119
|
+
);
|
|
14120
|
+
}
|
|
14121
|
+
if (scan.dlpFindings.length > topN) {
|
|
14122
|
+
console.log(
|
|
14123
|
+
chalk21.dim(
|
|
14124
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--top ${scan.dlpFindings.length})`
|
|
14125
|
+
)
|
|
14126
|
+
);
|
|
14127
|
+
}
|
|
14128
|
+
console.log("");
|
|
14129
|
+
}
|
|
14130
|
+
}
|
|
14131
|
+
if (scan.totalCostUSD > 0) {
|
|
14132
|
+
console.log(
|
|
14133
|
+
" " + chalk21.bold("Claude spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
|
|
14134
|
+
);
|
|
14135
|
+
console.log("");
|
|
14136
|
+
}
|
|
14137
|
+
const auditLog = path35.join(os28.homedir(), ".node9", "audit.log");
|
|
14138
|
+
if (fs32.existsSync(auditLog)) {
|
|
14139
|
+
console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
|
|
14140
|
+
console.log(
|
|
14141
|
+
chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
|
|
14142
|
+
);
|
|
14143
|
+
} else {
|
|
14144
|
+
console.log(chalk21.yellow.bold(" \u26A1 node9 was not running during these sessions."));
|
|
14145
|
+
console.log(
|
|
14146
|
+
" " + chalk21.white("Run ") + chalk21.cyan("node9 init") + chalk21.white(" to start protecting your AI agents.")
|
|
14147
|
+
);
|
|
14148
|
+
}
|
|
14149
|
+
console.log("");
|
|
14150
|
+
});
|
|
14151
|
+
}
|
|
14152
|
+
|
|
14153
|
+
// src/cli/commands/sessions.ts
|
|
14154
|
+
import chalk22 from "chalk";
|
|
14155
|
+
import fs33 from "fs";
|
|
14156
|
+
import path36 from "path";
|
|
14157
|
+
import os29 from "os";
|
|
14158
|
+
var CLAUDE_PRICING3 = {
|
|
14159
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
14160
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
14161
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
14162
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
14163
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
14164
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
14165
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
14166
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
14167
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
14168
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
14169
|
+
};
|
|
14170
|
+
function modelPrice(model) {
|
|
14171
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
14172
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
|
|
14173
|
+
if (base === key || base.startsWith(key)) return p;
|
|
14174
|
+
}
|
|
14175
|
+
return null;
|
|
14176
|
+
}
|
|
14177
|
+
function encodeProjectPath(projectPath) {
|
|
14178
|
+
return projectPath.replace(/\//g, "-");
|
|
14179
|
+
}
|
|
14180
|
+
function sessionJsonlPath(projectPath, sessionId) {
|
|
14181
|
+
const encoded = encodeProjectPath(projectPath);
|
|
14182
|
+
return path36.join(os29.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
14183
|
+
}
|
|
14184
|
+
function projectLabel(projectPath) {
|
|
14185
|
+
return projectPath.replace(os29.homedir(), "~");
|
|
14186
|
+
}
|
|
14187
|
+
function parseHistoryLines(lines) {
|
|
14188
|
+
const entries = [];
|
|
14189
|
+
for (const line of lines) {
|
|
14190
|
+
if (!line.trim()) continue;
|
|
14191
|
+
try {
|
|
14192
|
+
const obj = JSON.parse(line);
|
|
14193
|
+
if (typeof obj["display"] === "string" && (typeof obj["timestamp"] === "string" || typeof obj["timestamp"] === "number") && typeof obj["project"] === "string" && typeof obj["sessionId"] === "string") {
|
|
14194
|
+
const ts = typeof obj["timestamp"] === "number" ? new Date(obj["timestamp"]).toISOString() : obj["timestamp"];
|
|
14195
|
+
entries.push({
|
|
14196
|
+
display: obj["display"],
|
|
14197
|
+
timestamp: ts,
|
|
14198
|
+
project: obj["project"],
|
|
14199
|
+
sessionId: obj["sessionId"]
|
|
14200
|
+
});
|
|
14201
|
+
}
|
|
14202
|
+
} catch {
|
|
14203
|
+
}
|
|
14204
|
+
}
|
|
14205
|
+
return entries;
|
|
14206
|
+
}
|
|
14207
|
+
function parseSessionLines(lines) {
|
|
14208
|
+
const toolCalls = [];
|
|
14209
|
+
let costUSD = 0;
|
|
14210
|
+
let hasSnapshot = false;
|
|
14211
|
+
const modifiedFiles = [];
|
|
14212
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
14213
|
+
for (const line of lines) {
|
|
14214
|
+
if (!line.trim()) continue;
|
|
14215
|
+
let entry;
|
|
14216
|
+
try {
|
|
14217
|
+
entry = JSON.parse(line);
|
|
14218
|
+
} catch {
|
|
14219
|
+
continue;
|
|
14220
|
+
}
|
|
14221
|
+
if (entry.type === "file-history-snapshot") {
|
|
14222
|
+
hasSnapshot = true;
|
|
14223
|
+
continue;
|
|
14224
|
+
}
|
|
14225
|
+
if (entry.type !== "assistant") continue;
|
|
14226
|
+
const usage = entry.message?.usage;
|
|
14227
|
+
const model = entry.message?.model;
|
|
14228
|
+
if (usage && model) {
|
|
14229
|
+
const p = modelPrice(model);
|
|
14230
|
+
if (p) {
|
|
14231
|
+
costUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
|
|
14232
|
+
}
|
|
14233
|
+
}
|
|
14234
|
+
const content = entry.message?.content;
|
|
14235
|
+
if (!Array.isArray(content)) continue;
|
|
14236
|
+
for (const block of content) {
|
|
14237
|
+
if (block.type !== "tool_use") continue;
|
|
14238
|
+
const tool = block.name ?? "";
|
|
14239
|
+
const input = block.input ?? {};
|
|
14240
|
+
toolCalls.push({ tool, input, timestamp: entry.timestamp ?? "" });
|
|
14241
|
+
const toolLower = tool.toLowerCase();
|
|
14242
|
+
if (toolLower === "write" || toolLower === "edit" || toolLower === "notebookedit") {
|
|
14243
|
+
const fp = input.file_path ?? input.path;
|
|
14244
|
+
if (typeof fp === "string" && !seenFiles.has(fp)) {
|
|
14245
|
+
seenFiles.add(fp);
|
|
14246
|
+
modifiedFiles.push(fp);
|
|
14247
|
+
}
|
|
14248
|
+
}
|
|
14249
|
+
}
|
|
14250
|
+
}
|
|
14251
|
+
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
14252
|
+
}
|
|
14253
|
+
function loadAuditEntries(auditPath) {
|
|
14254
|
+
const aPath = auditPath ?? path36.join(os29.homedir(), ".node9", "audit.log");
|
|
14255
|
+
let raw;
|
|
14256
|
+
try {
|
|
14257
|
+
raw = fs33.readFileSync(aPath, "utf-8");
|
|
14258
|
+
} catch {
|
|
14259
|
+
return [];
|
|
14260
|
+
}
|
|
14261
|
+
const entries = [];
|
|
14262
|
+
for (const line of raw.split("\n")) {
|
|
14263
|
+
if (!line.trim()) continue;
|
|
14264
|
+
try {
|
|
14265
|
+
const e = JSON.parse(line);
|
|
14266
|
+
if (!e.ts || !e.tool || !e.decision) continue;
|
|
14267
|
+
if (e.decision === "allow" || e.decision === "allowed") continue;
|
|
14268
|
+
entries.push(e);
|
|
14269
|
+
} catch {
|
|
14270
|
+
}
|
|
14271
|
+
}
|
|
14272
|
+
return entries;
|
|
14273
|
+
}
|
|
14274
|
+
function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
14275
|
+
const start = new Date(windowStart).getTime();
|
|
14276
|
+
const end = new Date(windowEnd).getTime();
|
|
14277
|
+
const result = [];
|
|
14278
|
+
for (const e of entries) {
|
|
14279
|
+
const t = new Date(e.ts).getTime();
|
|
14280
|
+
if (t < start || t > end) continue;
|
|
14281
|
+
result.push({
|
|
14282
|
+
tool: e.tool,
|
|
14283
|
+
args: e.args,
|
|
14284
|
+
argsHash: e.argsHash,
|
|
14285
|
+
timestamp: e.ts,
|
|
14286
|
+
decision: e.decision,
|
|
14287
|
+
checkedBy: e.checkedBy
|
|
14288
|
+
});
|
|
14289
|
+
}
|
|
14290
|
+
return result;
|
|
14291
|
+
}
|
|
14292
|
+
function buildSessions(days, historyPath) {
|
|
14293
|
+
const hPath = historyPath ?? path36.join(os29.homedir(), ".claude", "history.jsonl");
|
|
14294
|
+
let historyRaw;
|
|
14295
|
+
try {
|
|
14296
|
+
historyRaw = fs33.readFileSync(hPath, "utf-8");
|
|
14297
|
+
} catch {
|
|
14298
|
+
return [];
|
|
14299
|
+
}
|
|
14300
|
+
const cutoff = days !== null ? (() => {
|
|
14301
|
+
const d = /* @__PURE__ */ new Date();
|
|
14302
|
+
d.setDate(d.getDate() - days);
|
|
14303
|
+
d.setHours(0, 0, 0, 0);
|
|
14304
|
+
return d;
|
|
14305
|
+
})() : null;
|
|
14306
|
+
const entries = parseHistoryLines(historyRaw.split("\n"));
|
|
14307
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
14308
|
+
for (const e of entries) {
|
|
14309
|
+
if (cutoff && new Date(e.timestamp) < cutoff) continue;
|
|
14310
|
+
const existing = bySession.get(e.sessionId);
|
|
14311
|
+
if (!existing || e.timestamp < existing.timestamp) {
|
|
14312
|
+
bySession.set(e.sessionId, e);
|
|
14313
|
+
}
|
|
14314
|
+
}
|
|
14315
|
+
const allAuditEntries = loadAuditEntries();
|
|
14316
|
+
const summaries = [];
|
|
14317
|
+
for (const entry of bySession.values()) {
|
|
14318
|
+
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
14319
|
+
let sessionLines = [];
|
|
14320
|
+
try {
|
|
14321
|
+
sessionLines = fs33.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
14322
|
+
} catch {
|
|
14323
|
+
}
|
|
14324
|
+
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
14325
|
+
const windowStart = entry.timestamp;
|
|
14326
|
+
const lastToolTs = toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].timestamp : "";
|
|
14327
|
+
const windowEnd = new Date(
|
|
14328
|
+
Math.max(new Date(windowStart).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
14329
|
+
// 5 min buffer
|
|
14330
|
+
).toISOString();
|
|
14331
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
|
|
14332
|
+
summaries.push({
|
|
14333
|
+
sessionId: entry.sessionId,
|
|
14334
|
+
project: entry.project,
|
|
14335
|
+
projectLabel: projectLabel(entry.project),
|
|
14336
|
+
firstPrompt: entry.display,
|
|
14337
|
+
startTime: entry.timestamp,
|
|
14338
|
+
toolCalls,
|
|
14339
|
+
blockedCalls,
|
|
14340
|
+
costUSD,
|
|
14341
|
+
hasSnapshot,
|
|
14342
|
+
modifiedFiles
|
|
14343
|
+
});
|
|
14344
|
+
}
|
|
14345
|
+
summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
|
|
14346
|
+
return summaries;
|
|
14347
|
+
}
|
|
14348
|
+
function fmtCost3(usd) {
|
|
14349
|
+
if (usd === 0) return "";
|
|
14350
|
+
if (usd < 1e-3) return "< $0.001";
|
|
14351
|
+
if (usd < 1) return "$" + usd.toFixed(3);
|
|
14352
|
+
return "$" + usd.toFixed(2);
|
|
14353
|
+
}
|
|
14354
|
+
function fmtDate2(iso) {
|
|
14355
|
+
try {
|
|
14356
|
+
return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
14357
|
+
} catch {
|
|
14358
|
+
return iso.slice(0, 10);
|
|
14359
|
+
}
|
|
14360
|
+
}
|
|
14361
|
+
function fmtTime(iso) {
|
|
14362
|
+
try {
|
|
14363
|
+
return new Date(iso).toLocaleTimeString("en-US", {
|
|
14364
|
+
hour: "2-digit",
|
|
14365
|
+
minute: "2-digit",
|
|
14366
|
+
hour12: false
|
|
14367
|
+
});
|
|
14368
|
+
} catch {
|
|
14369
|
+
return iso.slice(11, 16);
|
|
14370
|
+
}
|
|
14371
|
+
}
|
|
14372
|
+
function fmtDateTime(iso) {
|
|
14373
|
+
try {
|
|
14374
|
+
return new Date(iso).toLocaleString("en-US", {
|
|
14375
|
+
month: "short",
|
|
14376
|
+
day: "numeric",
|
|
14377
|
+
year: "numeric",
|
|
14378
|
+
hour: "2-digit",
|
|
14379
|
+
minute: "2-digit",
|
|
14380
|
+
hour12: false
|
|
14381
|
+
});
|
|
14382
|
+
} catch {
|
|
14383
|
+
return iso;
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14386
|
+
function truncate(s, max) {
|
|
14387
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
14388
|
+
}
|
|
14389
|
+
function toolInputSummary(tool, input) {
|
|
14390
|
+
const fp = input.file_path ?? input.path;
|
|
14391
|
+
if (typeof fp === "string") return fp;
|
|
14392
|
+
const cmd = input.command;
|
|
14393
|
+
if (typeof cmd === "string") return truncate(cmd.replace(/\s+/g, " "), 60);
|
|
14394
|
+
const q = input.query ?? input.url;
|
|
14395
|
+
if (typeof q === "string") return truncate(String(q), 60);
|
|
14396
|
+
return "";
|
|
14397
|
+
}
|
|
14398
|
+
function toolColor(tool) {
|
|
14399
|
+
const t = tool.toLowerCase();
|
|
14400
|
+
if (t === "bash" || t === "execute_bash") return chalk22.red;
|
|
14401
|
+
if (t === "write") return chalk22.green;
|
|
14402
|
+
if (t === "edit" || t === "notebookedit") return chalk22.yellow;
|
|
14403
|
+
if (t === "read") return chalk22.cyan;
|
|
14404
|
+
return chalk22.gray;
|
|
14405
|
+
}
|
|
14406
|
+
function barStr2(value, max, width) {
|
|
14407
|
+
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
14408
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
14409
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
14410
|
+
}
|
|
14411
|
+
function colorBar2(value, max, width) {
|
|
14412
|
+
const s = barStr2(value, max, width);
|
|
14413
|
+
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
14414
|
+
return chalk22.cyan(s.slice(0, filled)) + chalk22.dim(s.slice(filled));
|
|
14415
|
+
}
|
|
14416
|
+
function renderSummary(summaries) {
|
|
14417
|
+
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
14418
|
+
const totalCost = summaries.reduce((n, s) => n + s.costUSD, 0);
|
|
14419
|
+
const totalFiles = summaries.reduce((n, s) => n + s.modifiedFiles.length, 0);
|
|
14420
|
+
const totalBlocked = summaries.reduce((n, s) => n + s.blockedCalls.length, 0);
|
|
14421
|
+
const snapshots = summaries.filter((s) => s.hasSnapshot).length;
|
|
14422
|
+
const avgCost = summaries.length > 0 ? totalCost / summaries.length : 0;
|
|
14423
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
14424
|
+
for (const s of summaries) {
|
|
14425
|
+
for (const tc of s.toolCalls) {
|
|
14426
|
+
const key = tc.tool.toLowerCase();
|
|
14427
|
+
toolCounts.set(key, (toolCounts.get(key) ?? 0) + 1);
|
|
14428
|
+
}
|
|
14429
|
+
}
|
|
14430
|
+
const groups = { Bash: 0, Read: 0, Write: 0, Edit: 0, Other: 0 };
|
|
14431
|
+
for (const [tool, count] of toolCounts) {
|
|
14432
|
+
if (tool === "bash" || tool === "execute_bash") groups["Bash"] += count;
|
|
14433
|
+
else if (tool === "read") groups["Read"] += count;
|
|
14434
|
+
else if (tool === "write") groups["Write"] += count;
|
|
14435
|
+
else if (tool === "edit" || tool === "notebookedit") groups["Edit"] += count;
|
|
14436
|
+
else groups["Other"] += count;
|
|
14437
|
+
}
|
|
14438
|
+
const projCosts = /* @__PURE__ */ new Map();
|
|
14439
|
+
for (const s of summaries) {
|
|
14440
|
+
projCosts.set(s.projectLabel, (projCosts.get(s.projectLabel) ?? 0) + s.costUSD);
|
|
14441
|
+
}
|
|
14442
|
+
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
14443
|
+
const W = 20;
|
|
14444
|
+
console.log(chalk22.dim(" " + "\u2500".repeat(70)));
|
|
14445
|
+
console.log(
|
|
14446
|
+
" " + chalk22.bold.white(String(summaries.length).padEnd(4)) + chalk22.dim("sessions ") + chalk22.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk22.dim("total ") + chalk22.bold.white(String(totalTools).padEnd(6)) + chalk22.dim("tool calls ") + chalk22.bold.white(String(totalFiles)) + chalk22.dim(" files modified") + (totalBlocked > 0 ? chalk22.dim(" ") + chalk22.red.bold(String(totalBlocked)) + chalk22.dim(" blocked by node9") : "")
|
|
14447
|
+
);
|
|
14448
|
+
console.log(
|
|
14449
|
+
" " + chalk22.dim("avg ") + chalk22.white(fmtCost3(avgCost).padEnd(10)) + chalk22.dim("/session ") + chalk22.green(String(snapshots)) + chalk22.dim(` of ${summaries.length} sessions had snapshots`)
|
|
14450
|
+
);
|
|
14451
|
+
console.log("");
|
|
14452
|
+
console.log(" " + chalk22.dim("Tool breakdown:"));
|
|
14453
|
+
const maxGroup = Math.max(...Object.values(groups));
|
|
14454
|
+
for (const [label, count] of Object.entries(groups)) {
|
|
14455
|
+
if (count === 0) continue;
|
|
14456
|
+
const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
14457
|
+
console.log(
|
|
14458
|
+
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct2)}%)`)
|
|
14459
|
+
);
|
|
14460
|
+
}
|
|
14461
|
+
console.log("");
|
|
14462
|
+
if (topProjects.length > 1) {
|
|
14463
|
+
console.log(" " + chalk22.dim("Cost by project:"));
|
|
14464
|
+
const maxProjCost = topProjects[0][1];
|
|
14465
|
+
for (const [proj, cost] of topProjects) {
|
|
14466
|
+
console.log(
|
|
14467
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk22.yellow(fmtCost3(cost))
|
|
14468
|
+
);
|
|
14469
|
+
}
|
|
14470
|
+
console.log("");
|
|
14471
|
+
}
|
|
14472
|
+
console.log(chalk22.dim(" " + "\u2500".repeat(70)));
|
|
14473
|
+
console.log("");
|
|
14474
|
+
}
|
|
14475
|
+
function renderList(summaries, totalCost) {
|
|
14476
|
+
if (summaries.length === 0) {
|
|
14477
|
+
console.log(chalk22.yellow(" No sessions found in the requested range.\n"));
|
|
14478
|
+
return;
|
|
14479
|
+
}
|
|
14480
|
+
const totalLabel = totalCost > 0 ? chalk22.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
14481
|
+
console.log(
|
|
14482
|
+
" " + chalk22.white(String(summaries.length)) + chalk22.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
14483
|
+
);
|
|
14484
|
+
console.log("");
|
|
14485
|
+
let lastGroup = "";
|
|
14486
|
+
for (const s of summaries) {
|
|
14487
|
+
const group = fmtDate2(s.startTime) + " " + s.projectLabel;
|
|
14488
|
+
if (group !== lastGroup) {
|
|
14489
|
+
console.log(
|
|
14490
|
+
chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
|
|
14491
|
+
);
|
|
14492
|
+
lastGroup = group;
|
|
14493
|
+
}
|
|
14494
|
+
const timeStr = chalk22.dim(fmtTime(s.startTime));
|
|
14495
|
+
const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
14496
|
+
const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
|
|
14497
|
+
const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
14498
|
+
const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
14499
|
+
const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
|
|
14500
|
+
const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
|
|
14501
|
+
console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
|
|
14502
|
+
}
|
|
14503
|
+
console.log("");
|
|
14504
|
+
console.log(
|
|
14505
|
+
chalk22.dim(" Run") + " " + chalk22.cyan("node9 sessions --detail <session-id>") + chalk22.dim(" for full tool trace.")
|
|
14506
|
+
);
|
|
14507
|
+
console.log("");
|
|
14508
|
+
}
|
|
14509
|
+
function renderDetail(s) {
|
|
14510
|
+
console.log("");
|
|
14511
|
+
console.log(chalk22.bold(" Session ") + chalk22.dim(s.sessionId));
|
|
14512
|
+
console.log(
|
|
14513
|
+
chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
14514
|
+
);
|
|
14515
|
+
console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
|
|
14516
|
+
console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
|
|
14517
|
+
if (s.costUSD > 0)
|
|
14518
|
+
console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
|
|
14519
|
+
console.log(
|
|
14520
|
+
chalk22.bold(" Snapshot ") + (s.hasSnapshot ? chalk22.green("\u2713 taken") : chalk22.dim("none"))
|
|
14521
|
+
);
|
|
14522
|
+
console.log("");
|
|
14523
|
+
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
14524
|
+
console.log(chalk22.dim(" No tool calls recorded.\n"));
|
|
14525
|
+
return;
|
|
14526
|
+
}
|
|
14527
|
+
const timeline = [
|
|
14528
|
+
...s.toolCalls.map((tc) => ({ kind: "tool", tc })),
|
|
14529
|
+
...s.blockedCalls.map((bc) => ({ kind: "blocked", bc }))
|
|
14530
|
+
].sort((a, b) => {
|
|
14531
|
+
const ta = a.kind === "tool" ? a.tc.timestamp : a.bc.timestamp;
|
|
14532
|
+
const tb = b.kind === "tool" ? b.tc.timestamp : b.bc.timestamp;
|
|
14533
|
+
return ta < tb ? -1 : ta > tb ? 1 : 0;
|
|
14534
|
+
});
|
|
14535
|
+
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
14536
|
+
if (s.blockedCalls.length > 0)
|
|
14537
|
+
headerParts.push(chalk22.red(`${s.blockedCalls.length} blocked by node9`));
|
|
14538
|
+
console.log(chalk22.bold(" " + headerParts.join(" \xB7 ")));
|
|
14539
|
+
console.log("");
|
|
14540
|
+
for (const entry of timeline) {
|
|
14541
|
+
if (entry.kind === "tool") {
|
|
14542
|
+
const tc = entry.tc;
|
|
14543
|
+
const colorFn = toolColor(tc.tool);
|
|
14544
|
+
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
14545
|
+
const detail = chalk22.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
14546
|
+
const ts = tc.timestamp ? chalk22.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
14547
|
+
console.log(` ${ts}${toolPad} ${detail}`);
|
|
14548
|
+
} else {
|
|
14549
|
+
const bc = entry.bc;
|
|
14550
|
+
const ts = bc.timestamp ? chalk22.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
14551
|
+
const label = chalk22.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
14552
|
+
const toolName = chalk22.red(bc.tool.padEnd(10));
|
|
14553
|
+
const argsSummary = bc.args ? chalk22.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk22.dim("[args not logged]");
|
|
14554
|
+
const reason = bc.checkedBy ? chalk22.dim(" \u2190 " + bc.checkedBy) : "";
|
|
14555
|
+
console.log(` ${ts}${label} ${toolName} ${argsSummary}${reason}`);
|
|
14556
|
+
}
|
|
14557
|
+
}
|
|
14558
|
+
console.log("");
|
|
14559
|
+
if (s.modifiedFiles.length > 0) {
|
|
14560
|
+
console.log(chalk22.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
14561
|
+
for (const f of s.modifiedFiles) {
|
|
14562
|
+
console.log(" " + chalk22.yellow(f));
|
|
14563
|
+
}
|
|
14564
|
+
console.log("");
|
|
14565
|
+
}
|
|
14566
|
+
}
|
|
14567
|
+
function registerSessionsCommand(program2) {
|
|
14568
|
+
program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
|
|
14569
|
+
console.log("");
|
|
14570
|
+
console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
|
|
14571
|
+
console.log("");
|
|
14572
|
+
const historyPath = path36.join(os29.homedir(), ".claude", "history.jsonl");
|
|
14573
|
+
if (!fs33.existsSync(historyPath)) {
|
|
14574
|
+
console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
|
|
14575
|
+
console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
14576
|
+
return;
|
|
14577
|
+
}
|
|
14578
|
+
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
14579
|
+
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
14580
|
+
console.log(chalk22.dim(" " + rangeLabel));
|
|
14581
|
+
console.log("");
|
|
14582
|
+
process.stdout.write(chalk22.dim(" Loading\u2026"));
|
|
14583
|
+
const summaries = buildSessions(days);
|
|
14584
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
14585
|
+
if (options.detail) {
|
|
14586
|
+
const target = summaries.find(
|
|
14587
|
+
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
14588
|
+
);
|
|
14589
|
+
if (!target) {
|
|
14590
|
+
console.log(chalk22.red(` Session not found: ${options.detail}`));
|
|
14591
|
+
console.log(chalk22.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
14592
|
+
return;
|
|
14593
|
+
}
|
|
14594
|
+
renderDetail(target);
|
|
14595
|
+
return;
|
|
14596
|
+
}
|
|
14597
|
+
const totalCost = summaries.reduce((sum, s) => sum + s.costUSD, 0);
|
|
14598
|
+
if (summaries.length > 0) renderSummary(summaries);
|
|
14599
|
+
renderList(summaries, totalCost);
|
|
14600
|
+
});
|
|
14601
|
+
}
|
|
14602
|
+
|
|
14603
|
+
// src/cli/commands/skill-pin.ts
|
|
14604
|
+
import chalk23 from "chalk";
|
|
14605
|
+
import fs34 from "fs";
|
|
14606
|
+
import os30 from "os";
|
|
14607
|
+
import path37 from "path";
|
|
14608
|
+
function wipeSkillSessions() {
|
|
14609
|
+
try {
|
|
14610
|
+
fs34.rmSync(path37.join(os30.homedir(), ".node9", "skill-sessions"), {
|
|
14611
|
+
recursive: true,
|
|
14612
|
+
force: true
|
|
14613
|
+
});
|
|
14614
|
+
} catch {
|
|
14615
|
+
}
|
|
14616
|
+
}
|
|
14617
|
+
function registerSkillPinCommand(program2) {
|
|
14618
|
+
const skillCmd = program2.command("skill").description("Manage skill pinning (supply chain & update drift defense, AST 02 + AST 07)");
|
|
14619
|
+
const pinSubCmd = skillCmd.command("pin").description("Manage pinned skill roots");
|
|
14620
|
+
pinSubCmd.command("list").description("Show all pinned skill roots and their content hashes").action(() => {
|
|
14621
|
+
const result = readSkillPinsSafe();
|
|
14622
|
+
if (!result.ok) {
|
|
14623
|
+
if (result.reason === "missing") {
|
|
14624
|
+
console.log(chalk23.gray("\nNo skill roots are pinned yet."));
|
|
14625
|
+
console.log(
|
|
14626
|
+
chalk23.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
14627
|
+
);
|
|
14628
|
+
return;
|
|
14629
|
+
}
|
|
14630
|
+
console.error(chalk23.red(`
|
|
14631
|
+
\u274C Pin file is corrupt: ${result.detail}`));
|
|
14632
|
+
console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
|
|
14633
|
+
process.exit(1);
|
|
14634
|
+
}
|
|
14635
|
+
const entries = Object.entries(result.pins.roots);
|
|
14636
|
+
if (entries.length === 0) {
|
|
14637
|
+
console.log(chalk23.gray("\nNo skill roots are pinned yet.\n"));
|
|
14638
|
+
return;
|
|
14639
|
+
}
|
|
14640
|
+
console.log(chalk23.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
14641
|
+
for (const [key, entry] of entries) {
|
|
14642
|
+
const missing = entry.exists ? "" : chalk23.yellow(" (not present at pin time)");
|
|
14643
|
+
console.log(` ${chalk23.cyan(key)} ${chalk23.gray(entry.rootPath)}${missing}`);
|
|
14644
|
+
console.log(` Files (${entry.fileCount})`);
|
|
14645
|
+
console.log(` Hash: ${chalk23.gray(entry.contentHash.slice(0, 16))}...`);
|
|
14646
|
+
console.log(` Pinned: ${chalk23.gray(entry.pinnedAt)}
|
|
14647
|
+
`);
|
|
14648
|
+
}
|
|
14649
|
+
});
|
|
14650
|
+
pinSubCmd.command("update <rootKey>").description("Remove a pin so the next session re-pins with current state").action((rootKey) => {
|
|
14651
|
+
let pins;
|
|
14652
|
+
try {
|
|
14653
|
+
pins = readSkillPins();
|
|
14654
|
+
} catch {
|
|
14655
|
+
console.error(chalk23.red("\n\u274C Pin file is corrupt."));
|
|
14656
|
+
console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
|
|
14657
|
+
process.exit(1);
|
|
14658
|
+
}
|
|
14659
|
+
if (!pins.roots[rootKey]) {
|
|
14660
|
+
console.error(chalk23.red(`
|
|
14661
|
+
\u274C No pin found for root key "${rootKey}"
|
|
14662
|
+
`));
|
|
14663
|
+
console.error(`Run ${chalk23.cyan("node9 skill pin list")} to see pinned roots.
|
|
14664
|
+
`);
|
|
14665
|
+
process.exit(1);
|
|
14666
|
+
}
|
|
14667
|
+
const rootPath = pins.roots[rootKey].rootPath;
|
|
14668
|
+
removePin(rootKey);
|
|
14669
|
+
wipeSkillSessions();
|
|
14670
|
+
console.log(chalk23.green(`
|
|
14671
|
+
\u{1F513} Pin removed for ${chalk23.cyan(rootKey)}`));
|
|
14672
|
+
console.log(chalk23.gray(` ${rootPath}`));
|
|
14673
|
+
console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
|
|
14674
|
+
});
|
|
14675
|
+
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
14676
|
+
const result = readSkillPinsSafe();
|
|
14677
|
+
if (!result.ok && result.reason === "missing") {
|
|
14678
|
+
wipeSkillSessions();
|
|
14679
|
+
console.log(chalk23.gray("\nNo pins to clear.\n"));
|
|
14680
|
+
return;
|
|
14681
|
+
}
|
|
14682
|
+
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
14683
|
+
clearAllPins();
|
|
14684
|
+
wipeSkillSessions();
|
|
14685
|
+
console.log(chalk23.green(`
|
|
14686
|
+
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
14687
|
+
console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
|
|
14688
|
+
});
|
|
14689
|
+
}
|
|
14690
|
+
|
|
14691
|
+
// src/cli.ts
|
|
14692
|
+
var { version } = JSON.parse(
|
|
14693
|
+
fs37.readFileSync(path40.join(__dirname, "../package.json"), "utf-8")
|
|
14694
|
+
);
|
|
14695
|
+
var program = new Command();
|
|
14696
|
+
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
14697
|
+
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) => {
|
|
14698
|
+
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
14699
|
+
const credPath = path40.join(os33.homedir(), ".node9", "credentials.json");
|
|
14700
|
+
if (!fs37.existsSync(path40.dirname(credPath)))
|
|
14701
|
+
fs37.mkdirSync(path40.dirname(credPath), { recursive: true });
|
|
14702
|
+
const profileName = options.profile || "default";
|
|
14703
|
+
let existingCreds = {};
|
|
14704
|
+
try {
|
|
14705
|
+
if (fs37.existsSync(credPath)) {
|
|
14706
|
+
const raw = JSON.parse(fs37.readFileSync(credPath, "utf-8"));
|
|
14707
|
+
if (raw.apiKey) {
|
|
14708
|
+
existingCreds = {
|
|
14709
|
+
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
14710
|
+
};
|
|
14711
|
+
} else {
|
|
14712
|
+
existingCreds = raw;
|
|
14713
|
+
}
|
|
14714
|
+
}
|
|
14715
|
+
} catch {
|
|
14716
|
+
}
|
|
14717
|
+
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
14718
|
+
fs37.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
14719
|
+
if (profileName === "default") {
|
|
14720
|
+
const configPath = path40.join(os33.homedir(), ".node9", "config.json");
|
|
14721
|
+
let config = {};
|
|
14722
|
+
try {
|
|
14723
|
+
if (fs37.existsSync(configPath))
|
|
14724
|
+
config = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
|
|
14725
|
+
} catch {
|
|
14726
|
+
}
|
|
14727
|
+
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
14728
|
+
const s = config.settings;
|
|
14729
|
+
const approvers = s.approvers || {
|
|
14730
|
+
native: true,
|
|
14731
|
+
browser: true,
|
|
14732
|
+
cloud: true,
|
|
14733
|
+
terminal: true
|
|
14734
|
+
};
|
|
14735
|
+
if (options.local) {
|
|
14736
|
+
approvers.cloud = false;
|
|
14737
|
+
}
|
|
14738
|
+
s.approvers = approvers;
|
|
14739
|
+
if (!fs37.existsSync(path40.dirname(configPath)))
|
|
14740
|
+
fs37.mkdirSync(path40.dirname(configPath), { recursive: true });
|
|
14741
|
+
fs37.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
14742
|
+
}
|
|
14743
|
+
if (options.profile && profileName !== "default") {
|
|
14744
|
+
console.log(chalk25.green(`\u2705 Profile "${profileName}" saved`));
|
|
14745
|
+
console.log(chalk25.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
12545
14746
|
} else if (options.local) {
|
|
12546
|
-
console.log(
|
|
12547
|
-
console.log(
|
|
14747
|
+
console.log(chalk25.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
14748
|
+
console.log(chalk25.gray(` All decisions stay on this machine.`));
|
|
12548
14749
|
} else {
|
|
12549
|
-
console.log(
|
|
12550
|
-
console.log(
|
|
14750
|
+
console.log(chalk25.green(`\u2705 Logged in \u2014 agent mode`));
|
|
14751
|
+
console.log(chalk25.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
12551
14752
|
}
|
|
12552
14753
|
});
|
|
12553
|
-
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) => {
|
|
14754
|
+
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("<target>", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
|
|
12554
14755
|
if (target === "gemini") return await setupGemini();
|
|
12555
14756
|
if (target === "claude") return await setupClaude();
|
|
12556
14757
|
if (target === "cursor") return await setupCursor();
|
|
14758
|
+
if (target === "windsurf") return await setupWindsurf();
|
|
14759
|
+
if (target === "vscode") return await setupVSCode();
|
|
12557
14760
|
if (target === "hud") return setupHud();
|
|
12558
|
-
console.error(
|
|
14761
|
+
console.error(
|
|
14762
|
+
chalk25.red(
|
|
14763
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14764
|
+
)
|
|
14765
|
+
);
|
|
12559
14766
|
process.exit(1);
|
|
12560
14767
|
});
|
|
12561
|
-
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) => {
|
|
14768
|
+
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("[target]", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
|
|
12562
14769
|
if (!target) {
|
|
12563
|
-
console.log(
|
|
12564
|
-
console.log(" Usage: " +
|
|
14770
|
+
console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
14771
|
+
console.log(" Usage: " + chalk25.white("node9 setup <target>") + "\n");
|
|
12565
14772
|
console.log(" Targets:");
|
|
12566
|
-
console.log(" " +
|
|
12567
|
-
console.log(" " +
|
|
12568
|
-
console.log(" " +
|
|
14773
|
+
console.log(" " + chalk25.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
14774
|
+
console.log(" " + chalk25.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
14775
|
+
console.log(" " + chalk25.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
14776
|
+
console.log(" " + chalk25.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
14777
|
+
console.log(" " + chalk25.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
12569
14778
|
process.stdout.write(
|
|
12570
|
-
" " +
|
|
14779
|
+
" " + chalk25.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
12571
14780
|
);
|
|
12572
14781
|
console.log("");
|
|
12573
14782
|
return;
|
|
@@ -12576,93 +14785,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
12576
14785
|
if (t === "gemini") return await setupGemini();
|
|
12577
14786
|
if (t === "claude") return await setupClaude();
|
|
12578
14787
|
if (t === "cursor") return await setupCursor();
|
|
14788
|
+
if (t === "windsurf") return await setupWindsurf();
|
|
14789
|
+
if (t === "vscode") return await setupVSCode();
|
|
12579
14790
|
if (t === "hud") return setupHud();
|
|
12580
|
-
console.error(
|
|
14791
|
+
console.error(
|
|
14792
|
+
chalk25.red(
|
|
14793
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14794
|
+
)
|
|
14795
|
+
);
|
|
12581
14796
|
process.exit(1);
|
|
12582
14797
|
});
|
|
12583
|
-
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument(
|
|
14798
|
+
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument(
|
|
14799
|
+
"<target>",
|
|
14800
|
+
"The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
|
|
14801
|
+
).action((target) => {
|
|
12584
14802
|
let fn;
|
|
12585
14803
|
if (target === "claude") fn = teardownClaude;
|
|
12586
14804
|
else if (target === "gemini") fn = teardownGemini;
|
|
12587
14805
|
else if (target === "cursor") fn = teardownCursor;
|
|
14806
|
+
else if (target === "windsurf") fn = teardownWindsurf;
|
|
14807
|
+
else if (target === "vscode") fn = teardownVSCode;
|
|
12588
14808
|
else if (target === "hud") fn = teardownHud;
|
|
12589
14809
|
else {
|
|
12590
14810
|
console.error(
|
|
12591
|
-
|
|
14811
|
+
chalk25.red(
|
|
14812
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14813
|
+
)
|
|
12592
14814
|
);
|
|
12593
14815
|
process.exit(1);
|
|
12594
14816
|
}
|
|
12595
|
-
console.log(
|
|
14817
|
+
console.log(chalk25.cyan(`
|
|
12596
14818
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
12597
14819
|
`));
|
|
12598
14820
|
try {
|
|
12599
14821
|
fn();
|
|
12600
14822
|
} catch (err2) {
|
|
12601
|
-
console.error(
|
|
14823
|
+
console.error(chalk25.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12602
14824
|
process.exit(1);
|
|
12603
14825
|
}
|
|
12604
|
-
console.log(
|
|
14826
|
+
console.log(chalk25.gray("\n Restart the agent for changes to take effect."));
|
|
12605
14827
|
});
|
|
12606
14828
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
12607
|
-
console.log(
|
|
12608
|
-
console.log(
|
|
14829
|
+
console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
14830
|
+
console.log(chalk25.bold("Stopping daemon..."));
|
|
12609
14831
|
try {
|
|
12610
14832
|
stopDaemon();
|
|
12611
|
-
console.log(
|
|
14833
|
+
console.log(chalk25.green(" \u2705 Daemon stopped"));
|
|
12612
14834
|
} catch {
|
|
12613
|
-
console.log(
|
|
14835
|
+
console.log(chalk25.blue(" \u2139\uFE0F Daemon was not running"));
|
|
12614
14836
|
}
|
|
12615
|
-
console.log(
|
|
14837
|
+
console.log(chalk25.bold("\nRemoving hooks..."));
|
|
12616
14838
|
let teardownFailed = false;
|
|
12617
14839
|
for (const [label, fn] of [
|
|
12618
14840
|
["Claude", teardownClaude],
|
|
12619
14841
|
["Gemini", teardownGemini],
|
|
12620
|
-
["Cursor", teardownCursor]
|
|
14842
|
+
["Cursor", teardownCursor],
|
|
14843
|
+
["Windsurf", teardownWindsurf],
|
|
14844
|
+
["VSCode", teardownVSCode]
|
|
12621
14845
|
]) {
|
|
12622
14846
|
try {
|
|
12623
14847
|
fn();
|
|
12624
14848
|
} catch (err2) {
|
|
12625
14849
|
teardownFailed = true;
|
|
12626
14850
|
console.error(
|
|
12627
|
-
|
|
14851
|
+
chalk25.red(
|
|
12628
14852
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
12629
14853
|
)
|
|
12630
14854
|
);
|
|
12631
14855
|
}
|
|
12632
14856
|
}
|
|
12633
14857
|
if (options.purge) {
|
|
12634
|
-
const node9Dir =
|
|
12635
|
-
if (
|
|
14858
|
+
const node9Dir = path40.join(os33.homedir(), ".node9");
|
|
14859
|
+
if (fs37.existsSync(node9Dir)) {
|
|
12636
14860
|
const confirmed = await confirm2({
|
|
12637
14861
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
12638
14862
|
default: false
|
|
12639
14863
|
});
|
|
12640
14864
|
if (confirmed) {
|
|
12641
|
-
|
|
12642
|
-
if (
|
|
14865
|
+
fs37.rmSync(node9Dir, { recursive: true });
|
|
14866
|
+
if (fs37.existsSync(node9Dir)) {
|
|
12643
14867
|
console.error(
|
|
12644
|
-
|
|
14868
|
+
chalk25.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
12645
14869
|
);
|
|
12646
14870
|
} else {
|
|
12647
|
-
console.log(
|
|
14871
|
+
console.log(chalk25.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
12648
14872
|
}
|
|
12649
14873
|
} else {
|
|
12650
|
-
console.log(
|
|
14874
|
+
console.log(chalk25.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
12651
14875
|
}
|
|
12652
14876
|
} else {
|
|
12653
|
-
console.log(
|
|
14877
|
+
console.log(chalk25.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
12654
14878
|
}
|
|
12655
14879
|
} else {
|
|
12656
14880
|
console.log(
|
|
12657
|
-
|
|
14881
|
+
chalk25.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
12658
14882
|
);
|
|
12659
14883
|
}
|
|
12660
14884
|
if (teardownFailed) {
|
|
12661
|
-
console.error(
|
|
14885
|
+
console.error(chalk25.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
12662
14886
|
process.exit(1);
|
|
12663
14887
|
}
|
|
12664
|
-
console.log(
|
|
12665
|
-
console.log(
|
|
14888
|
+
console.log(chalk25.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
14889
|
+
console.log(chalk25.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
12666
14890
|
});
|
|
12667
14891
|
registerDoctorCommand(program, version);
|
|
12668
14892
|
program.command("explain").description(
|
|
@@ -12675,7 +14899,7 @@ program.command("explain").description(
|
|
|
12675
14899
|
try {
|
|
12676
14900
|
args = JSON.parse(trimmed);
|
|
12677
14901
|
} catch {
|
|
12678
|
-
console.error(
|
|
14902
|
+
console.error(chalk25.red(`
|
|
12679
14903
|
\u274C Invalid JSON: ${trimmed}
|
|
12680
14904
|
`));
|
|
12681
14905
|
process.exit(1);
|
|
@@ -12686,54 +14910,54 @@ program.command("explain").description(
|
|
|
12686
14910
|
}
|
|
12687
14911
|
const result = await explainPolicy(tool, args);
|
|
12688
14912
|
console.log("");
|
|
12689
|
-
console.log(
|
|
14913
|
+
console.log(chalk25.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
12690
14914
|
console.log("");
|
|
12691
|
-
console.log(` ${
|
|
14915
|
+
console.log(` ${chalk25.bold("Tool:")} ${chalk25.white(result.tool)}`);
|
|
12692
14916
|
if (argsRaw) {
|
|
12693
|
-
const
|
|
12694
|
-
console.log(` ${
|
|
14917
|
+
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
14918
|
+
console.log(` ${chalk25.bold("Input:")} ${chalk25.gray(preview2)}`);
|
|
12695
14919
|
}
|
|
12696
14920
|
console.log("");
|
|
12697
|
-
console.log(
|
|
14921
|
+
console.log(chalk25.bold("Config Sources (Waterfall):"));
|
|
12698
14922
|
for (const tier of result.waterfall) {
|
|
12699
|
-
const
|
|
14923
|
+
const num3 = chalk25.gray(` ${tier.tier}.`);
|
|
12700
14924
|
const label = tier.label.padEnd(16);
|
|
12701
14925
|
let statusStr;
|
|
12702
14926
|
if (tier.tier === 1) {
|
|
12703
|
-
statusStr =
|
|
14927
|
+
statusStr = chalk25.gray(tier.note ?? "");
|
|
12704
14928
|
} else if (tier.status === "active") {
|
|
12705
|
-
const loc = tier.path ?
|
|
12706
|
-
const note = tier.note ?
|
|
12707
|
-
statusStr =
|
|
14929
|
+
const loc = tier.path ? chalk25.gray(tier.path) : "";
|
|
14930
|
+
const note = tier.note ? chalk25.gray(`(${tier.note})`) : "";
|
|
14931
|
+
statusStr = chalk25.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
12708
14932
|
} else {
|
|
12709
|
-
statusStr =
|
|
14933
|
+
statusStr = chalk25.gray("\u25CB " + (tier.note ?? "not found"));
|
|
12710
14934
|
}
|
|
12711
|
-
console.log(`${
|
|
14935
|
+
console.log(`${num3} ${chalk25.white(label)} ${statusStr}`);
|
|
12712
14936
|
}
|
|
12713
14937
|
console.log("");
|
|
12714
|
-
console.log(
|
|
14938
|
+
console.log(chalk25.bold("Policy Evaluation:"));
|
|
12715
14939
|
for (const step of result.steps) {
|
|
12716
14940
|
const isFinal = step.isFinal;
|
|
12717
14941
|
let icon;
|
|
12718
|
-
if (step.outcome === "allow") icon =
|
|
12719
|
-
else if (step.outcome === "review") icon =
|
|
12720
|
-
else if (step.outcome === "skip") icon =
|
|
12721
|
-
else icon =
|
|
14942
|
+
if (step.outcome === "allow") icon = chalk25.green(" \u2705");
|
|
14943
|
+
else if (step.outcome === "review") icon = chalk25.red(" \u{1F534}");
|
|
14944
|
+
else if (step.outcome === "skip") icon = chalk25.gray(" \u2500 ");
|
|
14945
|
+
else icon = chalk25.gray(" \u25CB ");
|
|
12722
14946
|
const name = step.name.padEnd(18);
|
|
12723
|
-
const nameStr = isFinal ?
|
|
12724
|
-
const detail = isFinal ?
|
|
12725
|
-
const arrow = isFinal ?
|
|
14947
|
+
const nameStr = isFinal ? chalk25.white.bold(name) : chalk25.white(name);
|
|
14948
|
+
const detail = isFinal ? chalk25.white(step.detail) : chalk25.gray(step.detail);
|
|
14949
|
+
const arrow = isFinal ? chalk25.yellow(" \u2190 STOP") : "";
|
|
12726
14950
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
12727
14951
|
}
|
|
12728
14952
|
console.log("");
|
|
12729
14953
|
if (result.decision === "allow") {
|
|
12730
|
-
console.log(
|
|
14954
|
+
console.log(chalk25.green.bold(" Decision: \u2705 ALLOW") + chalk25.gray(" \u2014 no approval needed"));
|
|
12731
14955
|
} else {
|
|
12732
14956
|
console.log(
|
|
12733
|
-
|
|
14957
|
+
chalk25.red.bold(" Decision: \u{1F534} REVIEW") + chalk25.gray(" \u2014 human approval required")
|
|
12734
14958
|
);
|
|
12735
14959
|
if (result.blockedByLabel) {
|
|
12736
|
-
console.log(
|
|
14960
|
+
console.log(chalk25.gray(` Reason: ${result.blockedByLabel}`));
|
|
12737
14961
|
}
|
|
12738
14962
|
}
|
|
12739
14963
|
console.log("");
|
|
@@ -12748,7 +14972,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
12748
14972
|
try {
|
|
12749
14973
|
await startTail2(options);
|
|
12750
14974
|
} catch (err2) {
|
|
12751
|
-
console.error(
|
|
14975
|
+
console.error(chalk25.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12752
14976
|
process.exit(1);
|
|
12753
14977
|
}
|
|
12754
14978
|
});
|
|
@@ -12756,6 +14980,7 @@ registerWatchCommand(program);
|
|
|
12756
14980
|
registerMcpGatewayCommand(program);
|
|
12757
14981
|
registerMcpServerCommand(program);
|
|
12758
14982
|
registerMcpPinCommand(program);
|
|
14983
|
+
registerSkillPinCommand(program);
|
|
12759
14984
|
registerCheckCommand(program);
|
|
12760
14985
|
registerLogCommand(program);
|
|
12761
14986
|
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
|
|
@@ -12780,14 +15005,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
12780
15005
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
12781
15006
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
12782
15007
|
if (subcommand === "debug") {
|
|
12783
|
-
const flagFile =
|
|
15008
|
+
const flagFile = path40.join(os33.homedir(), ".node9", "hud-debug");
|
|
12784
15009
|
if (state === "on") {
|
|
12785
|
-
|
|
12786
|
-
|
|
15010
|
+
fs37.mkdirSync(path40.dirname(flagFile), { recursive: true });
|
|
15011
|
+
fs37.writeFileSync(flagFile, "");
|
|
12787
15012
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
12788
15013
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
12789
15014
|
} else if (state === "off") {
|
|
12790
|
-
if (
|
|
15015
|
+
if (fs37.existsSync(flagFile)) fs37.unlinkSync(flagFile);
|
|
12791
15016
|
console.log("HUD debug logging disabled.");
|
|
12792
15017
|
} else {
|
|
12793
15018
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12802,7 +15027,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12802
15027
|
const ms = parseDuration(options.duration);
|
|
12803
15028
|
if (ms === null) {
|
|
12804
15029
|
console.error(
|
|
12805
|
-
|
|
15030
|
+
chalk25.red(`
|
|
12806
15031
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12807
15032
|
`)
|
|
12808
15033
|
);
|
|
@@ -12810,20 +15035,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12810
15035
|
}
|
|
12811
15036
|
pauseNode9(ms, options.duration);
|
|
12812
15037
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12813
|
-
console.log(
|
|
15038
|
+
console.log(chalk25.yellow(`
|
|
12814
15039
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12815
|
-
console.log(
|
|
12816
|
-
console.log(
|
|
15040
|
+
console.log(chalk25.gray(` All tool calls will be allowed without review.`));
|
|
15041
|
+
console.log(chalk25.gray(` Run "node9 resume" to re-enable early.
|
|
12817
15042
|
`));
|
|
12818
15043
|
});
|
|
12819
15044
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12820
15045
|
const { paused } = checkPause();
|
|
12821
15046
|
if (!paused) {
|
|
12822
|
-
console.log(
|
|
15047
|
+
console.log(chalk25.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12823
15048
|
return;
|
|
12824
15049
|
}
|
|
12825
15050
|
resumeNode9();
|
|
12826
|
-
console.log(
|
|
15051
|
+
console.log(chalk25.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12827
15052
|
});
|
|
12828
15053
|
var HOOK_BASED_AGENTS = {
|
|
12829
15054
|
claude: "claude",
|
|
@@ -12836,15 +15061,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12836
15061
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12837
15062
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12838
15063
|
console.error(
|
|
12839
|
-
|
|
15064
|
+
chalk25.yellow(`
|
|
12840
15065
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12841
15066
|
);
|
|
12842
|
-
console.error(
|
|
15067
|
+
console.error(chalk25.white(`
|
|
12843
15068
|
"${target}" uses its own hook system. Use:`));
|
|
12844
15069
|
console.error(
|
|
12845
|
-
|
|
15070
|
+
chalk25.green(` node9 addto ${target} `) + chalk25.gray("# one-time setup")
|
|
12846
15071
|
);
|
|
12847
|
-
console.error(
|
|
15072
|
+
console.error(chalk25.green(` ${target} `) + chalk25.gray("# run normally"));
|
|
12848
15073
|
process.exit(1);
|
|
12849
15074
|
}
|
|
12850
15075
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12861,7 +15086,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12861
15086
|
}
|
|
12862
15087
|
);
|
|
12863
15088
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12864
|
-
console.error(
|
|
15089
|
+
console.error(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12865
15090
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12866
15091
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12867
15092
|
}
|
|
@@ -12874,12 +15099,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12874
15099
|
}
|
|
12875
15100
|
if (!result.approved) {
|
|
12876
15101
|
console.error(
|
|
12877
|
-
|
|
15102
|
+
chalk25.red(`
|
|
12878
15103
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12879
15104
|
);
|
|
12880
15105
|
process.exit(1);
|
|
12881
15106
|
}
|
|
12882
|
-
console.error(
|
|
15107
|
+
console.error(chalk25.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12883
15108
|
await runProxy(fullCommand);
|
|
12884
15109
|
} else {
|
|
12885
15110
|
program.help();
|
|
@@ -12889,14 +15114,18 @@ registerUndoCommand(program);
|
|
|
12889
15114
|
registerShieldCommand(program);
|
|
12890
15115
|
registerConfigShowCommand(program);
|
|
12891
15116
|
registerTrustCommand(program);
|
|
15117
|
+
registerSyncCommand(program);
|
|
15118
|
+
registerAgentsCommand(program);
|
|
15119
|
+
registerScanCommand(program);
|
|
15120
|
+
registerSessionsCommand(program);
|
|
12892
15121
|
if (process.argv[2] !== "daemon") {
|
|
12893
15122
|
process.on("unhandledRejection", (reason) => {
|
|
12894
15123
|
const isCheckHook = process.argv[2] === "check";
|
|
12895
15124
|
if (isCheckHook) {
|
|
12896
15125
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12897
|
-
const logPath =
|
|
15126
|
+
const logPath = path40.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
12898
15127
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12899
|
-
|
|
15128
|
+
fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12900
15129
|
`);
|
|
12901
15130
|
}
|
|
12902
15131
|
process.exit(0);
|