@node9/proxy 1.10.1 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -4
- package/dist/cli.js +2300 -480
- package/dist/cli.mjs +2279 -458
- package/dist/index.js +52 -8
- package/dist/index.mjs +52 -8
- 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 path39 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
151
|
+
return ` \u2022 ${path39}: ${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(),
|
|
@@ -495,11 +496,11 @@ function getGlobalSettings() {
|
|
|
495
496
|
};
|
|
496
497
|
}
|
|
497
498
|
function getCredentials() {
|
|
498
|
-
const
|
|
499
|
+
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
499
500
|
if (process.env.NODE9_API_KEY) {
|
|
500
501
|
return {
|
|
501
502
|
apiKey: process.env.NODE9_API_KEY,
|
|
502
|
-
apiUrl: process.env.NODE9_API_URL ||
|
|
503
|
+
apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL2
|
|
503
504
|
};
|
|
504
505
|
}
|
|
505
506
|
try {
|
|
@@ -511,13 +512,13 @@ function getCredentials() {
|
|
|
511
512
|
if (profile?.apiKey) {
|
|
512
513
|
return {
|
|
513
514
|
apiKey: profile.apiKey,
|
|
514
|
-
apiUrl: profile.apiUrl ||
|
|
515
|
+
apiUrl: profile.apiUrl || DEFAULT_API_URL2
|
|
515
516
|
};
|
|
516
517
|
}
|
|
517
518
|
if (creds.apiKey) {
|
|
518
519
|
return {
|
|
519
520
|
apiKey: creds.apiKey,
|
|
520
|
-
apiUrl: creds.apiUrl ||
|
|
521
|
+
apiUrl: creds.apiUrl || DEFAULT_API_URL2
|
|
521
522
|
};
|
|
522
523
|
}
|
|
523
524
|
}
|
|
@@ -568,6 +569,8 @@ function getConfig(cwd) {
|
|
|
568
569
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
569
570
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
570
571
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
572
|
+
if (s.cloudSyncIntervalHours !== void 0)
|
|
573
|
+
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
571
574
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
572
575
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
573
576
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -577,7 +580,12 @@ function getConfig(cwd) {
|
|
|
577
580
|
if (p.smartRules) {
|
|
578
581
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
579
582
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
580
|
-
|
|
583
|
+
const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
|
|
584
|
+
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
585
|
+
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
586
|
+
(r) => !r.name || !userRuleNames.has(r.name)
|
|
587
|
+
);
|
|
588
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
|
|
581
589
|
}
|
|
582
590
|
if (p.snapshot) {
|
|
583
591
|
const s2 = p.snapshot;
|
|
@@ -611,6 +619,16 @@ function getConfig(cwd) {
|
|
|
611
619
|
};
|
|
612
620
|
applyLayer(globalConfig);
|
|
613
621
|
applyLayer(projectConfig);
|
|
622
|
+
{
|
|
623
|
+
const cacheFile = path3.join(os3.homedir(), ".node9", "rules-cache.json");
|
|
624
|
+
try {
|
|
625
|
+
const raw = JSON.parse(fs3.readFileSync(cacheFile, "utf-8"));
|
|
626
|
+
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
627
|
+
applyLayer({ policy: { smartRules: raw.rules } });
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
614
632
|
const shieldOverrides = readShieldOverrides();
|
|
615
633
|
for (const shieldName of readActiveShields()) {
|
|
616
634
|
const shield = getShield(shieldName);
|
|
@@ -726,7 +744,8 @@ var init_config = __esm({
|
|
|
726
744
|
// 120-second auto-deny timeout
|
|
727
745
|
flightRecorder: true,
|
|
728
746
|
auditHashArgs: true,
|
|
729
|
-
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
747
|
+
approvers: { native: true, browser: true, cloud: false, terminal: true },
|
|
748
|
+
cloudSyncIntervalHours: 5
|
|
730
749
|
},
|
|
731
750
|
policy: {
|
|
732
751
|
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
@@ -1710,9 +1729,9 @@ function matchesPattern(text, patterns) {
|
|
|
1710
1729
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1711
1730
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1712
1731
|
}
|
|
1713
|
-
function getNestedValue(obj,
|
|
1732
|
+
function getNestedValue(obj, path39) {
|
|
1714
1733
|
if (!obj || typeof obj !== "object") return null;
|
|
1715
|
-
return
|
|
1734
|
+
return path39.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1716
1735
|
}
|
|
1717
1736
|
function shouldSnapshot(toolName, args, config) {
|
|
1718
1737
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2187,8 +2206,8 @@ async function explainPolicy(toolName, args) {
|
|
|
2187
2206
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
2188
2207
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
2189
2208
|
allTokens.push(...extraTokens);
|
|
2190
|
-
const
|
|
2191
|
-
detail += ` + deep scan of args: [${
|
|
2209
|
+
const preview2 = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
|
|
2210
|
+
detail += ` + deep scan of args: [${preview2}]`;
|
|
2192
2211
|
}
|
|
2193
2212
|
steps.push({ name: "Input parsing", outcome: "checked", detail });
|
|
2194
2213
|
}
|
|
@@ -2440,19 +2459,44 @@ function getInternalToken() {
|
|
|
2440
2459
|
function isDaemonRunning() {
|
|
2441
2460
|
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
2442
2461
|
if (fs9.existsSync(pidFile)) {
|
|
2462
|
+
let pid;
|
|
2463
|
+
let port;
|
|
2443
2464
|
try {
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
return true;
|
|
2465
|
+
const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
2466
|
+
pid = data.pid;
|
|
2467
|
+
port = data.port;
|
|
2448
2468
|
} catch {
|
|
2449
2469
|
return false;
|
|
2450
2470
|
}
|
|
2471
|
+
if (port !== DAEMON_PORT) {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
const MAX_PID2 = 4194304;
|
|
2475
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
|
|
2476
|
+
return false;
|
|
2477
|
+
}
|
|
2478
|
+
try {
|
|
2479
|
+
process.kill(pid, 0);
|
|
2480
|
+
} catch (err2) {
|
|
2481
|
+
if (err2 instanceof Error && "code" in err2 && err2.code === "ESRCH") {
|
|
2482
|
+
try {
|
|
2483
|
+
fs9.unlinkSync(pidFile);
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return false;
|
|
2488
|
+
}
|
|
2489
|
+
const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
2490
|
+
encoding: "utf8",
|
|
2491
|
+
timeout: 300
|
|
2492
|
+
});
|
|
2493
|
+
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) return true;
|
|
2494
|
+
return false;
|
|
2451
2495
|
}
|
|
2452
2496
|
try {
|
|
2453
2497
|
const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
2454
2498
|
encoding: "utf8",
|
|
2455
|
-
timeout:
|
|
2499
|
+
timeout: 300
|
|
2456
2500
|
});
|
|
2457
2501
|
return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
|
|
2458
2502
|
} catch {
|
|
@@ -6218,15 +6262,152 @@ var init_costSync = __esm({
|
|
|
6218
6262
|
}
|
|
6219
6263
|
});
|
|
6220
6264
|
|
|
6221
|
-
// src/daemon/
|
|
6222
|
-
import http from "http";
|
|
6265
|
+
// src/daemon/sync.ts
|
|
6223
6266
|
import fs17 from "fs";
|
|
6267
|
+
import https from "https";
|
|
6268
|
+
import os15 from "os";
|
|
6224
6269
|
import path20 from "path";
|
|
6270
|
+
function readCredentials() {
|
|
6271
|
+
if (process.env.NODE9_API_KEY) {
|
|
6272
|
+
return {
|
|
6273
|
+
apiKey: process.env.NODE9_API_KEY,
|
|
6274
|
+
apiUrl: process.env.NODE9_API_URL ?? DEFAULT_API_URL
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
try {
|
|
6278
|
+
const credPath = path20.join(os15.homedir(), ".node9", "credentials.json");
|
|
6279
|
+
const creds = JSON.parse(fs17.readFileSync(credPath, "utf-8"));
|
|
6280
|
+
const profileName = process.env.NODE9_PROFILE ?? "default";
|
|
6281
|
+
const profile = creds[profileName];
|
|
6282
|
+
if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
|
|
6283
|
+
return {
|
|
6284
|
+
apiKey: profile.apiKey,
|
|
6285
|
+
apiUrl: typeof profile.apiUrl === "string" ? profile.apiUrl.replace(/\/intercept$/, "/policy") : DEFAULT_API_URL
|
|
6286
|
+
};
|
|
6287
|
+
}
|
|
6288
|
+
if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
|
|
6289
|
+
return { apiKey: creds.apiKey, apiUrl: DEFAULT_API_URL };
|
|
6290
|
+
}
|
|
6291
|
+
} catch {
|
|
6292
|
+
}
|
|
6293
|
+
return null;
|
|
6294
|
+
}
|
|
6295
|
+
function fetchCloudRules(apiKey, apiUrl) {
|
|
6296
|
+
const parsed = new URL(apiUrl);
|
|
6297
|
+
return new Promise((resolve, reject) => {
|
|
6298
|
+
const req = https.request(
|
|
6299
|
+
{
|
|
6300
|
+
hostname: parsed.hostname,
|
|
6301
|
+
port: parsed.port ? parseInt(parsed.port, 10) : void 0,
|
|
6302
|
+
path: parsed.pathname + parsed.search,
|
|
6303
|
+
method: "GET",
|
|
6304
|
+
headers: {
|
|
6305
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6306
|
+
"Content-Type": "application/json"
|
|
6307
|
+
},
|
|
6308
|
+
timeout: 1e4
|
|
6309
|
+
},
|
|
6310
|
+
(res) => {
|
|
6311
|
+
const chunks = [];
|
|
6312
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
6313
|
+
res.on("end", () => {
|
|
6314
|
+
if (res.statusCode !== 200) {
|
|
6315
|
+
reject(new Error(`API returned ${res.statusCode ?? "unknown"}`));
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
try {
|
|
6319
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
6320
|
+
const rules = Array.isArray(body) ? body : Array.isArray(body.rules) ? body.rules : [];
|
|
6321
|
+
resolve(rules);
|
|
6322
|
+
} catch (e) {
|
|
6323
|
+
reject(e);
|
|
6324
|
+
}
|
|
6325
|
+
});
|
|
6326
|
+
}
|
|
6327
|
+
);
|
|
6328
|
+
req.on("error", reject);
|
|
6329
|
+
req.on("timeout", () => {
|
|
6330
|
+
req.destroy(new Error("Cloud policy fetch timed out"));
|
|
6331
|
+
});
|
|
6332
|
+
req.end();
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
async function syncOnce() {
|
|
6336
|
+
const creds = readCredentials();
|
|
6337
|
+
if (!creds) return;
|
|
6338
|
+
try {
|
|
6339
|
+
const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
|
|
6340
|
+
const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
|
|
6341
|
+
const dir = path20.dirname(rulesCacheFile());
|
|
6342
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
6343
|
+
fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
6344
|
+
} catch {
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
async function runCloudSync() {
|
|
6348
|
+
const creds = readCredentials();
|
|
6349
|
+
if (!creds) {
|
|
6350
|
+
return { ok: false, reason: "No API key configured. Add credentials with: node9 login" };
|
|
6351
|
+
}
|
|
6352
|
+
try {
|
|
6353
|
+
const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
|
|
6354
|
+
const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
|
|
6355
|
+
const dir = path20.dirname(rulesCacheFile());
|
|
6356
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
6357
|
+
fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
6358
|
+
return { ok: true, rules: rules.length, fetchedAt: cache.fetchedAt };
|
|
6359
|
+
} catch (err2) {
|
|
6360
|
+
return { ok: false, reason: err2 instanceof Error ? err2.message : String(err2) };
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
function getCloudSyncStatus() {
|
|
6364
|
+
try {
|
|
6365
|
+
const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
|
|
6366
|
+
if (!Array.isArray(raw.rules) || typeof raw.fetchedAt !== "string") return { cached: false };
|
|
6367
|
+
return { cached: true, rules: raw.rules.length, fetchedAt: raw.fetchedAt };
|
|
6368
|
+
} catch {
|
|
6369
|
+
return { cached: false };
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
function getCloudRules() {
|
|
6373
|
+
try {
|
|
6374
|
+
const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
|
|
6375
|
+
return Array.isArray(raw.rules) ? raw.rules : null;
|
|
6376
|
+
} catch {
|
|
6377
|
+
return null;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
function startCloudSync() {
|
|
6381
|
+
const rawHours = getConfig().settings.cloudSyncIntervalHours ?? DEFAULT_INTERVAL_HOURS;
|
|
6382
|
+
const intervalHours = Math.max(rawHours, MIN_INTERVAL_HOURS);
|
|
6383
|
+
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
6384
|
+
const initial = setTimeout(() => void syncOnce(), 3e4);
|
|
6385
|
+
initial.unref();
|
|
6386
|
+
const recurring = setInterval(() => void syncOnce(), intervalMs);
|
|
6387
|
+
recurring.unref();
|
|
6388
|
+
}
|
|
6389
|
+
var rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
|
|
6390
|
+
var init_sync = __esm({
|
|
6391
|
+
"src/daemon/sync.ts"() {
|
|
6392
|
+
"use strict";
|
|
6393
|
+
init_config();
|
|
6394
|
+
rulesCacheFile = () => path20.join(os15.homedir(), ".node9", "rules-cache.json");
|
|
6395
|
+
DEFAULT_API_URL = "https://api.node9.ai/api/v1/policy";
|
|
6396
|
+
DEFAULT_INTERVAL_HOURS = 5;
|
|
6397
|
+
MIN_INTERVAL_HOURS = 1;
|
|
6398
|
+
}
|
|
6399
|
+
});
|
|
6400
|
+
|
|
6401
|
+
// src/daemon/server.ts
|
|
6402
|
+
import http from "http";
|
|
6403
|
+
import fs18 from "fs";
|
|
6404
|
+
import path21 from "path";
|
|
6225
6405
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6226
6406
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6227
6407
|
import chalk2 from "chalk";
|
|
6228
6408
|
function startDaemon() {
|
|
6229
6409
|
startCostSync();
|
|
6410
|
+
startCloudSync();
|
|
6230
6411
|
loadInsightCounts();
|
|
6231
6412
|
const csrfToken = randomUUID4();
|
|
6232
6413
|
const internalToken = randomUUID4();
|
|
@@ -6242,7 +6423,7 @@ function startDaemon() {
|
|
|
6242
6423
|
idleTimer = setTimeout(() => {
|
|
6243
6424
|
if (autoStarted) {
|
|
6244
6425
|
try {
|
|
6245
|
-
|
|
6426
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6246
6427
|
} catch {
|
|
6247
6428
|
}
|
|
6248
6429
|
}
|
|
@@ -6405,7 +6586,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6405
6586
|
status: "pending"
|
|
6406
6587
|
});
|
|
6407
6588
|
}
|
|
6408
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6589
|
+
const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
|
|
6409
6590
|
const projectConfig = getConfig(projectCwd);
|
|
6410
6591
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6411
6592
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6795,8 +6976,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6795
6976
|
const body = await readBody(req);
|
|
6796
6977
|
const data = body ? JSON.parse(body) : {};
|
|
6797
6978
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6798
|
-
const node9Dir =
|
|
6799
|
-
if (!
|
|
6979
|
+
const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
|
|
6980
|
+
if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
|
|
6800
6981
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6801
6982
|
return res.end(
|
|
6802
6983
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6907,14 +7088,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6907
7088
|
server.on("error", (e) => {
|
|
6908
7089
|
if (e.code === "EADDRINUSE") {
|
|
6909
7090
|
try {
|
|
6910
|
-
if (
|
|
6911
|
-
const { pid } = JSON.parse(
|
|
7091
|
+
if (fs18.existsSync(DAEMON_PID_FILE)) {
|
|
7092
|
+
const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6912
7093
|
process.kill(pid, 0);
|
|
6913
7094
|
return process.exit(0);
|
|
6914
7095
|
}
|
|
6915
7096
|
} catch {
|
|
6916
7097
|
try {
|
|
6917
|
-
|
|
7098
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6918
7099
|
} catch {
|
|
6919
7100
|
}
|
|
6920
7101
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -6983,56 +7164,327 @@ var init_server = __esm({
|
|
|
6983
7164
|
init_patch();
|
|
6984
7165
|
init_config_schema();
|
|
6985
7166
|
init_costSync();
|
|
7167
|
+
init_sync();
|
|
7168
|
+
}
|
|
7169
|
+
});
|
|
7170
|
+
|
|
7171
|
+
// src/daemon/service.ts
|
|
7172
|
+
import fs19 from "fs";
|
|
7173
|
+
import path22 from "path";
|
|
7174
|
+
import os16 from "os";
|
|
7175
|
+
import { spawnSync as spawnSync3, execFileSync } from "child_process";
|
|
7176
|
+
function resolveNode9Binary() {
|
|
7177
|
+
try {
|
|
7178
|
+
const script = process.argv[1];
|
|
7179
|
+
if (typeof script === "string" && path22.isAbsolute(script) && fs19.existsSync(script)) {
|
|
7180
|
+
return fs19.realpathSync(script);
|
|
7181
|
+
}
|
|
7182
|
+
} catch {
|
|
7183
|
+
}
|
|
7184
|
+
try {
|
|
7185
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
7186
|
+
const r = spawnSync3(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
|
|
7187
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
7188
|
+
return r.stdout.trim().split("\n")[0].trim();
|
|
7189
|
+
}
|
|
7190
|
+
} catch {
|
|
7191
|
+
}
|
|
7192
|
+
return null;
|
|
7193
|
+
}
|
|
7194
|
+
function xmlEscape(s) {
|
|
7195
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7196
|
+
}
|
|
7197
|
+
function launchdPlist(binaryPath) {
|
|
7198
|
+
const logDir = path22.join(os16.homedir(), ".node9");
|
|
7199
|
+
const nodePath = xmlEscape(process.execPath);
|
|
7200
|
+
const scriptPath = xmlEscape(binaryPath);
|
|
7201
|
+
const outLog = xmlEscape(path22.join(logDir, "daemon.log"));
|
|
7202
|
+
const errLog = xmlEscape(path22.join(logDir, "daemon-error.log"));
|
|
7203
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7204
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7205
|
+
<plist version="1.0">
|
|
7206
|
+
<dict>
|
|
7207
|
+
<key>Label</key>
|
|
7208
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
7209
|
+
<key>ProgramArguments</key>
|
|
7210
|
+
<array>
|
|
7211
|
+
<string>${nodePath}</string>
|
|
7212
|
+
<string>${scriptPath}</string>
|
|
7213
|
+
<string>daemon</string>
|
|
7214
|
+
</array>
|
|
7215
|
+
<key>RunAtLoad</key>
|
|
7216
|
+
<true/>
|
|
7217
|
+
<key>KeepAlive</key>
|
|
7218
|
+
<true/>
|
|
7219
|
+
<key>ThrottleInterval</key>
|
|
7220
|
+
<integer>10</integer>
|
|
7221
|
+
<key>StandardOutPath</key>
|
|
7222
|
+
<string>${outLog}</string>
|
|
7223
|
+
<key>StandardErrorPath</key>
|
|
7224
|
+
<string>${errLog}</string>
|
|
7225
|
+
<key>EnvironmentVariables</key>
|
|
7226
|
+
<dict>
|
|
7227
|
+
<key>NODE9_AUTO_STARTED</key>
|
|
7228
|
+
<string>1</string>
|
|
7229
|
+
<key>NODE9_BROWSER_OPENED</key>
|
|
7230
|
+
<string>1</string>
|
|
7231
|
+
</dict>
|
|
7232
|
+
</dict>
|
|
7233
|
+
</plist>
|
|
7234
|
+
`;
|
|
7235
|
+
}
|
|
7236
|
+
function installLaunchd(binaryPath) {
|
|
7237
|
+
const dir = path22.dirname(LAUNCHD_PLIST);
|
|
7238
|
+
if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
|
|
7239
|
+
fs19.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
|
|
7240
|
+
spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
|
|
7241
|
+
const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
|
|
7242
|
+
encoding: "utf8",
|
|
7243
|
+
timeout: 5e3
|
|
7244
|
+
});
|
|
7245
|
+
if (r.status !== 0) {
|
|
7246
|
+
throw new Error(`launchctl load failed: ${r.stderr || r.stdout || "unknown error"}`);
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
function uninstallLaunchd() {
|
|
7250
|
+
if (fs19.existsSync(LAUNCHD_PLIST)) {
|
|
7251
|
+
spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
|
|
7252
|
+
fs19.unlinkSync(LAUNCHD_PLIST);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
function isLaunchdInstalled() {
|
|
7256
|
+
return fs19.existsSync(LAUNCHD_PLIST);
|
|
7257
|
+
}
|
|
7258
|
+
function systemdUnit(binaryPath) {
|
|
7259
|
+
return `[Unit]
|
|
7260
|
+
Description=node9 approval daemon
|
|
7261
|
+
After=network.target
|
|
7262
|
+
|
|
7263
|
+
[Service]
|
|
7264
|
+
Type=simple
|
|
7265
|
+
ExecStart=${process.execPath} ${binaryPath} daemon
|
|
7266
|
+
Restart=on-failure
|
|
7267
|
+
RestartSec=10s
|
|
7268
|
+
Environment=NODE9_AUTO_STARTED=1
|
|
7269
|
+
Environment=NODE9_BROWSER_OPENED=1
|
|
7270
|
+
|
|
7271
|
+
[Install]
|
|
7272
|
+
WantedBy=default.target
|
|
7273
|
+
`;
|
|
7274
|
+
}
|
|
7275
|
+
function installSystemd(binaryPath) {
|
|
7276
|
+
if (!fs19.existsSync(SYSTEMD_UNIT_DIR)) {
|
|
7277
|
+
fs19.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
|
|
7278
|
+
}
|
|
7279
|
+
fs19.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
|
|
7280
|
+
try {
|
|
7281
|
+
execFileSync("loginctl", ["enable-linger", os16.userInfo().username], { timeout: 3e3 });
|
|
7282
|
+
} catch {
|
|
7283
|
+
}
|
|
7284
|
+
const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
|
|
7285
|
+
encoding: "utf8",
|
|
7286
|
+
timeout: 5e3
|
|
7287
|
+
});
|
|
7288
|
+
if (reload.status !== 0) {
|
|
7289
|
+
throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
|
|
7290
|
+
}
|
|
7291
|
+
spawnSync3("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
|
|
7292
|
+
const enable = spawnSync3("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
|
|
7293
|
+
encoding: "utf8",
|
|
7294
|
+
timeout: 5e3
|
|
7295
|
+
});
|
|
7296
|
+
if (enable.status !== 0) {
|
|
7297
|
+
throw new Error(`systemctl enable failed: ${enable.stderr}`);
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
function uninstallSystemd() {
|
|
7301
|
+
if (fs19.existsSync(SYSTEMD_UNIT)) {
|
|
7302
|
+
spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
|
|
7303
|
+
encoding: "utf8",
|
|
7304
|
+
timeout: 5e3
|
|
7305
|
+
});
|
|
7306
|
+
spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
|
|
7307
|
+
fs19.unlinkSync(SYSTEMD_UNIT);
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
function isSystemdInstalled() {
|
|
7311
|
+
return fs19.existsSync(SYSTEMD_UNIT);
|
|
7312
|
+
}
|
|
7313
|
+
function stopRunningDaemon() {
|
|
7314
|
+
const pidFile = path22.join(os16.homedir(), ".node9", "daemon.pid");
|
|
7315
|
+
if (!fs19.existsSync(pidFile)) return;
|
|
7316
|
+
try {
|
|
7317
|
+
const data = JSON.parse(fs19.readFileSync(pidFile, "utf-8"));
|
|
7318
|
+
const pid = data.pid;
|
|
7319
|
+
const MAX_PID2 = 4194304;
|
|
7320
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
|
|
7321
|
+
try {
|
|
7322
|
+
process.kill(pid, "SIGTERM");
|
|
7323
|
+
const deadline = Date.now() + 3e3;
|
|
7324
|
+
const pollStop = spawnSync3(
|
|
7325
|
+
"sh",
|
|
7326
|
+
["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
|
|
7327
|
+
{
|
|
7328
|
+
timeout: 3100
|
|
7329
|
+
}
|
|
7330
|
+
);
|
|
7331
|
+
void pollStop;
|
|
7332
|
+
void deadline;
|
|
7333
|
+
} catch {
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
try {
|
|
7337
|
+
fs19.unlinkSync(pidFile);
|
|
7338
|
+
} catch {
|
|
7339
|
+
}
|
|
7340
|
+
} catch {
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
function installDaemonService() {
|
|
7344
|
+
const binary = resolveNode9Binary();
|
|
7345
|
+
if (!binary) {
|
|
7346
|
+
return { ok: false, reason: "Could not locate the node9 binary. Is it in your PATH?" };
|
|
7347
|
+
}
|
|
7348
|
+
stopRunningDaemon();
|
|
7349
|
+
try {
|
|
7350
|
+
if (process.platform === "darwin") {
|
|
7351
|
+
const alreadyInstalled = isLaunchdInstalled();
|
|
7352
|
+
installLaunchd(binary);
|
|
7353
|
+
return { ok: true, platform: "launchd", alreadyInstalled };
|
|
7354
|
+
}
|
|
7355
|
+
if (process.platform === "linux") {
|
|
7356
|
+
const check = spawnSync3("systemctl", ["--user", "--version"], {
|
|
7357
|
+
encoding: "utf8",
|
|
7358
|
+
timeout: 2e3
|
|
7359
|
+
});
|
|
7360
|
+
if (check.status !== 0) {
|
|
7361
|
+
return {
|
|
7362
|
+
ok: false,
|
|
7363
|
+
reason: "systemd not available. Start the daemon manually with: node9 daemon start"
|
|
7364
|
+
};
|
|
7365
|
+
}
|
|
7366
|
+
const alreadyInstalled = isSystemdInstalled();
|
|
7367
|
+
installSystemd(binary);
|
|
7368
|
+
return { ok: true, platform: "systemd", alreadyInstalled };
|
|
7369
|
+
}
|
|
7370
|
+
return {
|
|
7371
|
+
ok: false,
|
|
7372
|
+
reason: `Automatic service install is not supported on ${process.platform}. Start the daemon manually with: node9 daemon start`
|
|
7373
|
+
};
|
|
7374
|
+
} catch (err2) {
|
|
7375
|
+
return {
|
|
7376
|
+
ok: false,
|
|
7377
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
7378
|
+
};
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
function uninstallDaemonService() {
|
|
7382
|
+
try {
|
|
7383
|
+
if (process.platform === "darwin") {
|
|
7384
|
+
uninstallLaunchd();
|
|
7385
|
+
return { ok: true, platform: "launchd", alreadyInstalled: false };
|
|
7386
|
+
}
|
|
7387
|
+
if (process.platform === "linux") {
|
|
7388
|
+
uninstallSystemd();
|
|
7389
|
+
return { ok: true, platform: "systemd", alreadyInstalled: false };
|
|
7390
|
+
}
|
|
7391
|
+
return {
|
|
7392
|
+
ok: false,
|
|
7393
|
+
reason: `Service management not supported on ${process.platform}.`
|
|
7394
|
+
};
|
|
7395
|
+
} catch (err2) {
|
|
7396
|
+
return {
|
|
7397
|
+
ok: false,
|
|
7398
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
7399
|
+
};
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
function isDaemonServiceInstalled() {
|
|
7403
|
+
if (process.platform === "darwin") return isLaunchdInstalled();
|
|
7404
|
+
if (process.platform === "linux") return isSystemdInstalled();
|
|
7405
|
+
return false;
|
|
7406
|
+
}
|
|
7407
|
+
var LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
|
|
7408
|
+
var init_service = __esm({
|
|
7409
|
+
"src/daemon/service.ts"() {
|
|
7410
|
+
"use strict";
|
|
7411
|
+
LAUNCHD_LABEL = "ai.node9.daemon";
|
|
7412
|
+
LAUNCHD_PLIST = path22.join(os16.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
7413
|
+
SYSTEMD_UNIT_DIR = path22.join(os16.homedir(), ".config", "systemd", "user");
|
|
7414
|
+
SYSTEMD_UNIT = path22.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
|
|
6986
7415
|
}
|
|
6987
7416
|
});
|
|
6988
7417
|
|
|
6989
7418
|
// src/daemon/index.ts
|
|
6990
|
-
import
|
|
7419
|
+
import fs20 from "fs";
|
|
6991
7420
|
import chalk3 from "chalk";
|
|
6992
|
-
import { spawnSync as
|
|
7421
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
6993
7422
|
function stopDaemon() {
|
|
6994
|
-
if (!
|
|
7423
|
+
if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
6995
7424
|
try {
|
|
6996
|
-
const
|
|
7425
|
+
const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7426
|
+
const pid = data.pid;
|
|
7427
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
7428
|
+
console.log(chalk3.gray("Cleaned up invalid PID file."));
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
6997
7431
|
process.kill(pid, "SIGTERM");
|
|
6998
7432
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
6999
7433
|
} catch {
|
|
7000
7434
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
7001
7435
|
} finally {
|
|
7002
7436
|
try {
|
|
7003
|
-
|
|
7437
|
+
fs20.unlinkSync(DAEMON_PID_FILE);
|
|
7004
7438
|
} catch {
|
|
7005
7439
|
}
|
|
7006
7440
|
}
|
|
7007
7441
|
}
|
|
7008
7442
|
function daemonStatus() {
|
|
7009
|
-
|
|
7443
|
+
const serviceInstalled = isDaemonServiceInstalled();
|
|
7444
|
+
const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
|
|
7445
|
+
let processStatus;
|
|
7446
|
+
if (fs20.existsSync(DAEMON_PID_FILE)) {
|
|
7010
7447
|
try {
|
|
7011
|
-
const
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7448
|
+
const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7449
|
+
const pid = data.pid;
|
|
7450
|
+
const port = data.port;
|
|
7451
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
7452
|
+
processStatus = chalk3.yellow("not running (invalid PID file)");
|
|
7453
|
+
} else {
|
|
7454
|
+
process.kill(pid, 0);
|
|
7455
|
+
processStatus = chalk3.green(
|
|
7456
|
+
`running (PID ${pid}, port ${typeof port === "number" ? port : DAEMON_PORT})`
|
|
7457
|
+
);
|
|
7458
|
+
}
|
|
7015
7459
|
} catch {
|
|
7016
|
-
|
|
7017
|
-
return;
|
|
7460
|
+
processStatus = chalk3.yellow("not running (stale PID file)");
|
|
7018
7461
|
}
|
|
7019
|
-
}
|
|
7020
|
-
const r = spawnSync3("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
7021
|
-
encoding: "utf8",
|
|
7022
|
-
timeout: 500
|
|
7023
|
-
});
|
|
7024
|
-
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
|
|
7025
|
-
console.log(chalk3.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
7026
7462
|
} else {
|
|
7027
|
-
|
|
7463
|
+
const r = spawnSync4("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
7464
|
+
encoding: "utf8",
|
|
7465
|
+
timeout: 500
|
|
7466
|
+
});
|
|
7467
|
+
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
|
|
7468
|
+
processStatus = chalk3.yellow(`running (orphaned \u2014 no PID file)`);
|
|
7469
|
+
} else {
|
|
7470
|
+
processStatus = chalk3.yellow("not running");
|
|
7471
|
+
}
|
|
7028
7472
|
}
|
|
7473
|
+
console.log(`
|
|
7474
|
+
Process : ${processStatus}`);
|
|
7475
|
+
console.log(` Service : ${serviceLabel}
|
|
7476
|
+
`);
|
|
7029
7477
|
}
|
|
7478
|
+
var MAX_PID;
|
|
7030
7479
|
var init_daemon2 = __esm({
|
|
7031
7480
|
"src/daemon/index.ts"() {
|
|
7032
7481
|
"use strict";
|
|
7033
7482
|
init_server();
|
|
7034
7483
|
init_state2();
|
|
7484
|
+
init_service();
|
|
7035
7485
|
init_state2();
|
|
7486
|
+
init_service();
|
|
7487
|
+
MAX_PID = 4194304;
|
|
7036
7488
|
}
|
|
7037
7489
|
});
|
|
7038
7490
|
|
|
@@ -7042,10 +7494,10 @@ __export(tail_exports, {
|
|
|
7042
7494
|
startTail: () => startTail
|
|
7043
7495
|
});
|
|
7044
7496
|
import http2 from "http";
|
|
7045
|
-
import
|
|
7046
|
-
import
|
|
7047
|
-
import
|
|
7048
|
-
import
|
|
7497
|
+
import chalk23 from "chalk";
|
|
7498
|
+
import fs33 from "fs";
|
|
7499
|
+
import os29 from "os";
|
|
7500
|
+
import path36 from "path";
|
|
7049
7501
|
import readline5 from "readline";
|
|
7050
7502
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
7051
7503
|
function getIcon(tool) {
|
|
@@ -7068,22 +7520,22 @@ function formatBase(activity) {
|
|
|
7068
7520
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
7069
7521
|
const icon = getIcon(activity.tool);
|
|
7070
7522
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
7071
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
7523
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os29.homedir(), "~");
|
|
7072
7524
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
7073
|
-
return `${
|
|
7525
|
+
return `${chalk23.gray(time)} ${icon} ${chalk23.white.bold(toolName)} ${chalk23.dim(argsPreview)}`;
|
|
7074
7526
|
}
|
|
7075
7527
|
function renderResult(activity, result) {
|
|
7076
7528
|
const base = formatBase(activity);
|
|
7077
7529
|
let status;
|
|
7078
7530
|
if (result.status === "allow") {
|
|
7079
|
-
status =
|
|
7531
|
+
status = chalk23.green("\u2713 ALLOW");
|
|
7080
7532
|
} else if (result.status === "dlp") {
|
|
7081
|
-
status =
|
|
7533
|
+
status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
7082
7534
|
} else {
|
|
7083
|
-
status =
|
|
7535
|
+
status = chalk23.red("\u2717 BLOCK");
|
|
7084
7536
|
}
|
|
7085
7537
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
7086
|
-
const costSuffix = cost == null ? "" :
|
|
7538
|
+
const costSuffix = cost == null ? "" : chalk23.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
7087
7539
|
if (process.stdout.isTTY) {
|
|
7088
7540
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
7089
7541
|
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -7100,19 +7552,19 @@ function renderResult(activity, result) {
|
|
|
7100
7552
|
}
|
|
7101
7553
|
function renderPending(activity) {
|
|
7102
7554
|
if (!process.stdout.isTTY) return;
|
|
7103
|
-
const line = `${formatBase(activity)} ${
|
|
7555
|
+
const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
|
|
7104
7556
|
pendingShownForId = activity.id;
|
|
7105
7557
|
pendingWrappedLines = wrappedLineCount(line);
|
|
7106
7558
|
process.stdout.write(`${line}\r`);
|
|
7107
7559
|
}
|
|
7108
7560
|
async function ensureDaemon() {
|
|
7109
7561
|
let pidPort = null;
|
|
7110
|
-
if (
|
|
7562
|
+
if (fs33.existsSync(PID_FILE)) {
|
|
7111
7563
|
try {
|
|
7112
|
-
const { port } = JSON.parse(
|
|
7564
|
+
const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
|
|
7113
7565
|
pidPort = port;
|
|
7114
7566
|
} catch {
|
|
7115
|
-
console.error(
|
|
7567
|
+
console.error(chalk23.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
7116
7568
|
}
|
|
7117
7569
|
}
|
|
7118
7570
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -7123,7 +7575,7 @@ async function ensureDaemon() {
|
|
|
7123
7575
|
if (res.ok) return checkPort;
|
|
7124
7576
|
} catch {
|
|
7125
7577
|
}
|
|
7126
|
-
console.log(
|
|
7578
|
+
console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
7127
7579
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
7128
7580
|
detached: true,
|
|
7129
7581
|
stdio: "ignore",
|
|
@@ -7140,7 +7592,7 @@ async function ensureDaemon() {
|
|
|
7140
7592
|
} catch {
|
|
7141
7593
|
}
|
|
7142
7594
|
}
|
|
7143
|
-
console.error(
|
|
7595
|
+
console.error(chalk23.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7144
7596
|
process.exit(1);
|
|
7145
7597
|
}
|
|
7146
7598
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -7172,27 +7624,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
|
7172
7624
|
req.end(body);
|
|
7173
7625
|
});
|
|
7174
7626
|
}
|
|
7627
|
+
function extractArgsSummary(toolName, args) {
|
|
7628
|
+
const a = args;
|
|
7629
|
+
if (!a) return "";
|
|
7630
|
+
const cmd = a["command"] ?? a["cmd"];
|
|
7631
|
+
if (typeof cmd === "string") return cmd.replace(/\s+/g, " ").trim();
|
|
7632
|
+
const fp = a["file_path"] ?? a["path"] ?? a["filepath"];
|
|
7633
|
+
if (typeof fp === "string") return fp;
|
|
7634
|
+
const q = a["query"] ?? a["sql"];
|
|
7635
|
+
if (typeof q === "string") return q.replace(/\s+/g, " ").trim();
|
|
7636
|
+
const s = JSON.stringify(a).replace(/\s+/g, " ");
|
|
7637
|
+
return s.length > 80 ? s.slice(0, 80) + "\u2026" : s;
|
|
7638
|
+
}
|
|
7639
|
+
function cleanReason(raw) {
|
|
7640
|
+
return raw.replace(/\s*[—–-]+\s*(blocked by|requires human approval)[^)]*(\([^)]*\))?\.?\s*$/i, "").trim();
|
|
7641
|
+
}
|
|
7642
|
+
function cleanBlockedBy(raw) {
|
|
7643
|
+
const shieldMatch = raw.match(/shield:([^:]+):/i);
|
|
7644
|
+
if (shieldMatch) return shieldMatch[1];
|
|
7645
|
+
const smartMatch = raw.match(/^Smart Rule:\s*(.+)$/i);
|
|
7646
|
+
if (smartMatch) return smartMatch[1];
|
|
7647
|
+
return raw;
|
|
7648
|
+
}
|
|
7175
7649
|
function buildCardLines(req, localCount = 0) {
|
|
7176
7650
|
if (req.recoveryCommand) {
|
|
7177
7651
|
return buildRecoveryCardLines(req);
|
|
7178
7652
|
}
|
|
7179
|
-
const
|
|
7180
|
-
const argsPreview =
|
|
7181
|
-
const
|
|
7182
|
-
const blockedBy =
|
|
7653
|
+
const argsSummary = extractArgsSummary(req.toolName, req.args);
|
|
7654
|
+
const argsPreview = argsSummary.length > 72 ? argsSummary.slice(0, 72) + "\u2026" : argsSummary;
|
|
7655
|
+
const rawBlockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
7656
|
+
const blockedBy = cleanBlockedBy(rawBlockedBy);
|
|
7657
|
+
const isBlock = req.riskMetadata?.tier != null && req.riskMetadata.tier <= 2;
|
|
7658
|
+
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
7659
|
+
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
7660
|
+
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
7183
7661
|
const lines = [
|
|
7184
7662
|
``,
|
|
7185
7663
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
7186
7664
|
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
7187
|
-
`${CYAN}\u2551${RESET2}
|
|
7665
|
+
`${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
|
|
7188
7666
|
];
|
|
7189
|
-
if (
|
|
7190
|
-
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}
|
|
7667
|
+
if (description) {
|
|
7668
|
+
lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
|
|
7191
7669
|
}
|
|
7192
|
-
if (req.riskMetadata?.ruleName &&
|
|
7670
|
+
if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
|
|
7193
7671
|
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
7194
7672
|
}
|
|
7195
|
-
|
|
7673
|
+
if (argsPreview) {
|
|
7674
|
+
const argLabel = req.toolName.toLowerCase().includes("bash") ? "Command" : "Args ";
|
|
7675
|
+
lines.push(`${CYAN}\u2551${RESET2} ${argLabel}: ${GRAY}${argsPreview}${RESET2}`);
|
|
7676
|
+
}
|
|
7196
7677
|
if (localCount >= 2) {
|
|
7197
7678
|
lines.push(
|
|
7198
7679
|
`${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
|
|
@@ -7232,9 +7713,9 @@ function buildRecoveryCardLines(req) {
|
|
|
7232
7713
|
];
|
|
7233
7714
|
}
|
|
7234
7715
|
function readApproversFromDisk() {
|
|
7235
|
-
const configPath =
|
|
7716
|
+
const configPath = path36.join(os29.homedir(), ".node9", "config.json");
|
|
7236
7717
|
try {
|
|
7237
|
-
const raw = JSON.parse(
|
|
7718
|
+
const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
7238
7719
|
const settings = raw.settings ?? {};
|
|
7239
7720
|
return settings.approvers ?? {};
|
|
7240
7721
|
} catch {
|
|
@@ -7245,20 +7726,20 @@ function approverStatusLine() {
|
|
|
7245
7726
|
const a = readApproversFromDisk();
|
|
7246
7727
|
const fmt = (label, key) => {
|
|
7247
7728
|
const on = a[key] !== false;
|
|
7248
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7729
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk23.green("\u2713") : chalk23.dim("\u2717")}`;
|
|
7249
7730
|
};
|
|
7250
7731
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7251
7732
|
}
|
|
7252
7733
|
function toggleApprover(channel) {
|
|
7253
|
-
const configPath =
|
|
7734
|
+
const configPath = path36.join(os29.homedir(), ".node9", "config.json");
|
|
7254
7735
|
try {
|
|
7255
|
-
const raw = JSON.parse(
|
|
7736
|
+
const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
7256
7737
|
const settings = raw.settings ?? {};
|
|
7257
7738
|
const approvers = settings.approvers ?? {};
|
|
7258
7739
|
approvers[channel] = approvers[channel] === false;
|
|
7259
7740
|
settings.approvers = approvers;
|
|
7260
7741
|
raw.settings = settings;
|
|
7261
|
-
|
|
7742
|
+
fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7262
7743
|
} catch (err2) {
|
|
7263
7744
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7264
7745
|
`);
|
|
@@ -7290,7 +7771,7 @@ async function startTail(options = {}) {
|
|
|
7290
7771
|
req2.end();
|
|
7291
7772
|
});
|
|
7292
7773
|
if (result.ok) {
|
|
7293
|
-
console.log(
|
|
7774
|
+
console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
|
|
7294
7775
|
} else if (result.code === "ECONNREFUSED") {
|
|
7295
7776
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
7296
7777
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7334,7 +7815,7 @@ async function startTail(options = {}) {
|
|
|
7334
7815
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7335
7816
|
if (channel) {
|
|
7336
7817
|
toggleApprover(channel);
|
|
7337
|
-
console.log(
|
|
7818
|
+
console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
|
|
7338
7819
|
}
|
|
7339
7820
|
};
|
|
7340
7821
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7400,7 +7881,7 @@ async function startTail(options = {}) {
|
|
|
7400
7881
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7401
7882
|
)
|
|
7402
7883
|
);
|
|
7403
|
-
const decisionStamp = action === "always-allow" ?
|
|
7884
|
+
const decisionStamp = action === "always-allow" ? chalk23.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk23.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk23.green("\u2713 ALLOWED") : action === "redirect" ? chalk23.yellow("\u21A9 REDIRECT AI") : chalk23.red("\u2717 DENIED");
|
|
7404
7885
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7405
7886
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7406
7887
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7428,8 +7909,8 @@ async function startTail(options = {}) {
|
|
|
7428
7909
|
}
|
|
7429
7910
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7430
7911
|
try {
|
|
7431
|
-
|
|
7432
|
-
|
|
7912
|
+
fs33.appendFileSync(
|
|
7913
|
+
path36.join(os29.homedir(), ".node9", "hook-debug.log"),
|
|
7433
7914
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7434
7915
|
`
|
|
7435
7916
|
);
|
|
@@ -7451,7 +7932,7 @@ async function startTail(options = {}) {
|
|
|
7451
7932
|
);
|
|
7452
7933
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7453
7934
|
if (externalDecision) {
|
|
7454
|
-
const source = externalDecision === "allow" ?
|
|
7935
|
+
const source = externalDecision === "allow" ? chalk23.green("\u2713 ALLOWED") : chalk23.red("\u2717 DENIED");
|
|
7455
7936
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7456
7937
|
}
|
|
7457
7938
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7510,16 +7991,16 @@ async function startTail(options = {}) {
|
|
|
7510
7991
|
}
|
|
7511
7992
|
} catch {
|
|
7512
7993
|
}
|
|
7513
|
-
console.log(
|
|
7514
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7994
|
+
console.log(chalk23.cyan.bold(`
|
|
7995
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk23.dim(`\u2192 ${dashboardUrl}`));
|
|
7515
7996
|
if (canApprove) {
|
|
7516
|
-
console.log(
|
|
7517
|
-
console.log(
|
|
7997
|
+
console.log(chalk23.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7998
|
+
console.log(chalk23.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7518
7999
|
}
|
|
7519
8000
|
if (options.history) {
|
|
7520
|
-
console.log(
|
|
8001
|
+
console.log(chalk23.dim("Showing history + live events.\n"));
|
|
7521
8002
|
} else {
|
|
7522
|
-
console.log(
|
|
8003
|
+
console.log(chalk23.dim("Showing live events only. Use --history to include past.\n"));
|
|
7523
8004
|
}
|
|
7524
8005
|
process.on("SIGINT", () => {
|
|
7525
8006
|
exitIdleMode();
|
|
@@ -7529,13 +8010,13 @@ async function startTail(options = {}) {
|
|
|
7529
8010
|
readline5.clearLine(process.stdout, 0);
|
|
7530
8011
|
readline5.cursorTo(process.stdout, 0);
|
|
7531
8012
|
}
|
|
7532
|
-
console.log(
|
|
8013
|
+
console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7533
8014
|
process.exit(0);
|
|
7534
8015
|
});
|
|
7535
8016
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7536
8017
|
const req = http2.get(sseUrl, (res) => {
|
|
7537
8018
|
if (res.statusCode !== 200) {
|
|
7538
|
-
console.error(
|
|
8019
|
+
console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7539
8020
|
process.exit(1);
|
|
7540
8021
|
}
|
|
7541
8022
|
if (canApprove) enterIdleMode();
|
|
@@ -7566,7 +8047,7 @@ async function startTail(options = {}) {
|
|
|
7566
8047
|
readline5.clearLine(process.stdout, 0);
|
|
7567
8048
|
readline5.cursorTo(process.stdout, 0);
|
|
7568
8049
|
}
|
|
7569
|
-
console.log(
|
|
8050
|
+
console.log(chalk23.red("\n\u274C Daemon disconnected."));
|
|
7570
8051
|
process.exit(1);
|
|
7571
8052
|
});
|
|
7572
8053
|
});
|
|
@@ -7658,9 +8139,9 @@ async function startTail(options = {}) {
|
|
|
7658
8139
|
const hash = data.hash ?? "";
|
|
7659
8140
|
const summary = data.argsSummary ?? data.tool;
|
|
7660
8141
|
const fileCount = data.fileCount ?? 0;
|
|
7661
|
-
const files = fileCount > 0 ?
|
|
8142
|
+
const files = fileCount > 0 ? chalk23.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7662
8143
|
process.stdout.write(
|
|
7663
|
-
`${
|
|
8144
|
+
`${chalk23.dim(time)} ${chalk23.cyan("\u{1F4F8} snapshot")} ${chalk23.dim(hash)} ${summary}${files}
|
|
7664
8145
|
`
|
|
7665
8146
|
);
|
|
7666
8147
|
return;
|
|
@@ -7677,7 +8158,7 @@ async function startTail(options = {}) {
|
|
|
7677
8158
|
}
|
|
7678
8159
|
req.on("error", (err2) => {
|
|
7679
8160
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7680
|
-
console.error(
|
|
8161
|
+
console.error(chalk23.red(`
|
|
7681
8162
|
\u274C ${msg}`));
|
|
7682
8163
|
process.exit(1);
|
|
7683
8164
|
});
|
|
@@ -7689,7 +8170,7 @@ var init_tail = __esm({
|
|
|
7689
8170
|
init_daemon2();
|
|
7690
8171
|
init_daemon();
|
|
7691
8172
|
init_core();
|
|
7692
|
-
PID_FILE =
|
|
8173
|
+
PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
|
|
7693
8174
|
ICONS = {
|
|
7694
8175
|
bash: "\u{1F4BB}",
|
|
7695
8176
|
shell: "\u{1F4BB}",
|
|
@@ -7730,9 +8211,9 @@ __export(hud_exports, {
|
|
|
7730
8211
|
main: () => main,
|
|
7731
8212
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7732
8213
|
});
|
|
7733
|
-
import
|
|
7734
|
-
import
|
|
7735
|
-
import
|
|
8214
|
+
import fs34 from "fs";
|
|
8215
|
+
import path37 from "path";
|
|
8216
|
+
import os30 from "os";
|
|
7736
8217
|
import http3 from "http";
|
|
7737
8218
|
async function readStdin() {
|
|
7738
8219
|
const chunks = [];
|
|
@@ -7808,9 +8289,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7808
8289
|
return ` (${m}m left)`;
|
|
7809
8290
|
}
|
|
7810
8291
|
function safeReadJson(filePath) {
|
|
7811
|
-
if (!
|
|
8292
|
+
if (!fs34.existsSync(filePath)) return null;
|
|
7812
8293
|
try {
|
|
7813
|
-
return JSON.parse(
|
|
8294
|
+
return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
|
|
7814
8295
|
} catch {
|
|
7815
8296
|
return null;
|
|
7816
8297
|
}
|
|
@@ -7831,12 +8312,12 @@ function countHooksInFile(filePath) {
|
|
|
7831
8312
|
return Object.keys(cfg.hooks).length;
|
|
7832
8313
|
}
|
|
7833
8314
|
function countRulesInDir(rulesDir) {
|
|
7834
|
-
if (!
|
|
8315
|
+
if (!fs34.existsSync(rulesDir)) return 0;
|
|
7835
8316
|
let count = 0;
|
|
7836
8317
|
try {
|
|
7837
|
-
for (const entry of
|
|
8318
|
+
for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7838
8319
|
if (entry.isDirectory()) {
|
|
7839
|
-
count += countRulesInDir(
|
|
8320
|
+
count += countRulesInDir(path37.join(rulesDir, entry.name));
|
|
7840
8321
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7841
8322
|
count++;
|
|
7842
8323
|
}
|
|
@@ -7847,46 +8328,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7847
8328
|
}
|
|
7848
8329
|
function isSamePath(a, b) {
|
|
7849
8330
|
try {
|
|
7850
|
-
return
|
|
8331
|
+
return path37.resolve(a) === path37.resolve(b);
|
|
7851
8332
|
} catch {
|
|
7852
8333
|
return false;
|
|
7853
8334
|
}
|
|
7854
8335
|
}
|
|
7855
8336
|
function countConfigs(cwd) {
|
|
7856
|
-
const homeDir2 =
|
|
7857
|
-
const claudeDir =
|
|
8337
|
+
const homeDir2 = os30.homedir();
|
|
8338
|
+
const claudeDir = path37.join(homeDir2, ".claude");
|
|
7858
8339
|
let claudeMdCount = 0;
|
|
7859
8340
|
let rulesCount = 0;
|
|
7860
8341
|
let hooksCount = 0;
|
|
7861
8342
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7862
8343
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7863
|
-
if (
|
|
7864
|
-
rulesCount += countRulesInDir(
|
|
7865
|
-
const userSettings =
|
|
8344
|
+
if (fs34.existsSync(path37.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8345
|
+
rulesCount += countRulesInDir(path37.join(claudeDir, "rules"));
|
|
8346
|
+
const userSettings = path37.join(claudeDir, "settings.json");
|
|
7866
8347
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7867
8348
|
hooksCount += countHooksInFile(userSettings);
|
|
7868
|
-
const userClaudeJson =
|
|
8349
|
+
const userClaudeJson = path37.join(homeDir2, ".claude.json");
|
|
7869
8350
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7870
8351
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7871
8352
|
userMcpServers.delete(name);
|
|
7872
8353
|
}
|
|
7873
8354
|
if (cwd) {
|
|
7874
|
-
if (
|
|
7875
|
-
if (
|
|
7876
|
-
const projectClaudeDir =
|
|
8355
|
+
if (fs34.existsSync(path37.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
8356
|
+
if (fs34.existsSync(path37.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8357
|
+
const projectClaudeDir = path37.join(cwd, ".claude");
|
|
7877
8358
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7878
8359
|
if (!overlapsUserScope) {
|
|
7879
|
-
if (
|
|
7880
|
-
rulesCount += countRulesInDir(
|
|
7881
|
-
const projSettings =
|
|
8360
|
+
if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8361
|
+
rulesCount += countRulesInDir(path37.join(projectClaudeDir, "rules"));
|
|
8362
|
+
const projSettings = path37.join(projectClaudeDir, "settings.json");
|
|
7882
8363
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7883
8364
|
hooksCount += countHooksInFile(projSettings);
|
|
7884
8365
|
}
|
|
7885
|
-
if (
|
|
7886
|
-
const localSettings =
|
|
8366
|
+
if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8367
|
+
const localSettings = path37.join(projectClaudeDir, "settings.local.json");
|
|
7887
8368
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7888
8369
|
hooksCount += countHooksInFile(localSettings);
|
|
7889
|
-
const mcpJsonServers = getMcpServerNames(
|
|
8370
|
+
const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
|
|
7890
8371
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7891
8372
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7892
8373
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7919,12 +8400,12 @@ function readActiveShieldsHud() {
|
|
|
7919
8400
|
return shieldsCache.value;
|
|
7920
8401
|
}
|
|
7921
8402
|
try {
|
|
7922
|
-
const shieldsPath =
|
|
7923
|
-
if (!
|
|
8403
|
+
const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
|
|
8404
|
+
if (!fs34.existsSync(shieldsPath)) {
|
|
7924
8405
|
shieldsCache = { value: [], ts: now };
|
|
7925
8406
|
return [];
|
|
7926
8407
|
}
|
|
7927
|
-
const parsed = JSON.parse(
|
|
8408
|
+
const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
|
|
7928
8409
|
if (!Array.isArray(parsed.active)) {
|
|
7929
8410
|
shieldsCache = { value: [], ts: now };
|
|
7930
8411
|
return [];
|
|
@@ -8026,17 +8507,17 @@ function renderContextLine(stdin) {
|
|
|
8026
8507
|
async function main() {
|
|
8027
8508
|
try {
|
|
8028
8509
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
8029
|
-
if (
|
|
8510
|
+
if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
|
|
8030
8511
|
try {
|
|
8031
|
-
const logPath =
|
|
8512
|
+
const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
|
|
8032
8513
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8033
8514
|
let size = 0;
|
|
8034
8515
|
try {
|
|
8035
|
-
size =
|
|
8516
|
+
size = fs34.statSync(logPath).size;
|
|
8036
8517
|
} catch {
|
|
8037
8518
|
}
|
|
8038
8519
|
if (size < MAX_LOG_SIZE) {
|
|
8039
|
-
|
|
8520
|
+
fs34.appendFileSync(
|
|
8040
8521
|
logPath,
|
|
8041
8522
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
8042
8523
|
);
|
|
@@ -8057,11 +8538,11 @@ async function main() {
|
|
|
8057
8538
|
try {
|
|
8058
8539
|
const cwd = stdin.cwd ?? process.cwd();
|
|
8059
8540
|
for (const configPath of [
|
|
8060
|
-
|
|
8061
|
-
|
|
8541
|
+
path37.join(cwd, "node9.config.json"),
|
|
8542
|
+
path37.join(os30.homedir(), ".node9", "config.json")
|
|
8062
8543
|
]) {
|
|
8063
|
-
if (!
|
|
8064
|
-
const cfg = JSON.parse(
|
|
8544
|
+
if (!fs34.existsSync(configPath)) continue;
|
|
8545
|
+
const cfg = JSON.parse(fs34.readFileSync(configPath, "utf-8"));
|
|
8065
8546
|
const hud = cfg.settings?.hud;
|
|
8066
8547
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
8067
8548
|
}
|
|
@@ -8492,7 +8973,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
8492
8973
|
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
8493
8974
|
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
8494
8975
|
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
8495
|
-
codex: exists(path15.join(homeDir2, ".codex"))
|
|
8976
|
+
codex: exists(path15.join(homeDir2, ".codex")),
|
|
8977
|
+
windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
|
|
8978
|
+
vscode: exists(path15.join(homeDir2, ".vscode"))
|
|
8496
8979
|
};
|
|
8497
8980
|
}
|
|
8498
8981
|
async function setupCursor() {
|
|
@@ -8641,6 +9124,38 @@ async function setupCodex() {
|
|
|
8641
9124
|
printDaemonTip();
|
|
8642
9125
|
}
|
|
8643
9126
|
}
|
|
9127
|
+
function teardownCodex() {
|
|
9128
|
+
const homeDir2 = os11.homedir();
|
|
9129
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
9130
|
+
const config = readToml(configPath);
|
|
9131
|
+
if (!config?.mcp_servers) {
|
|
9132
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
|
|
9133
|
+
return;
|
|
9134
|
+
}
|
|
9135
|
+
let changed = false;
|
|
9136
|
+
if (removeNode9McpServer(config.mcp_servers)) {
|
|
9137
|
+
changed = true;
|
|
9138
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codex/config.toml"));
|
|
9139
|
+
}
|
|
9140
|
+
for (const [name, server] of Object.entries(config.mcp_servers)) {
|
|
9141
|
+
const args = server.args;
|
|
9142
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9143
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9144
|
+
config.mcp_servers[name] = {
|
|
9145
|
+
...server,
|
|
9146
|
+
command: originalCmd,
|
|
9147
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9148
|
+
};
|
|
9149
|
+
changed = true;
|
|
9150
|
+
}
|
|
9151
|
+
}
|
|
9152
|
+
if (changed) {
|
|
9153
|
+
writeToml(configPath, config);
|
|
9154
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
|
|
9155
|
+
} else {
|
|
9156
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
8644
9159
|
function setupHud() {
|
|
8645
9160
|
const homeDir2 = os11.homedir();
|
|
8646
9161
|
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8687,33 +9202,305 @@ function teardownHud() {
|
|
|
8687
9202
|
console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
|
|
8688
9203
|
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
8689
9204
|
}
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
const
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
9205
|
+
async function setupWindsurf() {
|
|
9206
|
+
const homeDir2 = os11.homedir();
|
|
9207
|
+
const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
|
|
9208
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
9209
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
9210
|
+
let anythingChanged = false;
|
|
9211
|
+
if (!hasNode9McpServer(servers)) {
|
|
9212
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
9213
|
+
mcpConfig.mcpServers = servers;
|
|
9214
|
+
writeJson(mcpPath, mcpConfig);
|
|
9215
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9216
|
+
anythingChanged = true;
|
|
9217
|
+
}
|
|
9218
|
+
const serversToWrap = [];
|
|
9219
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
9220
|
+
if (!server.command || server.command === "node9") continue;
|
|
9221
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
9222
|
+
}
|
|
9223
|
+
if (serversToWrap.length > 0) {
|
|
9224
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
9225
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
9226
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9227
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
9228
|
+
}
|
|
9229
|
+
console.log("");
|
|
9230
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
9231
|
+
if (proceed) {
|
|
9232
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9233
|
+
servers[name] = {
|
|
9234
|
+
...servers[name],
|
|
9235
|
+
command: "node9",
|
|
9236
|
+
args: ["mcp", "--upstream", upstream]
|
|
9237
|
+
};
|
|
9238
|
+
}
|
|
9239
|
+
mcpConfig.mcpServers = servers;
|
|
9240
|
+
writeJson(mcpPath, mcpConfig);
|
|
9241
|
+
console.log(chalk.green(`
|
|
9242
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
9243
|
+
anythingChanged = true;
|
|
9244
|
+
} else {
|
|
9245
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
9246
|
+
}
|
|
9247
|
+
console.log("");
|
|
9248
|
+
}
|
|
9249
|
+
console.log(
|
|
9250
|
+
chalk.yellow(
|
|
9251
|
+
" \u26A0\uFE0F Note: Windsurf does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Windsurf."
|
|
9252
|
+
)
|
|
9253
|
+
);
|
|
9254
|
+
console.log("");
|
|
9255
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
9256
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Windsurf."));
|
|
9257
|
+
printDaemonTip();
|
|
9258
|
+
return;
|
|
9259
|
+
}
|
|
9260
|
+
if (anythingChanged) {
|
|
9261
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Windsurf via MCP proxy!"));
|
|
9262
|
+
console.log(chalk.gray(" Restart Windsurf for changes to take effect."));
|
|
9263
|
+
printDaemonTip();
|
|
9264
|
+
}
|
|
9265
|
+
}
|
|
9266
|
+
function teardownWindsurf() {
|
|
9267
|
+
const homeDir2 = os11.homedir();
|
|
9268
|
+
const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
|
|
9269
|
+
const mcpConfig = readJson(mcpPath);
|
|
9270
|
+
if (!mcpConfig?.mcpServers) {
|
|
9271
|
+
console.log(
|
|
9272
|
+
chalk.blue(" \u2139\uFE0F ~/.codeium/windsurf/mcp_config.json not found \u2014 nothing to remove")
|
|
9273
|
+
);
|
|
9274
|
+
return;
|
|
9275
|
+
}
|
|
9276
|
+
let changed = false;
|
|
9277
|
+
if (removeNode9McpServer(mcpConfig.mcpServers)) {
|
|
9278
|
+
changed = true;
|
|
9279
|
+
console.log(
|
|
9280
|
+
chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codeium/windsurf/mcp_config.json")
|
|
9281
|
+
);
|
|
9282
|
+
}
|
|
9283
|
+
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
9284
|
+
const args = server.args;
|
|
9285
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9286
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9287
|
+
mcpConfig.mcpServers[name] = {
|
|
9288
|
+
...server,
|
|
9289
|
+
command: originalCmd,
|
|
9290
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9291
|
+
};
|
|
9292
|
+
changed = true;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
if (changed) {
|
|
9296
|
+
writeJson(mcpPath, mcpConfig);
|
|
9297
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codeium/windsurf/mcp_config.json"));
|
|
9298
|
+
} else {
|
|
9299
|
+
console.log(
|
|
9300
|
+
chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codeium/windsurf/mcp_config.json")
|
|
9301
|
+
);
|
|
9302
|
+
}
|
|
9303
|
+
}
|
|
9304
|
+
function hasNode9McpServerVSCode(servers) {
|
|
9305
|
+
const entry = servers["node9"];
|
|
9306
|
+
return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
|
|
9307
|
+
}
|
|
9308
|
+
async function setupVSCode() {
|
|
9309
|
+
const homeDir2 = os11.homedir();
|
|
9310
|
+
const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
|
|
9311
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
9312
|
+
const servers = mcpConfig.servers ?? {};
|
|
9313
|
+
let anythingChanged = false;
|
|
9314
|
+
if (!hasNode9McpServerVSCode(servers)) {
|
|
9315
|
+
servers["node9"] = { type: "stdio", command: "node9", args: ["mcp-server"] };
|
|
9316
|
+
mcpConfig.servers = servers;
|
|
9317
|
+
writeJson(mcpPath, mcpConfig);
|
|
9318
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9319
|
+
anythingChanged = true;
|
|
9320
|
+
}
|
|
9321
|
+
const serversToWrap = [];
|
|
9322
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
9323
|
+
if (!server.command || server.command === "node9") continue;
|
|
9324
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
9325
|
+
}
|
|
9326
|
+
if (serversToWrap.length > 0) {
|
|
9327
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
9328
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
9329
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9330
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
9331
|
+
}
|
|
9332
|
+
console.log("");
|
|
9333
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
9334
|
+
if (proceed) {
|
|
9335
|
+
for (const { name, upstream } of serversToWrap) {
|
|
9336
|
+
servers[name] = {
|
|
9337
|
+
...servers[name],
|
|
9338
|
+
type: "stdio",
|
|
9339
|
+
command: "node9",
|
|
9340
|
+
args: ["mcp", "--upstream", upstream]
|
|
9341
|
+
};
|
|
9342
|
+
}
|
|
9343
|
+
mcpConfig.servers = servers;
|
|
9344
|
+
writeJson(mcpPath, mcpConfig);
|
|
9345
|
+
console.log(chalk.green(`
|
|
9346
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
9347
|
+
anythingChanged = true;
|
|
9348
|
+
} else {
|
|
9349
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
9350
|
+
}
|
|
9351
|
+
console.log("");
|
|
9352
|
+
}
|
|
9353
|
+
console.log(
|
|
9354
|
+
chalk.yellow(
|
|
9355
|
+
" \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."
|
|
9356
|
+
)
|
|
9357
|
+
);
|
|
9358
|
+
console.log("");
|
|
9359
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
9360
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for VSCode."));
|
|
9361
|
+
printDaemonTip();
|
|
9362
|
+
return;
|
|
9363
|
+
}
|
|
9364
|
+
if (anythingChanged) {
|
|
9365
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting VSCode via MCP proxy!"));
|
|
9366
|
+
console.log(chalk.gray(" Restart VSCode for changes to take effect."));
|
|
9367
|
+
printDaemonTip();
|
|
9368
|
+
}
|
|
9369
|
+
}
|
|
9370
|
+
function teardownVSCode() {
|
|
9371
|
+
const homeDir2 = os11.homedir();
|
|
9372
|
+
const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
|
|
9373
|
+
const mcpConfig = readJson(mcpPath);
|
|
9374
|
+
if (!mcpConfig?.servers) {
|
|
9375
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.vscode/mcp.json not found \u2014 nothing to remove"));
|
|
9376
|
+
return;
|
|
9377
|
+
}
|
|
9378
|
+
let changed = false;
|
|
9379
|
+
if (hasNode9McpServerVSCode(mcpConfig.servers)) {
|
|
9380
|
+
delete mcpConfig.servers["node9"];
|
|
9381
|
+
changed = true;
|
|
9382
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.vscode/mcp.json"));
|
|
9383
|
+
}
|
|
9384
|
+
for (const [name, server] of Object.entries(mcpConfig.servers)) {
|
|
9385
|
+
const args = server.args;
|
|
9386
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
9387
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
9388
|
+
mcpConfig.servers[name] = {
|
|
9389
|
+
...server,
|
|
9390
|
+
type: "stdio",
|
|
9391
|
+
command: originalCmd,
|
|
9392
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
9393
|
+
};
|
|
9394
|
+
changed = true;
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
if (changed) {
|
|
9398
|
+
writeJson(mcpPath, mcpConfig);
|
|
9399
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.vscode/mcp.json"));
|
|
9400
|
+
} else {
|
|
9401
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
|
|
9402
|
+
}
|
|
9403
|
+
}
|
|
9404
|
+
function getAgentsStatus(homeDir2 = os11.homedir()) {
|
|
9405
|
+
const detected = detectAgents(homeDir2);
|
|
9406
|
+
const claudeWired = (() => {
|
|
9407
|
+
const settings = readJson(path15.join(homeDir2, ".claude", "settings.json"));
|
|
9408
|
+
return !!settings?.hooks?.PreToolUse?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
|
|
9409
|
+
})();
|
|
9410
|
+
const geminiWired = (() => {
|
|
9411
|
+
const settings = readJson(path15.join(homeDir2, ".gemini", "settings.json"));
|
|
9412
|
+
return !!settings?.hooks?.BeforeTool?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
|
|
9413
|
+
})();
|
|
9414
|
+
const cursorWired = (() => {
|
|
9415
|
+
const cfg = readJson(path15.join(homeDir2, ".cursor", "mcp.json"));
|
|
9416
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
9417
|
+
})();
|
|
9418
|
+
const codexWired = (() => {
|
|
9419
|
+
const cfg = readToml(path15.join(homeDir2, ".codex", "config.toml"));
|
|
9420
|
+
return !!(cfg?.mcp_servers && hasNode9McpServer(cfg.mcp_servers));
|
|
9421
|
+
})();
|
|
9422
|
+
const windsurfWired = (() => {
|
|
9423
|
+
const cfg = readJson(
|
|
9424
|
+
path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json")
|
|
9425
|
+
);
|
|
9426
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
9427
|
+
})();
|
|
9428
|
+
const vscodeWired = (() => {
|
|
9429
|
+
const cfg = readJson(path15.join(homeDir2, ".vscode", "mcp.json"));
|
|
9430
|
+
return !!(cfg?.servers && hasNode9McpServerVSCode(cfg.servers));
|
|
9431
|
+
})();
|
|
9432
|
+
return [
|
|
9433
|
+
{
|
|
9434
|
+
name: "claude",
|
|
9435
|
+
label: "Claude Code",
|
|
9436
|
+
installed: detected.claude,
|
|
9437
|
+
wired: claudeWired,
|
|
9438
|
+
mode: detected.claude ? "hooks" : null
|
|
9439
|
+
},
|
|
9440
|
+
{
|
|
9441
|
+
name: "gemini",
|
|
9442
|
+
label: "Gemini CLI",
|
|
9443
|
+
installed: detected.gemini,
|
|
9444
|
+
wired: geminiWired,
|
|
9445
|
+
mode: detected.gemini ? "hooks" : null
|
|
9446
|
+
},
|
|
9447
|
+
{
|
|
9448
|
+
name: "cursor",
|
|
9449
|
+
label: "Cursor",
|
|
9450
|
+
installed: detected.cursor,
|
|
9451
|
+
wired: cursorWired,
|
|
9452
|
+
mode: detected.cursor ? "mcp" : null
|
|
9453
|
+
},
|
|
9454
|
+
{
|
|
9455
|
+
name: "windsurf",
|
|
9456
|
+
label: "Windsurf",
|
|
9457
|
+
installed: detected.windsurf,
|
|
9458
|
+
wired: windsurfWired,
|
|
9459
|
+
mode: detected.windsurf ? "mcp" : null
|
|
9460
|
+
},
|
|
9461
|
+
{
|
|
9462
|
+
name: "vscode",
|
|
9463
|
+
label: "VSCode",
|
|
9464
|
+
installed: detected.vscode,
|
|
9465
|
+
wired: vscodeWired,
|
|
9466
|
+
mode: detected.vscode ? "mcp" : null
|
|
9467
|
+
},
|
|
9468
|
+
{
|
|
9469
|
+
name: "codex",
|
|
9470
|
+
label: "Codex",
|
|
9471
|
+
installed: detected.codex,
|
|
9472
|
+
wired: codexWired,
|
|
9473
|
+
mode: detected.codex ? "mcp" : null
|
|
9474
|
+
}
|
|
9475
|
+
];
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
// src/cli.ts
|
|
9479
|
+
init_daemon2();
|
|
9480
|
+
import chalk24 from "chalk";
|
|
9481
|
+
import fs35 from "fs";
|
|
9482
|
+
import path38 from "path";
|
|
9483
|
+
import os31 from "os";
|
|
9484
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
9485
|
+
|
|
9486
|
+
// src/utils/duration.ts
|
|
9487
|
+
function parseDuration(str) {
|
|
9488
|
+
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
9489
|
+
if (!m) return null;
|
|
9490
|
+
const n = parseFloat(m[1]);
|
|
9491
|
+
switch ((m[2] ?? "m").toLowerCase()) {
|
|
9492
|
+
case "s":
|
|
9493
|
+
return Math.round(n * 1e3);
|
|
9494
|
+
case "m":
|
|
9495
|
+
return Math.round(n * 6e4);
|
|
9496
|
+
case "h":
|
|
9497
|
+
return Math.round(n * 36e5);
|
|
9498
|
+
case "d":
|
|
9499
|
+
return Math.round(n * 864e5);
|
|
9500
|
+
default:
|
|
9501
|
+
return null;
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
8717
9504
|
|
|
8718
9505
|
// src/proxy/index.ts
|
|
8719
9506
|
init_orchestrator();
|
|
@@ -8922,19 +9709,19 @@ init_daemon();
|
|
|
8922
9709
|
init_config();
|
|
8923
9710
|
init_policy();
|
|
8924
9711
|
import chalk5 from "chalk";
|
|
8925
|
-
import
|
|
9712
|
+
import fs22 from "fs";
|
|
8926
9713
|
import { spawn as spawn6 } from "child_process";
|
|
8927
|
-
import
|
|
8928
|
-
import
|
|
9714
|
+
import path24 from "path";
|
|
9715
|
+
import os18 from "os";
|
|
8929
9716
|
|
|
8930
9717
|
// src/undo.ts
|
|
8931
|
-
import { spawnSync as
|
|
9718
|
+
import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
|
|
8932
9719
|
import crypto3 from "crypto";
|
|
8933
|
-
import
|
|
9720
|
+
import fs21 from "fs";
|
|
8934
9721
|
import net3 from "net";
|
|
8935
|
-
import
|
|
8936
|
-
import
|
|
8937
|
-
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
9722
|
+
import path23 from "path";
|
|
9723
|
+
import os17 from "os";
|
|
9724
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path23.join(os17.tmpdir(), "node9-activity.sock");
|
|
8938
9725
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8939
9726
|
try {
|
|
8940
9727
|
const payload = JSON.stringify({
|
|
@@ -8954,22 +9741,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8954
9741
|
} catch {
|
|
8955
9742
|
}
|
|
8956
9743
|
}
|
|
8957
|
-
var SNAPSHOT_STACK_PATH =
|
|
8958
|
-
var UNDO_LATEST_PATH =
|
|
9744
|
+
var SNAPSHOT_STACK_PATH = path23.join(os17.homedir(), ".node9", "snapshots.json");
|
|
9745
|
+
var UNDO_LATEST_PATH = path23.join(os17.homedir(), ".node9", "undo_latest.txt");
|
|
8959
9746
|
var MAX_SNAPSHOTS = 10;
|
|
8960
9747
|
var GIT_TIMEOUT = 15e3;
|
|
8961
9748
|
function readStack() {
|
|
8962
9749
|
try {
|
|
8963
|
-
if (
|
|
8964
|
-
return JSON.parse(
|
|
9750
|
+
if (fs21.existsSync(SNAPSHOT_STACK_PATH))
|
|
9751
|
+
return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8965
9752
|
} catch {
|
|
8966
9753
|
}
|
|
8967
9754
|
return [];
|
|
8968
9755
|
}
|
|
8969
9756
|
function writeStack(stack) {
|
|
8970
|
-
const dir =
|
|
8971
|
-
if (!
|
|
8972
|
-
|
|
9757
|
+
const dir = path23.dirname(SNAPSHOT_STACK_PATH);
|
|
9758
|
+
if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
|
|
9759
|
+
fs21.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
8973
9760
|
}
|
|
8974
9761
|
function extractFilePath(args) {
|
|
8975
9762
|
if (!args || typeof args !== "object") return null;
|
|
@@ -8989,12 +9776,12 @@ function buildArgsSummary(tool, args) {
|
|
|
8989
9776
|
return "";
|
|
8990
9777
|
}
|
|
8991
9778
|
function findProjectRoot(filePath) {
|
|
8992
|
-
let dir =
|
|
9779
|
+
let dir = path23.dirname(filePath);
|
|
8993
9780
|
while (true) {
|
|
8994
|
-
if (
|
|
9781
|
+
if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
|
|
8995
9782
|
return dir;
|
|
8996
9783
|
}
|
|
8997
|
-
const parent =
|
|
9784
|
+
const parent = path23.dirname(dir);
|
|
8998
9785
|
if (parent === dir) return process.cwd();
|
|
8999
9786
|
dir = parent;
|
|
9000
9787
|
}
|
|
@@ -9002,7 +9789,7 @@ function findProjectRoot(filePath) {
|
|
|
9002
9789
|
function normalizeCwdForHash(cwd) {
|
|
9003
9790
|
let normalized;
|
|
9004
9791
|
try {
|
|
9005
|
-
normalized =
|
|
9792
|
+
normalized = fs21.realpathSync(cwd);
|
|
9006
9793
|
} catch {
|
|
9007
9794
|
normalized = cwd;
|
|
9008
9795
|
}
|
|
@@ -9012,16 +9799,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
9012
9799
|
}
|
|
9013
9800
|
function getShadowRepoDir(cwd) {
|
|
9014
9801
|
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
9015
|
-
return
|
|
9802
|
+
return path23.join(os17.homedir(), ".node9", "snapshots", hash);
|
|
9016
9803
|
}
|
|
9017
9804
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
9018
9805
|
try {
|
|
9019
9806
|
const cutoff = Date.now() - 6e4;
|
|
9020
|
-
for (const f of
|
|
9807
|
+
for (const f of fs21.readdirSync(shadowDir)) {
|
|
9021
9808
|
if (f.startsWith("index_")) {
|
|
9022
|
-
const fp =
|
|
9809
|
+
const fp = path23.join(shadowDir, f);
|
|
9023
9810
|
try {
|
|
9024
|
-
if (
|
|
9811
|
+
if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
|
|
9025
9812
|
} catch {
|
|
9026
9813
|
}
|
|
9027
9814
|
}
|
|
@@ -9033,7 +9820,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
9033
9820
|
const hardcoded = [".git", ".node9"];
|
|
9034
9821
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
9035
9822
|
try {
|
|
9036
|
-
|
|
9823
|
+
fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
9037
9824
|
} catch {
|
|
9038
9825
|
}
|
|
9039
9826
|
}
|
|
@@ -9041,54 +9828,54 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9041
9828
|
cleanOrphanedIndexFiles(shadowDir);
|
|
9042
9829
|
const normalizedCwd = normalizeCwdForHash(cwd);
|
|
9043
9830
|
const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
9044
|
-
const check =
|
|
9831
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9045
9832
|
env: shadowEnvBase,
|
|
9046
9833
|
timeout: 3e3
|
|
9047
9834
|
});
|
|
9048
9835
|
if (check.status === 0) {
|
|
9049
|
-
const ptPath =
|
|
9836
|
+
const ptPath = path23.join(shadowDir, "project-path.txt");
|
|
9050
9837
|
try {
|
|
9051
|
-
const stored =
|
|
9838
|
+
const stored = fs21.readFileSync(ptPath, "utf8").trim();
|
|
9052
9839
|
if (stored === normalizedCwd) return true;
|
|
9053
9840
|
if (process.env.NODE9_DEBUG === "1")
|
|
9054
9841
|
console.error(
|
|
9055
9842
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
9056
9843
|
);
|
|
9057
|
-
|
|
9844
|
+
fs21.rmSync(shadowDir, { recursive: true, force: true });
|
|
9058
9845
|
} catch {
|
|
9059
9846
|
try {
|
|
9060
|
-
|
|
9847
|
+
fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
9061
9848
|
} catch {
|
|
9062
9849
|
}
|
|
9063
9850
|
return true;
|
|
9064
9851
|
}
|
|
9065
9852
|
}
|
|
9066
9853
|
try {
|
|
9067
|
-
|
|
9854
|
+
fs21.mkdirSync(shadowDir, { recursive: true });
|
|
9068
9855
|
} catch {
|
|
9069
9856
|
}
|
|
9070
|
-
const init =
|
|
9857
|
+
const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
9071
9858
|
if (init.status !== 0 || init.error) {
|
|
9072
9859
|
const reason = init.error ? init.error.message : init.stderr?.toString();
|
|
9073
9860
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
9074
9861
|
return false;
|
|
9075
9862
|
}
|
|
9076
|
-
const configFile =
|
|
9077
|
-
|
|
9863
|
+
const configFile = path23.join(shadowDir, "config");
|
|
9864
|
+
spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
9078
9865
|
timeout: 3e3
|
|
9079
9866
|
});
|
|
9080
|
-
|
|
9867
|
+
spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
|
|
9081
9868
|
timeout: 3e3
|
|
9082
9869
|
});
|
|
9083
9870
|
try {
|
|
9084
|
-
|
|
9871
|
+
fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
9085
9872
|
} catch {
|
|
9086
9873
|
}
|
|
9087
9874
|
return true;
|
|
9088
9875
|
}
|
|
9089
9876
|
function buildGitEnv(cwd) {
|
|
9090
9877
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9091
|
-
const check =
|
|
9878
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9092
9879
|
env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
|
|
9093
9880
|
timeout: 2e3
|
|
9094
9881
|
});
|
|
@@ -9101,23 +9888,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9101
9888
|
let indexFile = null;
|
|
9102
9889
|
try {
|
|
9103
9890
|
const rawFilePath = extractFilePath(args);
|
|
9104
|
-
const absFilePath = rawFilePath &&
|
|
9891
|
+
const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
9105
9892
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
9106
9893
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9107
9894
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
9108
9895
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
9109
|
-
indexFile =
|
|
9896
|
+
indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
9110
9897
|
const shadowEnv = {
|
|
9111
9898
|
...process.env,
|
|
9112
9899
|
GIT_DIR: shadowDir,
|
|
9113
9900
|
GIT_WORK_TREE: cwd,
|
|
9114
9901
|
GIT_INDEX_FILE: indexFile
|
|
9115
9902
|
};
|
|
9116
|
-
|
|
9117
|
-
const treeRes =
|
|
9903
|
+
spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9904
|
+
const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9118
9905
|
const treeHash = treeRes.stdout?.toString().trim();
|
|
9119
9906
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
9120
|
-
const commitRes =
|
|
9907
|
+
const commitRes = spawnSync5(
|
|
9121
9908
|
"git",
|
|
9122
9909
|
["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
|
|
9123
9910
|
{ env: shadowEnv, timeout: GIT_TIMEOUT }
|
|
@@ -9129,7 +9916,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9129
9916
|
let capturedFiles = [];
|
|
9130
9917
|
let capturedDiff = null;
|
|
9131
9918
|
if (prevEntry) {
|
|
9132
|
-
const filesRes =
|
|
9919
|
+
const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
|
|
9133
9920
|
env: shadowEnv,
|
|
9134
9921
|
timeout: GIT_TIMEOUT
|
|
9135
9922
|
});
|
|
@@ -9139,7 +9926,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9139
9926
|
if (capturedFiles.length === 0) {
|
|
9140
9927
|
return prevEntry.hash;
|
|
9141
9928
|
}
|
|
9142
|
-
const diffRes =
|
|
9929
|
+
const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
|
|
9143
9930
|
env: shadowEnv,
|
|
9144
9931
|
timeout: GIT_TIMEOUT
|
|
9145
9932
|
});
|
|
@@ -9147,7 +9934,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9147
9934
|
capturedDiff = diffRes.stdout?.toString() || null;
|
|
9148
9935
|
}
|
|
9149
9936
|
} else {
|
|
9150
|
-
const filesRes =
|
|
9937
|
+
const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
|
|
9151
9938
|
env: shadowEnv,
|
|
9152
9939
|
timeout: GIT_TIMEOUT
|
|
9153
9940
|
});
|
|
@@ -9178,7 +9965,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9178
9965
|
writeStack(stack);
|
|
9179
9966
|
const entry = stack[stack.length - 1];
|
|
9180
9967
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
9181
|
-
|
|
9968
|
+
fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
9182
9969
|
if (shouldGc) {
|
|
9183
9970
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
9184
9971
|
}
|
|
@@ -9189,7 +9976,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9189
9976
|
} finally {
|
|
9190
9977
|
if (indexFile) {
|
|
9191
9978
|
try {
|
|
9192
|
-
|
|
9979
|
+
fs21.unlinkSync(indexFile);
|
|
9193
9980
|
} catch {
|
|
9194
9981
|
}
|
|
9195
9982
|
}
|
|
@@ -9201,14 +9988,14 @@ function getSnapshotHistory() {
|
|
|
9201
9988
|
function computeUndoDiff(hash, cwd) {
|
|
9202
9989
|
try {
|
|
9203
9990
|
const env = buildGitEnv(cwd);
|
|
9204
|
-
const statRes =
|
|
9991
|
+
const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
|
|
9205
9992
|
cwd,
|
|
9206
9993
|
env,
|
|
9207
9994
|
timeout: GIT_TIMEOUT
|
|
9208
9995
|
});
|
|
9209
9996
|
const stat = statRes.stdout?.toString().trim();
|
|
9210
9997
|
if (!stat || statRes.status !== 0) return null;
|
|
9211
|
-
const diffRes =
|
|
9998
|
+
const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
|
|
9212
9999
|
cwd,
|
|
9213
10000
|
env,
|
|
9214
10001
|
timeout: GIT_TIMEOUT
|
|
@@ -9227,7 +10014,7 @@ function applyUndo(hash, cwd) {
|
|
|
9227
10014
|
try {
|
|
9228
10015
|
const dir = cwd ?? process.cwd();
|
|
9229
10016
|
const env = buildGitEnv(dir);
|
|
9230
|
-
const restore =
|
|
10017
|
+
const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
9231
10018
|
cwd: dir,
|
|
9232
10019
|
env,
|
|
9233
10020
|
timeout: GIT_TIMEOUT
|
|
@@ -9239,7 +10026,7 @@ function applyUndo(hash, cwd) {
|
|
|
9239
10026
|
}
|
|
9240
10027
|
return false;
|
|
9241
10028
|
}
|
|
9242
|
-
const lsTree =
|
|
10029
|
+
const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
9243
10030
|
cwd: dir,
|
|
9244
10031
|
env,
|
|
9245
10032
|
timeout: GIT_TIMEOUT
|
|
@@ -9258,16 +10045,16 @@ function applyUndo(hash, cwd) {
|
|
|
9258
10045
|
`);
|
|
9259
10046
|
return false;
|
|
9260
10047
|
}
|
|
9261
|
-
const tracked =
|
|
9262
|
-
const untracked =
|
|
10048
|
+
const tracked = spawnSync5("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
10049
|
+
const untracked = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
9263
10050
|
cwd: dir,
|
|
9264
10051
|
env,
|
|
9265
10052
|
timeout: GIT_TIMEOUT
|
|
9266
10053
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
9267
10054
|
for (const file of [...tracked, ...untracked]) {
|
|
9268
|
-
const fullPath =
|
|
9269
|
-
if (!snapshotFiles.has(file) &&
|
|
9270
|
-
|
|
10055
|
+
const fullPath = path23.join(dir, file);
|
|
10056
|
+
if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
|
|
10057
|
+
fs21.unlinkSync(fullPath);
|
|
9271
10058
|
}
|
|
9272
10059
|
}
|
|
9273
10060
|
return true;
|
|
@@ -9291,9 +10078,9 @@ function registerCheckCommand(program2) {
|
|
|
9291
10078
|
} catch (err2) {
|
|
9292
10079
|
const tempConfig = getConfig();
|
|
9293
10080
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
9294
|
-
const logPath =
|
|
10081
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
9295
10082
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9296
|
-
|
|
10083
|
+
fs22.appendFileSync(
|
|
9297
10084
|
logPath,
|
|
9298
10085
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
9299
10086
|
RAW: ${raw}
|
|
@@ -9306,13 +10093,13 @@ RAW: ${raw}
|
|
|
9306
10093
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
9307
10094
|
try {
|
|
9308
10095
|
const scriptPath = process.argv[1];
|
|
9309
|
-
if (typeof scriptPath !== "string" || !
|
|
10096
|
+
if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
|
|
9310
10097
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
9311
|
-
const resolvedScript =
|
|
9312
|
-
const
|
|
9313
|
-
if (resolvedScript !==
|
|
10098
|
+
const resolvedScript = fs22.realpathSync(scriptPath);
|
|
10099
|
+
const packageDist = fs22.realpathSync(path24.resolve(__dirname, "../.."));
|
|
10100
|
+
if (!resolvedScript.startsWith(packageDist + path24.sep) && resolvedScript !== packageDist)
|
|
9314
10101
|
throw new Error(
|
|
9315
|
-
|
|
10102
|
+
`node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
|
|
9316
10103
|
);
|
|
9317
10104
|
const safeEnv = { ...process.env };
|
|
9318
10105
|
for (const key of [
|
|
@@ -9331,14 +10118,24 @@ RAW: ${raw}
|
|
|
9331
10118
|
env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
9332
10119
|
});
|
|
9333
10120
|
d.unref();
|
|
9334
|
-
} catch {
|
|
10121
|
+
} catch (spawnErr) {
|
|
10122
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
10123
|
+
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
10124
|
+
try {
|
|
10125
|
+
fs22.appendFileSync(
|
|
10126
|
+
logPath,
|
|
10127
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
|
|
10128
|
+
`
|
|
10129
|
+
);
|
|
10130
|
+
} catch {
|
|
10131
|
+
}
|
|
9335
10132
|
}
|
|
9336
10133
|
}
|
|
9337
10134
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9338
|
-
const logPath =
|
|
9339
|
-
if (!
|
|
9340
|
-
|
|
9341
|
-
|
|
10135
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
10136
|
+
if (!fs22.existsSync(path24.dirname(logPath)))
|
|
10137
|
+
fs22.mkdirSync(path24.dirname(logPath), { recursive: true });
|
|
10138
|
+
fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
9342
10139
|
`);
|
|
9343
10140
|
}
|
|
9344
10141
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9351,8 +10148,8 @@ RAW: ${raw}
|
|
|
9351
10148
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9352
10149
|
let ttyFd = null;
|
|
9353
10150
|
try {
|
|
9354
|
-
ttyFd =
|
|
9355
|
-
const writeTty = (line) =>
|
|
10151
|
+
ttyFd = fs22.openSync("/dev/tty", "w");
|
|
10152
|
+
const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
|
|
9356
10153
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9357
10154
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9358
10155
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9371,7 +10168,7 @@ RAW: ${raw}
|
|
|
9371
10168
|
} finally {
|
|
9372
10169
|
if (ttyFd !== null)
|
|
9373
10170
|
try {
|
|
9374
|
-
|
|
10171
|
+
fs22.closeSync(ttyFd);
|
|
9375
10172
|
} catch {
|
|
9376
10173
|
}
|
|
9377
10174
|
}
|
|
@@ -9403,7 +10200,7 @@ RAW: ${raw}
|
|
|
9403
10200
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9404
10201
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9405
10202
|
}
|
|
9406
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
10203
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path24.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9407
10204
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9408
10205
|
cwd: safeCwdForAuth
|
|
9409
10206
|
});
|
|
@@ -9415,12 +10212,12 @@ RAW: ${raw}
|
|
|
9415
10212
|
}
|
|
9416
10213
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9417
10214
|
try {
|
|
9418
|
-
const tty =
|
|
9419
|
-
|
|
10215
|
+
const tty = fs22.openSync("/dev/tty", "w");
|
|
10216
|
+
fs22.writeSync(
|
|
9420
10217
|
tty,
|
|
9421
10218
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9422
10219
|
);
|
|
9423
|
-
|
|
10220
|
+
fs22.closeSync(tty);
|
|
9424
10221
|
} catch {
|
|
9425
10222
|
}
|
|
9426
10223
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9447,9 +10244,9 @@ RAW: ${raw}
|
|
|
9447
10244
|
});
|
|
9448
10245
|
} catch (err2) {
|
|
9449
10246
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9450
|
-
const logPath =
|
|
10247
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
9451
10248
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9452
|
-
|
|
10249
|
+
fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9453
10250
|
`);
|
|
9454
10251
|
}
|
|
9455
10252
|
process.exit(0);
|
|
@@ -9486,9 +10283,9 @@ RAW: ${raw}
|
|
|
9486
10283
|
init_audit();
|
|
9487
10284
|
init_config();
|
|
9488
10285
|
init_policy();
|
|
9489
|
-
import
|
|
9490
|
-
import
|
|
9491
|
-
import
|
|
10286
|
+
import fs23 from "fs";
|
|
10287
|
+
import path25 from "path";
|
|
10288
|
+
import os19 from "os";
|
|
9492
10289
|
init_daemon();
|
|
9493
10290
|
|
|
9494
10291
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9561,10 +10358,10 @@ function registerLogCommand(program2) {
|
|
|
9561
10358
|
decision: "allowed",
|
|
9562
10359
|
source: "post-hook"
|
|
9563
10360
|
};
|
|
9564
|
-
const logPath =
|
|
9565
|
-
if (!
|
|
9566
|
-
|
|
9567
|
-
|
|
10361
|
+
const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
|
|
10362
|
+
if (!fs23.existsSync(path25.dirname(logPath)))
|
|
10363
|
+
fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
|
|
10364
|
+
fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
9568
10365
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9569
10366
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9570
10367
|
if (command) {
|
|
@@ -9597,7 +10394,7 @@ function registerLogCommand(program2) {
|
|
|
9597
10394
|
}
|
|
9598
10395
|
}
|
|
9599
10396
|
}
|
|
9600
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
10397
|
+
const safeCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9601
10398
|
const config = getConfig(safeCwd);
|
|
9602
10399
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9603
10400
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9606,9 +10403,9 @@ function registerLogCommand(program2) {
|
|
|
9606
10403
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9607
10404
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9608
10405
|
`);
|
|
9609
|
-
const debugPath =
|
|
10406
|
+
const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
9610
10407
|
try {
|
|
9611
|
-
|
|
10408
|
+
fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9612
10409
|
`);
|
|
9613
10410
|
} catch {
|
|
9614
10411
|
}
|
|
@@ -9638,10 +10435,10 @@ init_config();
|
|
|
9638
10435
|
import chalk6 from "chalk";
|
|
9639
10436
|
|
|
9640
10437
|
// src/utils/https-fetch.ts
|
|
9641
|
-
import
|
|
10438
|
+
import https2 from "https";
|
|
9642
10439
|
function httpsFetch(url) {
|
|
9643
10440
|
return new Promise((resolve, reject) => {
|
|
9644
|
-
|
|
10441
|
+
https2.get(url, (res) => {
|
|
9645
10442
|
if (res.statusCode !== 200) {
|
|
9646
10443
|
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9647
10444
|
res.resume();
|
|
@@ -10009,13 +10806,13 @@ function registerConfigShowCommand(program2) {
|
|
|
10009
10806
|
// src/cli/commands/doctor.ts
|
|
10010
10807
|
init_daemon();
|
|
10011
10808
|
import chalk7 from "chalk";
|
|
10012
|
-
import
|
|
10013
|
-
import
|
|
10014
|
-
import
|
|
10809
|
+
import fs24 from "fs";
|
|
10810
|
+
import path26 from "path";
|
|
10811
|
+
import os20 from "os";
|
|
10015
10812
|
import { execSync as execSync2 } from "child_process";
|
|
10016
10813
|
function registerDoctorCommand(program2, version2) {
|
|
10017
10814
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
10018
|
-
const homeDir2 =
|
|
10815
|
+
const homeDir2 = os20.homedir();
|
|
10019
10816
|
let failures = 0;
|
|
10020
10817
|
function pass(msg) {
|
|
10021
10818
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -10064,10 +10861,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10064
10861
|
);
|
|
10065
10862
|
}
|
|
10066
10863
|
section("Configuration");
|
|
10067
|
-
const globalConfigPath =
|
|
10068
|
-
if (
|
|
10864
|
+
const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
|
|
10865
|
+
if (fs24.existsSync(globalConfigPath)) {
|
|
10069
10866
|
try {
|
|
10070
|
-
JSON.parse(
|
|
10867
|
+
JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
|
|
10071
10868
|
pass("~/.node9/config.json found and valid");
|
|
10072
10869
|
} catch {
|
|
10073
10870
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -10075,10 +10872,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10075
10872
|
} else {
|
|
10076
10873
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
10077
10874
|
}
|
|
10078
|
-
const projectConfigPath =
|
|
10079
|
-
if (
|
|
10875
|
+
const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
|
|
10876
|
+
if (fs24.existsSync(projectConfigPath)) {
|
|
10080
10877
|
try {
|
|
10081
|
-
JSON.parse(
|
|
10878
|
+
JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
|
|
10082
10879
|
pass("node9.config.json found and valid (project)");
|
|
10083
10880
|
} catch {
|
|
10084
10881
|
fail(
|
|
@@ -10087,8 +10884,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10087
10884
|
);
|
|
10088
10885
|
}
|
|
10089
10886
|
}
|
|
10090
|
-
const credsPath =
|
|
10091
|
-
if (
|
|
10887
|
+
const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
|
|
10888
|
+
if (fs24.existsSync(credsPath)) {
|
|
10092
10889
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
10093
10890
|
} else {
|
|
10094
10891
|
warn(
|
|
@@ -10097,10 +10894,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10097
10894
|
);
|
|
10098
10895
|
}
|
|
10099
10896
|
section("Agent Hooks");
|
|
10100
|
-
const claudeSettingsPath =
|
|
10101
|
-
if (
|
|
10897
|
+
const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
|
|
10898
|
+
if (fs24.existsSync(claudeSettingsPath)) {
|
|
10102
10899
|
try {
|
|
10103
|
-
const cs = JSON.parse(
|
|
10900
|
+
const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
|
|
10104
10901
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
10105
10902
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10106
10903
|
);
|
|
@@ -10116,10 +10913,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10116
10913
|
} else {
|
|
10117
10914
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
10118
10915
|
}
|
|
10119
|
-
const geminiSettingsPath =
|
|
10120
|
-
if (
|
|
10916
|
+
const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
|
|
10917
|
+
if (fs24.existsSync(geminiSettingsPath)) {
|
|
10121
10918
|
try {
|
|
10122
|
-
const gs = JSON.parse(
|
|
10919
|
+
const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
|
|
10123
10920
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
10124
10921
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10125
10922
|
);
|
|
@@ -10135,10 +10932,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10135
10932
|
} else {
|
|
10136
10933
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
10137
10934
|
}
|
|
10138
|
-
const cursorHooksPath =
|
|
10139
|
-
if (
|
|
10935
|
+
const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
|
|
10936
|
+
if (fs24.existsSync(cursorHooksPath)) {
|
|
10140
10937
|
try {
|
|
10141
|
-
const cur = JSON.parse(
|
|
10938
|
+
const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
|
|
10142
10939
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
10143
10940
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
10144
10941
|
);
|
|
@@ -10176,9 +10973,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10176
10973
|
|
|
10177
10974
|
// src/cli/commands/audit.ts
|
|
10178
10975
|
import chalk8 from "chalk";
|
|
10179
|
-
import
|
|
10180
|
-
import
|
|
10181
|
-
import
|
|
10976
|
+
import fs25 from "fs";
|
|
10977
|
+
import path27 from "path";
|
|
10978
|
+
import os21 from "os";
|
|
10182
10979
|
function formatRelativeTime(timestamp) {
|
|
10183
10980
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
10184
10981
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -10191,14 +10988,14 @@ function formatRelativeTime(timestamp) {
|
|
|
10191
10988
|
}
|
|
10192
10989
|
function registerAuditCommand(program2) {
|
|
10193
10990
|
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) => {
|
|
10194
|
-
const logPath =
|
|
10195
|
-
if (!
|
|
10991
|
+
const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
|
|
10992
|
+
if (!fs25.existsSync(logPath)) {
|
|
10196
10993
|
console.log(
|
|
10197
10994
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
10198
10995
|
);
|
|
10199
10996
|
return;
|
|
10200
10997
|
}
|
|
10201
|
-
const raw =
|
|
10998
|
+
const raw = fs25.readFileSync(logPath, "utf-8");
|
|
10202
10999
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
10203
11000
|
let entries = lines.flatMap((line) => {
|
|
10204
11001
|
try {
|
|
@@ -10252,9 +11049,9 @@ function registerAuditCommand(program2) {
|
|
|
10252
11049
|
|
|
10253
11050
|
// src/cli/commands/report.ts
|
|
10254
11051
|
import chalk9 from "chalk";
|
|
10255
|
-
import
|
|
10256
|
-
import
|
|
10257
|
-
import
|
|
11052
|
+
import fs26 from "fs";
|
|
11053
|
+
import path28 from "path";
|
|
11054
|
+
import os22 from "os";
|
|
10258
11055
|
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;
|
|
10259
11056
|
function buildTestTimestamps(allEntries) {
|
|
10260
11057
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -10301,8 +11098,8 @@ function getDateRange(period) {
|
|
|
10301
11098
|
}
|
|
10302
11099
|
}
|
|
10303
11100
|
function parseAuditLog(logPath) {
|
|
10304
|
-
if (!
|
|
10305
|
-
const raw =
|
|
11101
|
+
if (!fs26.existsSync(logPath)) return [];
|
|
11102
|
+
const raw = fs26.readFileSync(logPath, "utf-8");
|
|
10306
11103
|
return raw.split("\n").flatMap((line) => {
|
|
10307
11104
|
if (!line.trim()) return [];
|
|
10308
11105
|
try {
|
|
@@ -10328,9 +11125,9 @@ function colorBar(value, max, width) {
|
|
|
10328
11125
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
10329
11126
|
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
10330
11127
|
}
|
|
10331
|
-
function pct(
|
|
11128
|
+
function pct(num3, total) {
|
|
10332
11129
|
if (total === 0) return "\u2013";
|
|
10333
|
-
return Math.round(
|
|
11130
|
+
return Math.round(num3 / total * 100) + "%";
|
|
10334
11131
|
}
|
|
10335
11132
|
function fmtDate(d) {
|
|
10336
11133
|
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
@@ -10371,11 +11168,11 @@ function loadClaudeCost(start, end) {
|
|
|
10371
11168
|
inputTokens: 0,
|
|
10372
11169
|
cacheReadTokens: 0
|
|
10373
11170
|
};
|
|
10374
|
-
const projectsDir =
|
|
10375
|
-
if (!
|
|
11171
|
+
const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
|
|
11172
|
+
if (!fs26.existsSync(projectsDir)) return empty;
|
|
10376
11173
|
let dirs;
|
|
10377
11174
|
try {
|
|
10378
|
-
dirs =
|
|
11175
|
+
dirs = fs26.readdirSync(projectsDir);
|
|
10379
11176
|
} catch {
|
|
10380
11177
|
return empty;
|
|
10381
11178
|
}
|
|
@@ -10385,18 +11182,18 @@ function loadClaudeCost(start, end) {
|
|
|
10385
11182
|
const byDay = /* @__PURE__ */ new Map();
|
|
10386
11183
|
const byModel = /* @__PURE__ */ new Map();
|
|
10387
11184
|
for (const proj of dirs) {
|
|
10388
|
-
const projPath =
|
|
11185
|
+
const projPath = path28.join(projectsDir, proj);
|
|
10389
11186
|
let files;
|
|
10390
11187
|
try {
|
|
10391
|
-
const stat =
|
|
11188
|
+
const stat = fs26.statSync(projPath);
|
|
10392
11189
|
if (!stat.isDirectory()) continue;
|
|
10393
|
-
files =
|
|
11190
|
+
files = fs26.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
10394
11191
|
} catch {
|
|
10395
11192
|
continue;
|
|
10396
11193
|
}
|
|
10397
11194
|
for (const file of files) {
|
|
10398
11195
|
try {
|
|
10399
|
-
const raw =
|
|
11196
|
+
const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
|
|
10400
11197
|
for (const line of raw.split("\n")) {
|
|
10401
11198
|
if (!line.trim()) continue;
|
|
10402
11199
|
let entry;
|
|
@@ -10439,7 +11236,7 @@ function registerReportCommand(program2) {
|
|
|
10439
11236
|
const period = ["today", "7d", "30d", "month"].includes(
|
|
10440
11237
|
options.period
|
|
10441
11238
|
) ? options.period : "7d";
|
|
10442
|
-
const logPath =
|
|
11239
|
+
const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
|
|
10443
11240
|
const allEntries = parseAuditLog(logPath);
|
|
10444
11241
|
if (allEntries.length === 0) {
|
|
10445
11242
|
console.log(
|
|
@@ -10690,26 +11487,67 @@ init_daemon2();
|
|
|
10690
11487
|
init_daemon();
|
|
10691
11488
|
import chalk10 from "chalk";
|
|
10692
11489
|
import { spawn as spawn7 } from "child_process";
|
|
11490
|
+
var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
|
|
10693
11491
|
function registerDaemonCommand(program2) {
|
|
10694
|
-
program2.command("daemon").description("
|
|
11492
|
+
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(
|
|
10695
11493
|
"-w, --watch",
|
|
10696
11494
|
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
10697
11495
|
).action(
|
|
10698
11496
|
async (action, options) => {
|
|
10699
11497
|
const cmd = (action ?? "start").toLowerCase();
|
|
10700
|
-
if (cmd === "
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
11498
|
+
if (cmd === "install") {
|
|
11499
|
+
const result = installDaemonService();
|
|
11500
|
+
if (!result.ok) {
|
|
11501
|
+
console.error(chalk10.red(`\u2717 ${result.reason}`));
|
|
11502
|
+
process.exit(1);
|
|
11503
|
+
}
|
|
11504
|
+
if (result.alreadyInstalled) {
|
|
11505
|
+
console.log(chalk10.green(`\u2713 Daemon service reinstalled (${result.platform})`));
|
|
11506
|
+
} else {
|
|
11507
|
+
console.log(chalk10.green(`\u2713 Daemon installed as login service (${result.platform})`));
|
|
11508
|
+
console.log(chalk10.gray(" The daemon will now start automatically on login."));
|
|
11509
|
+
}
|
|
11510
|
+
process.exit(0);
|
|
10707
11511
|
}
|
|
10708
|
-
if (
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
11512
|
+
if (cmd === "uninstall") {
|
|
11513
|
+
const result = uninstallDaemonService();
|
|
11514
|
+
if (!result.ok) {
|
|
11515
|
+
console.error(chalk10.red(`\u2717 ${result.reason}`));
|
|
11516
|
+
process.exit(1);
|
|
11517
|
+
}
|
|
11518
|
+
console.log(chalk10.green(`\u2713 Daemon service removed (${result.platform})`));
|
|
11519
|
+
console.log(chalk10.gray(" The daemon will no longer start automatically on login."));
|
|
11520
|
+
console.log(chalk10.gray(" To stop the running daemon: node9 daemon stop"));
|
|
11521
|
+
process.exit(0);
|
|
11522
|
+
}
|
|
11523
|
+
if (cmd === "stop") return stopDaemon();
|
|
11524
|
+
if (cmd === "restart") {
|
|
11525
|
+
stopDaemon();
|
|
11526
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11527
|
+
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
11528
|
+
detached: true,
|
|
11529
|
+
stdio: "ignore",
|
|
11530
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
11531
|
+
});
|
|
11532
|
+
child.unref();
|
|
11533
|
+
if (child.pid) {
|
|
11534
|
+
console.log(chalk10.green(`\u2713 Daemon restarted (PID ${child.pid})`));
|
|
11535
|
+
} else {
|
|
11536
|
+
console.error(chalk10.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
|
|
11537
|
+
process.exit(1);
|
|
11538
|
+
}
|
|
11539
|
+
process.exit(0);
|
|
11540
|
+
}
|
|
11541
|
+
if (cmd === "status") return daemonStatus();
|
|
11542
|
+
if (cmd !== "start" && action !== void 0) {
|
|
11543
|
+
console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
|
|
11544
|
+
process.exit(1);
|
|
11545
|
+
}
|
|
11546
|
+
if (options.watch) {
|
|
11547
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
11548
|
+
setTimeout(() => {
|
|
11549
|
+
openBrowserLocal();
|
|
11550
|
+
console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
10713
11551
|
}, 600);
|
|
10714
11552
|
startDaemon();
|
|
10715
11553
|
return;
|
|
@@ -10753,12 +11591,12 @@ function registerDaemonCommand(program2) {
|
|
|
10753
11591
|
init_core();
|
|
10754
11592
|
init_daemon();
|
|
10755
11593
|
import chalk11 from "chalk";
|
|
10756
|
-
import
|
|
10757
|
-
import
|
|
10758
|
-
import
|
|
11594
|
+
import fs27 from "fs";
|
|
11595
|
+
import path29 from "path";
|
|
11596
|
+
import os23 from "os";
|
|
10759
11597
|
function readJson2(filePath) {
|
|
10760
11598
|
try {
|
|
10761
|
-
if (
|
|
11599
|
+
if (fs27.existsSync(filePath)) return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
|
|
10762
11600
|
} catch {
|
|
10763
11601
|
}
|
|
10764
11602
|
return null;
|
|
@@ -10823,28 +11661,28 @@ function registerStatusCommand(program2) {
|
|
|
10823
11661
|
console.log("");
|
|
10824
11662
|
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10825
11663
|
console.log(` Mode: ${modeLabel}`);
|
|
10826
|
-
const projectConfig =
|
|
10827
|
-
const globalConfig =
|
|
11664
|
+
const projectConfig = path29.join(process.cwd(), "node9.config.json");
|
|
11665
|
+
const globalConfig = path29.join(os23.homedir(), ".node9", "config.json");
|
|
10828
11666
|
console.log(
|
|
10829
|
-
` Local: ${
|
|
11667
|
+
` Local: ${fs27.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10830
11668
|
);
|
|
10831
11669
|
console.log(
|
|
10832
|
-
` Global: ${
|
|
11670
|
+
` Global: ${fs27.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10833
11671
|
);
|
|
10834
11672
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10835
11673
|
console.log(
|
|
10836
11674
|
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10837
11675
|
);
|
|
10838
11676
|
}
|
|
10839
|
-
const homeDir2 =
|
|
11677
|
+
const homeDir2 = os23.homedir();
|
|
10840
11678
|
const claudeSettings = readJson2(
|
|
10841
|
-
|
|
11679
|
+
path29.join(homeDir2, ".claude", "settings.json")
|
|
10842
11680
|
);
|
|
10843
|
-
const claudeConfig = readJson2(
|
|
11681
|
+
const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
|
|
10844
11682
|
const geminiSettings = readJson2(
|
|
10845
|
-
|
|
11683
|
+
path29.join(homeDir2, ".gemini", "settings.json")
|
|
10846
11684
|
);
|
|
10847
|
-
const cursorConfig = readJson2(
|
|
11685
|
+
const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
|
|
10848
11686
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10849
11687
|
if (agentFound) {
|
|
10850
11688
|
console.log("");
|
|
@@ -10904,11 +11742,12 @@ function registerStatusCommand(program2) {
|
|
|
10904
11742
|
// src/cli/commands/init.ts
|
|
10905
11743
|
init_core();
|
|
10906
11744
|
import chalk12 from "chalk";
|
|
10907
|
-
import
|
|
10908
|
-
import
|
|
10909
|
-
import
|
|
10910
|
-
import
|
|
11745
|
+
import fs28 from "fs";
|
|
11746
|
+
import path30 from "path";
|
|
11747
|
+
import os24 from "os";
|
|
11748
|
+
import https3 from "https";
|
|
10911
11749
|
init_shields();
|
|
11750
|
+
init_service();
|
|
10912
11751
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10913
11752
|
function fireTelemetryPing(agents) {
|
|
10914
11753
|
try {
|
|
@@ -10918,7 +11757,7 @@ function fireTelemetryPing(agents) {
|
|
|
10918
11757
|
os: process.platform,
|
|
10919
11758
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10920
11759
|
});
|
|
10921
|
-
const req =
|
|
11760
|
+
const req = https3.request(
|
|
10922
11761
|
{
|
|
10923
11762
|
hostname: "api.node9.ai",
|
|
10924
11763
|
path: "/api/v1/telemetry",
|
|
@@ -10965,15 +11804,15 @@ function registerInitCommand(program2) {
|
|
|
10965
11804
|
}
|
|
10966
11805
|
console.log("");
|
|
10967
11806
|
}
|
|
10968
|
-
const configPath =
|
|
10969
|
-
if (
|
|
11807
|
+
const configPath = path30.join(os24.homedir(), ".node9", "config.json");
|
|
11808
|
+
if (fs28.existsSync(configPath) && !options.force) {
|
|
10970
11809
|
try {
|
|
10971
|
-
const existing = JSON.parse(
|
|
11810
|
+
const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
10972
11811
|
const settings = existing.settings ?? {};
|
|
10973
11812
|
if (settings.mode !== chosenMode) {
|
|
10974
11813
|
settings.mode = chosenMode;
|
|
10975
11814
|
existing.settings = settings;
|
|
10976
|
-
|
|
11815
|
+
fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10977
11816
|
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10978
11817
|
} else {
|
|
10979
11818
|
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
@@ -10986,9 +11825,9 @@ function registerInitCommand(program2) {
|
|
|
10986
11825
|
...DEFAULT_CONFIG,
|
|
10987
11826
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
10988
11827
|
};
|
|
10989
|
-
const dir =
|
|
10990
|
-
if (!
|
|
10991
|
-
|
|
11828
|
+
const dir = path30.dirname(configPath);
|
|
11829
|
+
if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
|
|
11830
|
+
fs28.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
10992
11831
|
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
10993
11832
|
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
10994
11833
|
}
|
|
@@ -11000,9 +11839,13 @@ function registerInitCommand(program2) {
|
|
|
11000
11839
|
);
|
|
11001
11840
|
if (found.length === 0) {
|
|
11002
11841
|
console.log(
|
|
11003
|
-
chalk12.gray(
|
|
11842
|
+
chalk12.gray(
|
|
11843
|
+
"No AI agents detected. Install Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
|
|
11844
|
+
)
|
|
11845
|
+
);
|
|
11846
|
+
console.log(
|
|
11847
|
+
chalk12.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
|
|
11004
11848
|
);
|
|
11005
|
-
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
11006
11849
|
return;
|
|
11007
11850
|
}
|
|
11008
11851
|
console.log(chalk12.bold("Detected agents:"));
|
|
@@ -11016,6 +11859,32 @@ function registerInitCommand(program2) {
|
|
|
11016
11859
|
else if (agent === "gemini") await setupGemini();
|
|
11017
11860
|
else if (agent === "cursor") await setupCursor();
|
|
11018
11861
|
else if (agent === "codex") await setupCodex();
|
|
11862
|
+
else if (agent === "windsurf") await setupWindsurf();
|
|
11863
|
+
else if (agent === "vscode") await setupVSCode();
|
|
11864
|
+
console.log("");
|
|
11865
|
+
}
|
|
11866
|
+
if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
|
|
11867
|
+
const alreadyInstalled = isDaemonServiceInstalled();
|
|
11868
|
+
if (!alreadyInstalled) {
|
|
11869
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
11870
|
+
const installService = await confirm3({
|
|
11871
|
+
message: "Install daemon as a login service? (starts automatically on login)",
|
|
11872
|
+
default: true
|
|
11873
|
+
});
|
|
11874
|
+
if (installService) {
|
|
11875
|
+
const result = installDaemonService();
|
|
11876
|
+
if (result.ok) {
|
|
11877
|
+
console.log(
|
|
11878
|
+
chalk12.green(` \u2713 Daemon installed as login service (${result.platform})`)
|
|
11879
|
+
);
|
|
11880
|
+
} else {
|
|
11881
|
+
console.log(chalk12.yellow(` \u26A0\uFE0F Could not install service: ${result.reason}`));
|
|
11882
|
+
console.log(chalk12.gray(" You can try again later with: node9 daemon install"));
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
} else {
|
|
11886
|
+
console.log(chalk12.green(" \u2713 Daemon login service already installed"));
|
|
11887
|
+
}
|
|
11019
11888
|
console.log("");
|
|
11020
11889
|
}
|
|
11021
11890
|
{
|
|
@@ -11042,7 +11911,7 @@ function registerInitCommand(program2) {
|
|
|
11042
11911
|
}
|
|
11043
11912
|
|
|
11044
11913
|
// src/cli/commands/undo.ts
|
|
11045
|
-
import
|
|
11914
|
+
import path31 from "path";
|
|
11046
11915
|
import chalk14 from "chalk";
|
|
11047
11916
|
|
|
11048
11917
|
// src/tui/undo-navigator.ts
|
|
@@ -11201,7 +12070,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
11201
12070
|
let dir = startDir;
|
|
11202
12071
|
while (true) {
|
|
11203
12072
|
if (cwds.has(dir)) return dir;
|
|
11204
|
-
const parent =
|
|
12073
|
+
const parent = path31.dirname(dir);
|
|
11205
12074
|
if (parent === dir) return null;
|
|
11206
12075
|
dir = parent;
|
|
11207
12076
|
}
|
|
@@ -11330,7 +12199,7 @@ function registerUndoCommand(program2) {
|
|
|
11330
12199
|
// src/cli/commands/watch.ts
|
|
11331
12200
|
init_daemon();
|
|
11332
12201
|
import chalk15 from "chalk";
|
|
11333
|
-
import { spawn as spawn8, spawnSync as
|
|
12202
|
+
import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
|
|
11334
12203
|
function registerWatchCommand(program2) {
|
|
11335
12204
|
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) => {
|
|
11336
12205
|
let port = DAEMON_PORT;
|
|
@@ -11376,7 +12245,7 @@ function registerWatchCommand(program2) {
|
|
|
11376
12245
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
11377
12246
|
)
|
|
11378
12247
|
);
|
|
11379
|
-
const result =
|
|
12248
|
+
const result = spawnSync6(cmd, args, {
|
|
11380
12249
|
stdio: "inherit",
|
|
11381
12250
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
11382
12251
|
});
|
|
@@ -11397,12 +12266,12 @@ import { execa as execa2 } from "execa";
|
|
|
11397
12266
|
init_provenance();
|
|
11398
12267
|
|
|
11399
12268
|
// src/mcp-pin.ts
|
|
11400
|
-
import
|
|
11401
|
-
import
|
|
11402
|
-
import
|
|
12269
|
+
import fs29 from "fs";
|
|
12270
|
+
import path32 from "path";
|
|
12271
|
+
import os25 from "os";
|
|
11403
12272
|
import crypto4 from "crypto";
|
|
11404
12273
|
function getPinsFilePath() {
|
|
11405
|
-
return
|
|
12274
|
+
return path32.join(os25.homedir(), ".node9", "mcp-pins.json");
|
|
11406
12275
|
}
|
|
11407
12276
|
function hashToolDefinitions(tools) {
|
|
11408
12277
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -11419,7 +12288,7 @@ function getServerKey(upstreamCommand) {
|
|
|
11419
12288
|
function readMcpPinsSafe() {
|
|
11420
12289
|
const filePath = getPinsFilePath();
|
|
11421
12290
|
try {
|
|
11422
|
-
const raw =
|
|
12291
|
+
const raw = fs29.readFileSync(filePath, "utf-8");
|
|
11423
12292
|
if (!raw.trim()) {
|
|
11424
12293
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
11425
12294
|
}
|
|
@@ -11443,10 +12312,10 @@ function readMcpPins() {
|
|
|
11443
12312
|
}
|
|
11444
12313
|
function writeMcpPins(data) {
|
|
11445
12314
|
const filePath = getPinsFilePath();
|
|
11446
|
-
|
|
12315
|
+
fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
|
|
11447
12316
|
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
11448
|
-
|
|
11449
|
-
|
|
12317
|
+
fs29.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
12318
|
+
fs29.renameSync(tmp, filePath);
|
|
11450
12319
|
}
|
|
11451
12320
|
function checkPin(serverKey, currentHash) {
|
|
11452
12321
|
const result = readMcpPinsSafe();
|
|
@@ -11818,9 +12687,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11818
12687
|
|
|
11819
12688
|
// src/mcp-server/index.ts
|
|
11820
12689
|
import readline4 from "readline";
|
|
11821
|
-
import
|
|
11822
|
-
import
|
|
11823
|
-
import
|
|
12690
|
+
import fs30 from "fs";
|
|
12691
|
+
import os26 from "os";
|
|
12692
|
+
import path33 from "path";
|
|
11824
12693
|
init_core();
|
|
11825
12694
|
init_daemon();
|
|
11826
12695
|
init_shields();
|
|
@@ -11995,13 +12864,13 @@ function handleStatus() {
|
|
|
11995
12864
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
11996
12865
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
11997
12866
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
11998
|
-
const projectConfig =
|
|
11999
|
-
const globalConfig =
|
|
12867
|
+
const projectConfig = path33.join(process.cwd(), "node9.config.json");
|
|
12868
|
+
const globalConfig = path33.join(os26.homedir(), ".node9", "config.json");
|
|
12000
12869
|
lines.push(
|
|
12001
|
-
`Project config (node9.config.json): ${
|
|
12870
|
+
`Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
|
|
12002
12871
|
);
|
|
12003
12872
|
lines.push(
|
|
12004
|
-
`Global config (~/.node9/config.json): ${
|
|
12873
|
+
`Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
|
|
12005
12874
|
);
|
|
12006
12875
|
return lines.join("\n");
|
|
12007
12876
|
}
|
|
@@ -12075,21 +12944,21 @@ function handleShieldDisable(args) {
|
|
|
12075
12944
|
writeActiveShields(active.filter((s) => s !== name));
|
|
12076
12945
|
return `Shield "${name}" disabled.`;
|
|
12077
12946
|
}
|
|
12078
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
12947
|
+
var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
|
|
12079
12948
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
12080
12949
|
function readGlobalConfigRaw() {
|
|
12081
12950
|
try {
|
|
12082
|
-
if (
|
|
12083
|
-
return JSON.parse(
|
|
12951
|
+
if (fs30.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
12952
|
+
return JSON.parse(fs30.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
12084
12953
|
}
|
|
12085
12954
|
} catch {
|
|
12086
12955
|
}
|
|
12087
12956
|
return {};
|
|
12088
12957
|
}
|
|
12089
12958
|
function writeGlobalConfigRaw(data) {
|
|
12090
|
-
const dir =
|
|
12091
|
-
if (!
|
|
12092
|
-
|
|
12959
|
+
const dir = path33.dirname(GLOBAL_CONFIG_PATH2);
|
|
12960
|
+
if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
|
|
12961
|
+
fs30.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
12093
12962
|
}
|
|
12094
12963
|
function handleApproverList() {
|
|
12095
12964
|
const config = getConfig();
|
|
@@ -12132,9 +13001,9 @@ function handleApproverSet(args) {
|
|
|
12132
13001
|
}
|
|
12133
13002
|
function handleAuditGet(args) {
|
|
12134
13003
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
12135
|
-
const auditPath =
|
|
12136
|
-
if (!
|
|
12137
|
-
const lines =
|
|
13004
|
+
const auditPath = path33.join(os26.homedir(), ".node9", "audit.log");
|
|
13005
|
+
if (!fs30.existsSync(auditPath)) return "No audit log found.";
|
|
13006
|
+
const lines = fs30.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
12138
13007
|
const recent = lines.slice(-limit);
|
|
12139
13008
|
const entries = recent.map((line) => {
|
|
12140
13009
|
try {
|
|
@@ -12452,25 +13321,950 @@ function registerMcpPinCommand(program2) {
|
|
|
12452
13321
|
});
|
|
12453
13322
|
}
|
|
12454
13323
|
|
|
13324
|
+
// src/cli/commands/sync.ts
|
|
13325
|
+
init_sync();
|
|
13326
|
+
import chalk19 from "chalk";
|
|
13327
|
+
function registerSyncCommand(program2) {
|
|
13328
|
+
const policy = program2.command("policy").description("Manage cloud policy rules");
|
|
13329
|
+
policy.command("sync").description("Sync cloud policy rules to local cache (~/.node9/rules-cache.json)").action(async () => {
|
|
13330
|
+
process.stdout.write(chalk19.cyan("Syncing cloud policy rules\u2026"));
|
|
13331
|
+
const result = await runCloudSync();
|
|
13332
|
+
process.stdout.write("\n");
|
|
13333
|
+
if (!result.ok) {
|
|
13334
|
+
console.error(chalk19.red(`\u2717 ${result.reason}`));
|
|
13335
|
+
process.exit(1);
|
|
13336
|
+
}
|
|
13337
|
+
console.log(
|
|
13338
|
+
chalk19.green(`\u2713 Synced ${result.rules} rule${result.rules === 1 ? "" : "s"} from cloud`)
|
|
13339
|
+
);
|
|
13340
|
+
console.log(chalk19.gray(` Cached at: ${result.fetchedAt}`));
|
|
13341
|
+
console.log(chalk19.gray(` File: ~/.node9/rules-cache.json`));
|
|
13342
|
+
});
|
|
13343
|
+
policy.command("show").description("List all cloud policy rules in the local cache").action(() => {
|
|
13344
|
+
const status = getCloudSyncStatus();
|
|
13345
|
+
if (!status.cached) {
|
|
13346
|
+
console.log(chalk19.yellow("\n No cloud rules cached \u2014 run: node9 policy sync\n"));
|
|
13347
|
+
return;
|
|
13348
|
+
}
|
|
13349
|
+
const rules = getCloudRules() ?? [];
|
|
13350
|
+
const age = Math.round((Date.now() - new Date(status.fetchedAt).getTime()) / 6e4);
|
|
13351
|
+
console.log(
|
|
13352
|
+
chalk19.bold(`
|
|
13353
|
+
Cloud policy rules`) + chalk19.gray(
|
|
13354
|
+
` (${rules.length} rule${rules.length === 1 ? "" : "s"}, synced ${age}m ago)
|
|
13355
|
+
`
|
|
13356
|
+
)
|
|
13357
|
+
);
|
|
13358
|
+
if (rules.length === 0) {
|
|
13359
|
+
console.log(chalk19.gray(" No rules defined in cloud policy.\n"));
|
|
13360
|
+
return;
|
|
13361
|
+
}
|
|
13362
|
+
for (const rule of rules) {
|
|
13363
|
+
const r = rule;
|
|
13364
|
+
const verdictColor = r.verdict === "block" ? chalk19.red : r.verdict === "allow" ? chalk19.green : chalk19.yellow;
|
|
13365
|
+
console.log(
|
|
13366
|
+
` ${verdictColor(
|
|
13367
|
+
String(r.verdict ?? "unknown").toUpperCase().padEnd(6)
|
|
13368
|
+
)} ${chalk19.white(String(r.name ?? "(unnamed)"))}`
|
|
13369
|
+
);
|
|
13370
|
+
if (r.reason) console.log(chalk19.gray(` ${String(r.reason)}`));
|
|
13371
|
+
}
|
|
13372
|
+
console.log("");
|
|
13373
|
+
});
|
|
13374
|
+
policy.command("status").description("Show current cloud policy cache status").action(() => {
|
|
13375
|
+
const s = getCloudSyncStatus();
|
|
13376
|
+
if (!s.cached) {
|
|
13377
|
+
console.log(chalk19.yellow("\n No cache yet \u2014 run: node9 policy sync\n"));
|
|
13378
|
+
} else {
|
|
13379
|
+
const age = Math.round((Date.now() - new Date(s.fetchedAt).getTime()) / 6e4);
|
|
13380
|
+
console.log(`
|
|
13381
|
+
Rules : ${chalk19.green(String(s.rules))} cloud rules loaded`);
|
|
13382
|
+
console.log(
|
|
13383
|
+
` Synced : ${chalk19.gray(`${age} minute${age === 1 ? "" : "s"} ago`)} (${s.fetchedAt})
|
|
13384
|
+
`
|
|
13385
|
+
);
|
|
13386
|
+
}
|
|
13387
|
+
});
|
|
13388
|
+
}
|
|
13389
|
+
|
|
13390
|
+
// src/cli/commands/agents.ts
|
|
13391
|
+
import chalk20 from "chalk";
|
|
13392
|
+
var SETUP_FN = {
|
|
13393
|
+
claude: setupClaude,
|
|
13394
|
+
gemini: setupGemini,
|
|
13395
|
+
cursor: setupCursor,
|
|
13396
|
+
codex: setupCodex,
|
|
13397
|
+
windsurf: setupWindsurf,
|
|
13398
|
+
vscode: setupVSCode
|
|
13399
|
+
};
|
|
13400
|
+
var TEARDOWN_FN = {
|
|
13401
|
+
claude: teardownClaude,
|
|
13402
|
+
gemini: teardownGemini,
|
|
13403
|
+
cursor: teardownCursor,
|
|
13404
|
+
codex: teardownCodex,
|
|
13405
|
+
windsurf: teardownWindsurf,
|
|
13406
|
+
vscode: teardownVSCode
|
|
13407
|
+
};
|
|
13408
|
+
var AGENT_NAMES = Object.keys(SETUP_FN);
|
|
13409
|
+
function registerAgentsCommand(program2) {
|
|
13410
|
+
const agents = program2.command("agents").description("List and manage AI agent integrations");
|
|
13411
|
+
agents.command("list").description("Show all supported agents and their Node9 status").action(() => {
|
|
13412
|
+
const statuses = getAgentsStatus();
|
|
13413
|
+
const anyInstalled = statuses.some((s) => s.installed);
|
|
13414
|
+
console.log("");
|
|
13415
|
+
console.log(` ${"Agent".padEnd(14)}${"Installed".padEnd(11)}${"Wired".padEnd(8)}Mode`);
|
|
13416
|
+
console.log(" " + "\u2500".repeat(44));
|
|
13417
|
+
for (const s of statuses) {
|
|
13418
|
+
const installed = s.installed ? chalk20.green("\u2713") : chalk20.gray("\u2717");
|
|
13419
|
+
const wired = !s.installed ? chalk20.gray("\u2014") : s.wired ? chalk20.green("\u2713") : chalk20.yellow("\u2717");
|
|
13420
|
+
const mode = s.mode ? chalk20.gray(s.mode) : chalk20.gray("\u2014");
|
|
13421
|
+
const hint = s.installed && !s.wired ? chalk20.gray(` \u2190 node9 agents add ${s.name}`) : "";
|
|
13422
|
+
console.log(` ${s.label.padEnd(14)}${installed} ${wired} ${mode}${hint}`);
|
|
13423
|
+
}
|
|
13424
|
+
console.log("");
|
|
13425
|
+
if (!anyInstalled) {
|
|
13426
|
+
console.log(
|
|
13427
|
+
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")
|
|
13428
|
+
);
|
|
13429
|
+
return;
|
|
13430
|
+
}
|
|
13431
|
+
const unwired = statuses.filter((s) => s.installed && !s.wired);
|
|
13432
|
+
if (unwired.length > 0) {
|
|
13433
|
+
console.log(
|
|
13434
|
+
chalk20.yellow(` ${unwired.length} agent(s) not yet wired. Run: `) + chalk20.white(`node9 agents add ${unwired[0].name}`) + "\n"
|
|
13435
|
+
);
|
|
13436
|
+
}
|
|
13437
|
+
});
|
|
13438
|
+
agents.command("add").description("Wire Node9 into an agent").argument("<agent>", `Agent to wire: ${AGENT_NAMES.join(" | ")}`).action(async (agent) => {
|
|
13439
|
+
const name = agent.toLowerCase();
|
|
13440
|
+
const fn = SETUP_FN[name];
|
|
13441
|
+
if (!fn) {
|
|
13442
|
+
console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
|
|
13443
|
+
process.exit(1);
|
|
13444
|
+
}
|
|
13445
|
+
await fn();
|
|
13446
|
+
});
|
|
13447
|
+
agents.command("remove").description("Remove Node9 from an agent").argument("<agent>", `Agent to unwire: ${AGENT_NAMES.join(" | ")}`).action((agent) => {
|
|
13448
|
+
const name = agent.toLowerCase();
|
|
13449
|
+
const fn = TEARDOWN_FN[name];
|
|
13450
|
+
if (!fn) {
|
|
13451
|
+
console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
|
|
13452
|
+
process.exit(1);
|
|
13453
|
+
}
|
|
13454
|
+
console.log(chalk20.cyan(`
|
|
13455
|
+
\u{1F6E1}\uFE0F Node9: removing from ${name}...
|
|
13456
|
+
`));
|
|
13457
|
+
fn();
|
|
13458
|
+
console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
|
|
13459
|
+
});
|
|
13460
|
+
}
|
|
13461
|
+
|
|
13462
|
+
// src/cli/commands/scan.ts
|
|
13463
|
+
init_shields();
|
|
13464
|
+
init_config();
|
|
13465
|
+
init_policy();
|
|
13466
|
+
init_dlp();
|
|
13467
|
+
import chalk21 from "chalk";
|
|
13468
|
+
import fs31 from "fs";
|
|
13469
|
+
import path34 from "path";
|
|
13470
|
+
import os27 from "os";
|
|
13471
|
+
var CLAUDE_PRICING2 = {
|
|
13472
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13473
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13474
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
13475
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13476
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13477
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13478
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13479
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13480
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
13481
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
13482
|
+
};
|
|
13483
|
+
function claudeModelPrice2(model) {
|
|
13484
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
13485
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
|
|
13486
|
+
if (base === key || base.startsWith(key)) return p;
|
|
13487
|
+
}
|
|
13488
|
+
return null;
|
|
13489
|
+
}
|
|
13490
|
+
function num2(n) {
|
|
13491
|
+
return n.toLocaleString();
|
|
13492
|
+
}
|
|
13493
|
+
function fmtCost2(usd) {
|
|
13494
|
+
if (usd < 1e-3) return "< $0.001";
|
|
13495
|
+
if (usd < 1) return "$" + usd.toFixed(4);
|
|
13496
|
+
return "$" + usd.toFixed(2);
|
|
13497
|
+
}
|
|
13498
|
+
function fmtTs(ts) {
|
|
13499
|
+
try {
|
|
13500
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
13501
|
+
month: "short",
|
|
13502
|
+
day: "numeric",
|
|
13503
|
+
year: "numeric"
|
|
13504
|
+
});
|
|
13505
|
+
} catch {
|
|
13506
|
+
return ts.slice(0, 10);
|
|
13507
|
+
}
|
|
13508
|
+
}
|
|
13509
|
+
function preview(input, max) {
|
|
13510
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
13511
|
+
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
13512
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
13513
|
+
}
|
|
13514
|
+
function buildRuleSources() {
|
|
13515
|
+
const sources = [];
|
|
13516
|
+
for (const [shieldName, shield] of Object.entries(SHIELDS)) {
|
|
13517
|
+
for (const rule of shield.smartRules) {
|
|
13518
|
+
sources.push({ shieldName, shieldLabel: shieldName, rule });
|
|
13519
|
+
}
|
|
13520
|
+
}
|
|
13521
|
+
try {
|
|
13522
|
+
const config = getConfig();
|
|
13523
|
+
for (const rule of config.policy.smartRules) {
|
|
13524
|
+
if (!rule.name) continue;
|
|
13525
|
+
if (rule.name.startsWith("shield:")) continue;
|
|
13526
|
+
const isCloud = rule.name.startsWith("cloud:");
|
|
13527
|
+
sources.push({
|
|
13528
|
+
shieldName: isCloud ? "cloud" : "custom",
|
|
13529
|
+
shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
|
|
13530
|
+
rule
|
|
13531
|
+
});
|
|
13532
|
+
}
|
|
13533
|
+
} catch {
|
|
13534
|
+
}
|
|
13535
|
+
return sources;
|
|
13536
|
+
}
|
|
13537
|
+
function scanClaudeHistory(startDate) {
|
|
13538
|
+
const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
|
|
13539
|
+
const result = {
|
|
13540
|
+
filesScanned: 0,
|
|
13541
|
+
sessions: 0,
|
|
13542
|
+
totalToolCalls: 0,
|
|
13543
|
+
bashCalls: 0,
|
|
13544
|
+
findings: [],
|
|
13545
|
+
dlpFindings: [],
|
|
13546
|
+
totalCostUSD: 0,
|
|
13547
|
+
firstDate: null,
|
|
13548
|
+
lastDate: null
|
|
13549
|
+
};
|
|
13550
|
+
if (!fs31.existsSync(projectsDir)) return result;
|
|
13551
|
+
let projDirs;
|
|
13552
|
+
try {
|
|
13553
|
+
projDirs = fs31.readdirSync(projectsDir);
|
|
13554
|
+
} catch {
|
|
13555
|
+
return result;
|
|
13556
|
+
}
|
|
13557
|
+
const ruleSources = buildRuleSources();
|
|
13558
|
+
for (const proj of projDirs) {
|
|
13559
|
+
const projPath = path34.join(projectsDir, proj);
|
|
13560
|
+
try {
|
|
13561
|
+
if (!fs31.statSync(projPath).isDirectory()) continue;
|
|
13562
|
+
} catch {
|
|
13563
|
+
continue;
|
|
13564
|
+
}
|
|
13565
|
+
const projLabel = decodeURIComponent(proj).replace(os27.homedir(), "~").slice(0, 40);
|
|
13566
|
+
let files;
|
|
13567
|
+
try {
|
|
13568
|
+
files = fs31.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
13569
|
+
} catch {
|
|
13570
|
+
continue;
|
|
13571
|
+
}
|
|
13572
|
+
for (const file of files) {
|
|
13573
|
+
result.filesScanned++;
|
|
13574
|
+
result.sessions++;
|
|
13575
|
+
let raw;
|
|
13576
|
+
try {
|
|
13577
|
+
raw = fs31.readFileSync(path34.join(projPath, file), "utf-8");
|
|
13578
|
+
} catch {
|
|
13579
|
+
continue;
|
|
13580
|
+
}
|
|
13581
|
+
for (const line of raw.split("\n")) {
|
|
13582
|
+
if (!line.trim()) continue;
|
|
13583
|
+
let entry;
|
|
13584
|
+
try {
|
|
13585
|
+
entry = JSON.parse(line);
|
|
13586
|
+
} catch {
|
|
13587
|
+
continue;
|
|
13588
|
+
}
|
|
13589
|
+
if (entry.type !== "assistant") continue;
|
|
13590
|
+
if (startDate && entry.timestamp) {
|
|
13591
|
+
if (new Date(entry.timestamp) < startDate) continue;
|
|
13592
|
+
}
|
|
13593
|
+
if (entry.timestamp) {
|
|
13594
|
+
if (!result.firstDate || entry.timestamp < result.firstDate)
|
|
13595
|
+
result.firstDate = entry.timestamp;
|
|
13596
|
+
if (!result.lastDate || entry.timestamp > result.lastDate)
|
|
13597
|
+
result.lastDate = entry.timestamp;
|
|
13598
|
+
}
|
|
13599
|
+
const usage = entry.message?.usage;
|
|
13600
|
+
const model = entry.message?.model;
|
|
13601
|
+
if (usage && model) {
|
|
13602
|
+
const p = claudeModelPrice2(model);
|
|
13603
|
+
if (p) {
|
|
13604
|
+
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;
|
|
13605
|
+
}
|
|
13606
|
+
}
|
|
13607
|
+
const content = entry.message?.content;
|
|
13608
|
+
if (!Array.isArray(content)) continue;
|
|
13609
|
+
for (const block of content) {
|
|
13610
|
+
if (block.type !== "tool_use") continue;
|
|
13611
|
+
result.totalToolCalls++;
|
|
13612
|
+
const toolName = block.name ?? "";
|
|
13613
|
+
const toolNameLower = toolName.toLowerCase();
|
|
13614
|
+
const input = block.input ?? {};
|
|
13615
|
+
if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
|
|
13616
|
+
result.bashCalls++;
|
|
13617
|
+
}
|
|
13618
|
+
const dlpMatch = scanArgs(input);
|
|
13619
|
+
if (dlpMatch) {
|
|
13620
|
+
const isDupe = result.dlpFindings.some(
|
|
13621
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
13622
|
+
);
|
|
13623
|
+
if (!isDupe) {
|
|
13624
|
+
result.dlpFindings.push({
|
|
13625
|
+
patternName: dlpMatch.patternName,
|
|
13626
|
+
redactedSample: dlpMatch.redactedSample,
|
|
13627
|
+
toolName,
|
|
13628
|
+
timestamp: entry.timestamp ?? "",
|
|
13629
|
+
project: projLabel
|
|
13630
|
+
});
|
|
13631
|
+
}
|
|
13632
|
+
}
|
|
13633
|
+
for (const source of ruleSources) {
|
|
13634
|
+
const { rule } = source;
|
|
13635
|
+
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
13636
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
13637
|
+
const inputPreview = preview(input, 120);
|
|
13638
|
+
const isDupe = result.findings.some(
|
|
13639
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
13640
|
+
);
|
|
13641
|
+
if (!isDupe) {
|
|
13642
|
+
result.findings.push({
|
|
13643
|
+
source,
|
|
13644
|
+
toolName,
|
|
13645
|
+
input,
|
|
13646
|
+
timestamp: entry.timestamp ?? "",
|
|
13647
|
+
project: projLabel
|
|
13648
|
+
});
|
|
13649
|
+
}
|
|
13650
|
+
break;
|
|
13651
|
+
}
|
|
13652
|
+
}
|
|
13653
|
+
}
|
|
13654
|
+
}
|
|
13655
|
+
}
|
|
13656
|
+
return result;
|
|
13657
|
+
}
|
|
13658
|
+
function registerScanCommand(program2) {
|
|
13659
|
+
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) => {
|
|
13660
|
+
const topN = Math.max(1, parseInt(options.top, 10) || 5);
|
|
13661
|
+
const startDate = options.all ? null : (() => {
|
|
13662
|
+
const d = /* @__PURE__ */ new Date();
|
|
13663
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
13664
|
+
d.setHours(0, 0, 0, 0);
|
|
13665
|
+
return d;
|
|
13666
|
+
})();
|
|
13667
|
+
console.log("");
|
|
13668
|
+
console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
|
|
13669
|
+
console.log("");
|
|
13670
|
+
const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
|
|
13671
|
+
if (!fs31.existsSync(projectsDir)) {
|
|
13672
|
+
console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
|
|
13673
|
+
console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
13674
|
+
return;
|
|
13675
|
+
}
|
|
13676
|
+
process.stdout.write(chalk21.dim(" Scanning\u2026"));
|
|
13677
|
+
const scan = scanClaudeHistory(startDate);
|
|
13678
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
13679
|
+
if (scan.filesScanned === 0) {
|
|
13680
|
+
console.log(chalk21.yellow(" No JSONL session files found.\n"));
|
|
13681
|
+
return;
|
|
13682
|
+
}
|
|
13683
|
+
const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
|
|
13684
|
+
const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
13685
|
+
console.log(
|
|
13686
|
+
" " + 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
|
|
13687
|
+
);
|
|
13688
|
+
console.log("");
|
|
13689
|
+
const byShield = /* @__PURE__ */ new Map();
|
|
13690
|
+
for (const f of scan.findings) {
|
|
13691
|
+
const key = f.source.shieldName;
|
|
13692
|
+
const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
|
|
13693
|
+
entry.findings.push(f);
|
|
13694
|
+
byShield.set(key, entry);
|
|
13695
|
+
}
|
|
13696
|
+
const totalFindings = scan.findings.length;
|
|
13697
|
+
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
13698
|
+
console.log(chalk21.green(" \u2705 No findings across all shields and rules."));
|
|
13699
|
+
console.log(chalk21.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
|
|
13700
|
+
} else {
|
|
13701
|
+
if (totalFindings > 0) {
|
|
13702
|
+
console.log(
|
|
13703
|
+
" " + chalk21.bold("If node9 had been installed:") + " " + chalk21.yellow.bold(
|
|
13704
|
+
`${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
|
|
13705
|
+
)
|
|
13706
|
+
);
|
|
13707
|
+
console.log("");
|
|
13708
|
+
const sorted = [...byShield.entries()].sort(
|
|
13709
|
+
(a, b) => b[1].findings.length - a[1].findings.length
|
|
13710
|
+
);
|
|
13711
|
+
for (const [shieldName, { label, findings }] of sorted) {
|
|
13712
|
+
const count = findings.length;
|
|
13713
|
+
const isUserRule = shieldName === "custom" || shieldName === "cloud";
|
|
13714
|
+
const shieldBadge = isUserRule ? chalk21.magenta(label) : chalk21.cyan(label);
|
|
13715
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
13716
|
+
console.log(
|
|
13717
|
+
" " + shieldBadge + chalk21.dim(" \xB7 ") + chalk21.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : chalk21.dim(` \u2192 node9 shield enable ${shieldName}`))
|
|
13718
|
+
);
|
|
13719
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
13720
|
+
for (const f of findings) {
|
|
13721
|
+
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
13722
|
+
const arr = byRule.get(ruleKey) ?? [];
|
|
13723
|
+
arr.push(f);
|
|
13724
|
+
byRule.set(ruleKey, arr);
|
|
13725
|
+
}
|
|
13726
|
+
for (const [, ruleFindings] of byRule) {
|
|
13727
|
+
const rule = ruleFindings[0].source.rule;
|
|
13728
|
+
const ruleCount = ruleFindings.length;
|
|
13729
|
+
const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
|
|
13730
|
+
const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
13731
|
+
console.log(
|
|
13732
|
+
" " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
|
|
13733
|
+
);
|
|
13734
|
+
const shown = ruleFindings.slice(0, topN);
|
|
13735
|
+
for (const f of shown) {
|
|
13736
|
+
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
13737
|
+
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
13738
|
+
const cmd = chalk21.gray(preview(f.input, 55));
|
|
13739
|
+
console.log(` ${ts}${proj}${cmd}`);
|
|
13740
|
+
}
|
|
13741
|
+
if (ruleFindings.length > topN) {
|
|
13742
|
+
console.log(
|
|
13743
|
+
chalk21.dim(
|
|
13744
|
+
` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
|
|
13745
|
+
)
|
|
13746
|
+
);
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
console.log("");
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13752
|
+
if (scan.dlpFindings.length > 0) {
|
|
13753
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
13754
|
+
console.log(
|
|
13755
|
+
" " + chalk21.red.bold("Secrets / DLP") + chalk21.dim(" \xB7 ") + chalk21.red(
|
|
13756
|
+
`${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
|
|
13757
|
+
)
|
|
13758
|
+
);
|
|
13759
|
+
const shownDlp = scan.dlpFindings.slice(0, topN);
|
|
13760
|
+
for (const f of shownDlp) {
|
|
13761
|
+
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
13762
|
+
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
13763
|
+
console.log(
|
|
13764
|
+
` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
|
|
13765
|
+
);
|
|
13766
|
+
}
|
|
13767
|
+
if (scan.dlpFindings.length > topN) {
|
|
13768
|
+
console.log(
|
|
13769
|
+
chalk21.dim(
|
|
13770
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--top ${scan.dlpFindings.length})`
|
|
13771
|
+
)
|
|
13772
|
+
);
|
|
13773
|
+
}
|
|
13774
|
+
console.log("");
|
|
13775
|
+
}
|
|
13776
|
+
}
|
|
13777
|
+
if (scan.totalCostUSD > 0) {
|
|
13778
|
+
console.log(
|
|
13779
|
+
" " + chalk21.bold("Claude spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
|
|
13780
|
+
);
|
|
13781
|
+
console.log("");
|
|
13782
|
+
}
|
|
13783
|
+
const auditLog = path34.join(os27.homedir(), ".node9", "audit.log");
|
|
13784
|
+
if (fs31.existsSync(auditLog)) {
|
|
13785
|
+
console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
|
|
13786
|
+
console.log(
|
|
13787
|
+
chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
|
|
13788
|
+
);
|
|
13789
|
+
} else {
|
|
13790
|
+
console.log(chalk21.yellow.bold(" \u26A1 node9 was not running during these sessions."));
|
|
13791
|
+
console.log(
|
|
13792
|
+
" " + chalk21.white("Run ") + chalk21.cyan("node9 init") + chalk21.white(" to start protecting your AI agents.")
|
|
13793
|
+
);
|
|
13794
|
+
}
|
|
13795
|
+
console.log("");
|
|
13796
|
+
});
|
|
13797
|
+
}
|
|
13798
|
+
|
|
13799
|
+
// src/cli/commands/sessions.ts
|
|
13800
|
+
import chalk22 from "chalk";
|
|
13801
|
+
import fs32 from "fs";
|
|
13802
|
+
import path35 from "path";
|
|
13803
|
+
import os28 from "os";
|
|
13804
|
+
var CLAUDE_PRICING3 = {
|
|
13805
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13806
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13807
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
13808
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13809
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13810
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13811
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13812
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
13813
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
13814
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
13815
|
+
};
|
|
13816
|
+
function modelPrice(model) {
|
|
13817
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
13818
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
|
|
13819
|
+
if (base === key || base.startsWith(key)) return p;
|
|
13820
|
+
}
|
|
13821
|
+
return null;
|
|
13822
|
+
}
|
|
13823
|
+
function encodeProjectPath(projectPath) {
|
|
13824
|
+
return projectPath.replace(/\//g, "-");
|
|
13825
|
+
}
|
|
13826
|
+
function sessionJsonlPath(projectPath, sessionId) {
|
|
13827
|
+
const encoded = encodeProjectPath(projectPath);
|
|
13828
|
+
return path35.join(os28.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
13829
|
+
}
|
|
13830
|
+
function projectLabel(projectPath) {
|
|
13831
|
+
return projectPath.replace(os28.homedir(), "~");
|
|
13832
|
+
}
|
|
13833
|
+
function parseHistoryLines(lines) {
|
|
13834
|
+
const entries = [];
|
|
13835
|
+
for (const line of lines) {
|
|
13836
|
+
if (!line.trim()) continue;
|
|
13837
|
+
try {
|
|
13838
|
+
const obj = JSON.parse(line);
|
|
13839
|
+
if (typeof obj["display"] === "string" && (typeof obj["timestamp"] === "string" || typeof obj["timestamp"] === "number") && typeof obj["project"] === "string" && typeof obj["sessionId"] === "string") {
|
|
13840
|
+
const ts = typeof obj["timestamp"] === "number" ? new Date(obj["timestamp"]).toISOString() : obj["timestamp"];
|
|
13841
|
+
entries.push({
|
|
13842
|
+
display: obj["display"],
|
|
13843
|
+
timestamp: ts,
|
|
13844
|
+
project: obj["project"],
|
|
13845
|
+
sessionId: obj["sessionId"]
|
|
13846
|
+
});
|
|
13847
|
+
}
|
|
13848
|
+
} catch {
|
|
13849
|
+
}
|
|
13850
|
+
}
|
|
13851
|
+
return entries;
|
|
13852
|
+
}
|
|
13853
|
+
function parseSessionLines(lines) {
|
|
13854
|
+
const toolCalls = [];
|
|
13855
|
+
let costUSD = 0;
|
|
13856
|
+
let hasSnapshot = false;
|
|
13857
|
+
const modifiedFiles = [];
|
|
13858
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
13859
|
+
for (const line of lines) {
|
|
13860
|
+
if (!line.trim()) continue;
|
|
13861
|
+
let entry;
|
|
13862
|
+
try {
|
|
13863
|
+
entry = JSON.parse(line);
|
|
13864
|
+
} catch {
|
|
13865
|
+
continue;
|
|
13866
|
+
}
|
|
13867
|
+
if (entry.type === "file-history-snapshot") {
|
|
13868
|
+
hasSnapshot = true;
|
|
13869
|
+
continue;
|
|
13870
|
+
}
|
|
13871
|
+
if (entry.type !== "assistant") continue;
|
|
13872
|
+
const usage = entry.message?.usage;
|
|
13873
|
+
const model = entry.message?.model;
|
|
13874
|
+
if (usage && model) {
|
|
13875
|
+
const p = modelPrice(model);
|
|
13876
|
+
if (p) {
|
|
13877
|
+
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;
|
|
13878
|
+
}
|
|
13879
|
+
}
|
|
13880
|
+
const content = entry.message?.content;
|
|
13881
|
+
if (!Array.isArray(content)) continue;
|
|
13882
|
+
for (const block of content) {
|
|
13883
|
+
if (block.type !== "tool_use") continue;
|
|
13884
|
+
const tool = block.name ?? "";
|
|
13885
|
+
const input = block.input ?? {};
|
|
13886
|
+
toolCalls.push({ tool, input, timestamp: entry.timestamp ?? "" });
|
|
13887
|
+
const toolLower = tool.toLowerCase();
|
|
13888
|
+
if (toolLower === "write" || toolLower === "edit" || toolLower === "notebookedit") {
|
|
13889
|
+
const fp = input.file_path ?? input.path;
|
|
13890
|
+
if (typeof fp === "string" && !seenFiles.has(fp)) {
|
|
13891
|
+
seenFiles.add(fp);
|
|
13892
|
+
modifiedFiles.push(fp);
|
|
13893
|
+
}
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13896
|
+
}
|
|
13897
|
+
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
13898
|
+
}
|
|
13899
|
+
function loadAuditEntries(auditPath) {
|
|
13900
|
+
const aPath = auditPath ?? path35.join(os28.homedir(), ".node9", "audit.log");
|
|
13901
|
+
let raw;
|
|
13902
|
+
try {
|
|
13903
|
+
raw = fs32.readFileSync(aPath, "utf-8");
|
|
13904
|
+
} catch {
|
|
13905
|
+
return [];
|
|
13906
|
+
}
|
|
13907
|
+
const entries = [];
|
|
13908
|
+
for (const line of raw.split("\n")) {
|
|
13909
|
+
if (!line.trim()) continue;
|
|
13910
|
+
try {
|
|
13911
|
+
const e = JSON.parse(line);
|
|
13912
|
+
if (!e.ts || !e.tool || !e.decision) continue;
|
|
13913
|
+
if (e.decision === "allow" || e.decision === "allowed") continue;
|
|
13914
|
+
entries.push(e);
|
|
13915
|
+
} catch {
|
|
13916
|
+
}
|
|
13917
|
+
}
|
|
13918
|
+
return entries;
|
|
13919
|
+
}
|
|
13920
|
+
function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
13921
|
+
const start = new Date(windowStart).getTime();
|
|
13922
|
+
const end = new Date(windowEnd).getTime();
|
|
13923
|
+
const result = [];
|
|
13924
|
+
for (const e of entries) {
|
|
13925
|
+
const t = new Date(e.ts).getTime();
|
|
13926
|
+
if (t < start || t > end) continue;
|
|
13927
|
+
result.push({
|
|
13928
|
+
tool: e.tool,
|
|
13929
|
+
args: e.args,
|
|
13930
|
+
argsHash: e.argsHash,
|
|
13931
|
+
timestamp: e.ts,
|
|
13932
|
+
decision: e.decision,
|
|
13933
|
+
checkedBy: e.checkedBy
|
|
13934
|
+
});
|
|
13935
|
+
}
|
|
13936
|
+
return result;
|
|
13937
|
+
}
|
|
13938
|
+
function buildSessions(days, historyPath) {
|
|
13939
|
+
const hPath = historyPath ?? path35.join(os28.homedir(), ".claude", "history.jsonl");
|
|
13940
|
+
let historyRaw;
|
|
13941
|
+
try {
|
|
13942
|
+
historyRaw = fs32.readFileSync(hPath, "utf-8");
|
|
13943
|
+
} catch {
|
|
13944
|
+
return [];
|
|
13945
|
+
}
|
|
13946
|
+
const cutoff = days !== null ? (() => {
|
|
13947
|
+
const d = /* @__PURE__ */ new Date();
|
|
13948
|
+
d.setDate(d.getDate() - days);
|
|
13949
|
+
d.setHours(0, 0, 0, 0);
|
|
13950
|
+
return d;
|
|
13951
|
+
})() : null;
|
|
13952
|
+
const entries = parseHistoryLines(historyRaw.split("\n"));
|
|
13953
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
13954
|
+
for (const e of entries) {
|
|
13955
|
+
if (cutoff && new Date(e.timestamp) < cutoff) continue;
|
|
13956
|
+
const existing = bySession.get(e.sessionId);
|
|
13957
|
+
if (!existing || e.timestamp < existing.timestamp) {
|
|
13958
|
+
bySession.set(e.sessionId, e);
|
|
13959
|
+
}
|
|
13960
|
+
}
|
|
13961
|
+
const allAuditEntries = loadAuditEntries();
|
|
13962
|
+
const summaries = [];
|
|
13963
|
+
for (const entry of bySession.values()) {
|
|
13964
|
+
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
13965
|
+
let sessionLines = [];
|
|
13966
|
+
try {
|
|
13967
|
+
sessionLines = fs32.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
13968
|
+
} catch {
|
|
13969
|
+
}
|
|
13970
|
+
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
13971
|
+
const windowStart = entry.timestamp;
|
|
13972
|
+
const lastToolTs = toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].timestamp : "";
|
|
13973
|
+
const windowEnd = new Date(
|
|
13974
|
+
Math.max(new Date(windowStart).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
13975
|
+
// 5 min buffer
|
|
13976
|
+
).toISOString();
|
|
13977
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
|
|
13978
|
+
summaries.push({
|
|
13979
|
+
sessionId: entry.sessionId,
|
|
13980
|
+
project: entry.project,
|
|
13981
|
+
projectLabel: projectLabel(entry.project),
|
|
13982
|
+
firstPrompt: entry.display,
|
|
13983
|
+
startTime: entry.timestamp,
|
|
13984
|
+
toolCalls,
|
|
13985
|
+
blockedCalls,
|
|
13986
|
+
costUSD,
|
|
13987
|
+
hasSnapshot,
|
|
13988
|
+
modifiedFiles
|
|
13989
|
+
});
|
|
13990
|
+
}
|
|
13991
|
+
summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
|
|
13992
|
+
return summaries;
|
|
13993
|
+
}
|
|
13994
|
+
function fmtCost3(usd) {
|
|
13995
|
+
if (usd === 0) return "";
|
|
13996
|
+
if (usd < 1e-3) return "< $0.001";
|
|
13997
|
+
if (usd < 1) return "$" + usd.toFixed(3);
|
|
13998
|
+
return "$" + usd.toFixed(2);
|
|
13999
|
+
}
|
|
14000
|
+
function fmtDate2(iso) {
|
|
14001
|
+
try {
|
|
14002
|
+
return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
14003
|
+
} catch {
|
|
14004
|
+
return iso.slice(0, 10);
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
function fmtTime(iso) {
|
|
14008
|
+
try {
|
|
14009
|
+
return new Date(iso).toLocaleTimeString("en-US", {
|
|
14010
|
+
hour: "2-digit",
|
|
14011
|
+
minute: "2-digit",
|
|
14012
|
+
hour12: false
|
|
14013
|
+
});
|
|
14014
|
+
} catch {
|
|
14015
|
+
return iso.slice(11, 16);
|
|
14016
|
+
}
|
|
14017
|
+
}
|
|
14018
|
+
function fmtDateTime(iso) {
|
|
14019
|
+
try {
|
|
14020
|
+
return new Date(iso).toLocaleString("en-US", {
|
|
14021
|
+
month: "short",
|
|
14022
|
+
day: "numeric",
|
|
14023
|
+
year: "numeric",
|
|
14024
|
+
hour: "2-digit",
|
|
14025
|
+
minute: "2-digit",
|
|
14026
|
+
hour12: false
|
|
14027
|
+
});
|
|
14028
|
+
} catch {
|
|
14029
|
+
return iso;
|
|
14030
|
+
}
|
|
14031
|
+
}
|
|
14032
|
+
function truncate(s, max) {
|
|
14033
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
14034
|
+
}
|
|
14035
|
+
function toolInputSummary(tool, input) {
|
|
14036
|
+
const fp = input.file_path ?? input.path;
|
|
14037
|
+
if (typeof fp === "string") return fp;
|
|
14038
|
+
const cmd = input.command;
|
|
14039
|
+
if (typeof cmd === "string") return truncate(cmd.replace(/\s+/g, " "), 60);
|
|
14040
|
+
const q = input.query ?? input.url;
|
|
14041
|
+
if (typeof q === "string") return truncate(String(q), 60);
|
|
14042
|
+
return "";
|
|
14043
|
+
}
|
|
14044
|
+
function toolColor(tool) {
|
|
14045
|
+
const t = tool.toLowerCase();
|
|
14046
|
+
if (t === "bash" || t === "execute_bash") return chalk22.red;
|
|
14047
|
+
if (t === "write") return chalk22.green;
|
|
14048
|
+
if (t === "edit" || t === "notebookedit") return chalk22.yellow;
|
|
14049
|
+
if (t === "read") return chalk22.cyan;
|
|
14050
|
+
return chalk22.gray;
|
|
14051
|
+
}
|
|
14052
|
+
function barStr2(value, max, width) {
|
|
14053
|
+
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
14054
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
14055
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
14056
|
+
}
|
|
14057
|
+
function colorBar2(value, max, width) {
|
|
14058
|
+
const s = barStr2(value, max, width);
|
|
14059
|
+
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
14060
|
+
return chalk22.cyan(s.slice(0, filled)) + chalk22.dim(s.slice(filled));
|
|
14061
|
+
}
|
|
14062
|
+
function renderSummary(summaries) {
|
|
14063
|
+
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
14064
|
+
const totalCost = summaries.reduce((n, s) => n + s.costUSD, 0);
|
|
14065
|
+
const totalFiles = summaries.reduce((n, s) => n + s.modifiedFiles.length, 0);
|
|
14066
|
+
const totalBlocked = summaries.reduce((n, s) => n + s.blockedCalls.length, 0);
|
|
14067
|
+
const snapshots = summaries.filter((s) => s.hasSnapshot).length;
|
|
14068
|
+
const avgCost = summaries.length > 0 ? totalCost / summaries.length : 0;
|
|
14069
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
14070
|
+
for (const s of summaries) {
|
|
14071
|
+
for (const tc of s.toolCalls) {
|
|
14072
|
+
const key = tc.tool.toLowerCase();
|
|
14073
|
+
toolCounts.set(key, (toolCounts.get(key) ?? 0) + 1);
|
|
14074
|
+
}
|
|
14075
|
+
}
|
|
14076
|
+
const groups = { Bash: 0, Read: 0, Write: 0, Edit: 0, Other: 0 };
|
|
14077
|
+
for (const [tool, count] of toolCounts) {
|
|
14078
|
+
if (tool === "bash" || tool === "execute_bash") groups["Bash"] += count;
|
|
14079
|
+
else if (tool === "read") groups["Read"] += count;
|
|
14080
|
+
else if (tool === "write") groups["Write"] += count;
|
|
14081
|
+
else if (tool === "edit" || tool === "notebookedit") groups["Edit"] += count;
|
|
14082
|
+
else groups["Other"] += count;
|
|
14083
|
+
}
|
|
14084
|
+
const projCosts = /* @__PURE__ */ new Map();
|
|
14085
|
+
for (const s of summaries) {
|
|
14086
|
+
projCosts.set(s.projectLabel, (projCosts.get(s.projectLabel) ?? 0) + s.costUSD);
|
|
14087
|
+
}
|
|
14088
|
+
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
14089
|
+
const W = 20;
|
|
14090
|
+
console.log(chalk22.dim(" " + "\u2500".repeat(70)));
|
|
14091
|
+
console.log(
|
|
14092
|
+
" " + 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") : "")
|
|
14093
|
+
);
|
|
14094
|
+
console.log(
|
|
14095
|
+
" " + chalk22.dim("avg ") + chalk22.white(fmtCost3(avgCost).padEnd(10)) + chalk22.dim("/session ") + chalk22.green(String(snapshots)) + chalk22.dim(` of ${summaries.length} sessions had snapshots`)
|
|
14096
|
+
);
|
|
14097
|
+
console.log("");
|
|
14098
|
+
console.log(" " + chalk22.dim("Tool breakdown:"));
|
|
14099
|
+
const maxGroup = Math.max(...Object.values(groups));
|
|
14100
|
+
for (const [label, count] of Object.entries(groups)) {
|
|
14101
|
+
if (count === 0) continue;
|
|
14102
|
+
const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
14103
|
+
console.log(
|
|
14104
|
+
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct2)}%)`)
|
|
14105
|
+
);
|
|
14106
|
+
}
|
|
14107
|
+
console.log("");
|
|
14108
|
+
if (topProjects.length > 1) {
|
|
14109
|
+
console.log(" " + chalk22.dim("Cost by project:"));
|
|
14110
|
+
const maxProjCost = topProjects[0][1];
|
|
14111
|
+
for (const [proj, cost] of topProjects) {
|
|
14112
|
+
console.log(
|
|
14113
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk22.yellow(fmtCost3(cost))
|
|
14114
|
+
);
|
|
14115
|
+
}
|
|
14116
|
+
console.log("");
|
|
14117
|
+
}
|
|
14118
|
+
console.log(chalk22.dim(" " + "\u2500".repeat(70)));
|
|
14119
|
+
console.log("");
|
|
14120
|
+
}
|
|
14121
|
+
function renderList(summaries, totalCost) {
|
|
14122
|
+
if (summaries.length === 0) {
|
|
14123
|
+
console.log(chalk22.yellow(" No sessions found in the requested range.\n"));
|
|
14124
|
+
return;
|
|
14125
|
+
}
|
|
14126
|
+
const totalLabel = totalCost > 0 ? chalk22.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
14127
|
+
console.log(
|
|
14128
|
+
" " + chalk22.white(String(summaries.length)) + chalk22.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
14129
|
+
);
|
|
14130
|
+
console.log("");
|
|
14131
|
+
let lastGroup = "";
|
|
14132
|
+
for (const s of summaries) {
|
|
14133
|
+
const group = fmtDate2(s.startTime) + " " + s.projectLabel;
|
|
14134
|
+
if (group !== lastGroup) {
|
|
14135
|
+
console.log(
|
|
14136
|
+
chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
|
|
14137
|
+
);
|
|
14138
|
+
lastGroup = group;
|
|
14139
|
+
}
|
|
14140
|
+
const timeStr = chalk22.dim(fmtTime(s.startTime));
|
|
14141
|
+
const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
14142
|
+
const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
|
|
14143
|
+
const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
14144
|
+
const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
14145
|
+
const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
|
|
14146
|
+
const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
|
|
14147
|
+
console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
|
|
14148
|
+
}
|
|
14149
|
+
console.log("");
|
|
14150
|
+
console.log(
|
|
14151
|
+
chalk22.dim(" Run") + " " + chalk22.cyan("node9 sessions --detail <session-id>") + chalk22.dim(" for full tool trace.")
|
|
14152
|
+
);
|
|
14153
|
+
console.log("");
|
|
14154
|
+
}
|
|
14155
|
+
function renderDetail(s) {
|
|
14156
|
+
console.log("");
|
|
14157
|
+
console.log(chalk22.bold(" Session ") + chalk22.dim(s.sessionId));
|
|
14158
|
+
console.log(
|
|
14159
|
+
chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
14160
|
+
);
|
|
14161
|
+
console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
|
|
14162
|
+
console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
|
|
14163
|
+
if (s.costUSD > 0)
|
|
14164
|
+
console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
|
|
14165
|
+
console.log(
|
|
14166
|
+
chalk22.bold(" Snapshot ") + (s.hasSnapshot ? chalk22.green("\u2713 taken") : chalk22.dim("none"))
|
|
14167
|
+
);
|
|
14168
|
+
console.log("");
|
|
14169
|
+
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
14170
|
+
console.log(chalk22.dim(" No tool calls recorded.\n"));
|
|
14171
|
+
return;
|
|
14172
|
+
}
|
|
14173
|
+
const timeline = [
|
|
14174
|
+
...s.toolCalls.map((tc) => ({ kind: "tool", tc })),
|
|
14175
|
+
...s.blockedCalls.map((bc) => ({ kind: "blocked", bc }))
|
|
14176
|
+
].sort((a, b) => {
|
|
14177
|
+
const ta = a.kind === "tool" ? a.tc.timestamp : a.bc.timestamp;
|
|
14178
|
+
const tb = b.kind === "tool" ? b.tc.timestamp : b.bc.timestamp;
|
|
14179
|
+
return ta < tb ? -1 : ta > tb ? 1 : 0;
|
|
14180
|
+
});
|
|
14181
|
+
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
14182
|
+
if (s.blockedCalls.length > 0)
|
|
14183
|
+
headerParts.push(chalk22.red(`${s.blockedCalls.length} blocked by node9`));
|
|
14184
|
+
console.log(chalk22.bold(" " + headerParts.join(" \xB7 ")));
|
|
14185
|
+
console.log("");
|
|
14186
|
+
for (const entry of timeline) {
|
|
14187
|
+
if (entry.kind === "tool") {
|
|
14188
|
+
const tc = entry.tc;
|
|
14189
|
+
const colorFn = toolColor(tc.tool);
|
|
14190
|
+
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
14191
|
+
const detail = chalk22.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
14192
|
+
const ts = tc.timestamp ? chalk22.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
14193
|
+
console.log(` ${ts}${toolPad} ${detail}`);
|
|
14194
|
+
} else {
|
|
14195
|
+
const bc = entry.bc;
|
|
14196
|
+
const ts = bc.timestamp ? chalk22.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
14197
|
+
const label = chalk22.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
14198
|
+
const toolName = chalk22.red(bc.tool.padEnd(10));
|
|
14199
|
+
const argsSummary = bc.args ? chalk22.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk22.dim("[args not logged]");
|
|
14200
|
+
const reason = bc.checkedBy ? chalk22.dim(" \u2190 " + bc.checkedBy) : "";
|
|
14201
|
+
console.log(` ${ts}${label} ${toolName} ${argsSummary}${reason}`);
|
|
14202
|
+
}
|
|
14203
|
+
}
|
|
14204
|
+
console.log("");
|
|
14205
|
+
if (s.modifiedFiles.length > 0) {
|
|
14206
|
+
console.log(chalk22.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
14207
|
+
for (const f of s.modifiedFiles) {
|
|
14208
|
+
console.log(" " + chalk22.yellow(f));
|
|
14209
|
+
}
|
|
14210
|
+
console.log("");
|
|
14211
|
+
}
|
|
14212
|
+
}
|
|
14213
|
+
function registerSessionsCommand(program2) {
|
|
14214
|
+
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) => {
|
|
14215
|
+
console.log("");
|
|
14216
|
+
console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
|
|
14217
|
+
console.log("");
|
|
14218
|
+
const historyPath = path35.join(os28.homedir(), ".claude", "history.jsonl");
|
|
14219
|
+
if (!fs32.existsSync(historyPath)) {
|
|
14220
|
+
console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
|
|
14221
|
+
console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
14222
|
+
return;
|
|
14223
|
+
}
|
|
14224
|
+
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
14225
|
+
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
14226
|
+
console.log(chalk22.dim(" " + rangeLabel));
|
|
14227
|
+
console.log("");
|
|
14228
|
+
process.stdout.write(chalk22.dim(" Loading\u2026"));
|
|
14229
|
+
const summaries = buildSessions(days);
|
|
14230
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
14231
|
+
if (options.detail) {
|
|
14232
|
+
const target = summaries.find(
|
|
14233
|
+
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
14234
|
+
);
|
|
14235
|
+
if (!target) {
|
|
14236
|
+
console.log(chalk22.red(` Session not found: ${options.detail}`));
|
|
14237
|
+
console.log(chalk22.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
14238
|
+
return;
|
|
14239
|
+
}
|
|
14240
|
+
renderDetail(target);
|
|
14241
|
+
return;
|
|
14242
|
+
}
|
|
14243
|
+
const totalCost = summaries.reduce((sum, s) => sum + s.costUSD, 0);
|
|
14244
|
+
if (summaries.length > 0) renderSummary(summaries);
|
|
14245
|
+
renderList(summaries, totalCost);
|
|
14246
|
+
});
|
|
14247
|
+
}
|
|
14248
|
+
|
|
12455
14249
|
// src/cli.ts
|
|
12456
14250
|
var { version } = JSON.parse(
|
|
12457
|
-
|
|
14251
|
+
fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
|
|
12458
14252
|
);
|
|
12459
14253
|
var program = new Command();
|
|
12460
14254
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
12461
14255
|
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) => {
|
|
12462
|
-
const
|
|
12463
|
-
const credPath =
|
|
12464
|
-
if (!
|
|
12465
|
-
|
|
14256
|
+
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
14257
|
+
const credPath = path38.join(os31.homedir(), ".node9", "credentials.json");
|
|
14258
|
+
if (!fs35.existsSync(path38.dirname(credPath)))
|
|
14259
|
+
fs35.mkdirSync(path38.dirname(credPath), { recursive: true });
|
|
12466
14260
|
const profileName = options.profile || "default";
|
|
12467
14261
|
let existingCreds = {};
|
|
12468
14262
|
try {
|
|
12469
|
-
if (
|
|
12470
|
-
const raw = JSON.parse(
|
|
14263
|
+
if (fs35.existsSync(credPath)) {
|
|
14264
|
+
const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
|
|
12471
14265
|
if (raw.apiKey) {
|
|
12472
14266
|
existingCreds = {
|
|
12473
|
-
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl ||
|
|
14267
|
+
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
12474
14268
|
};
|
|
12475
14269
|
} else {
|
|
12476
14270
|
existingCreds = raw;
|
|
@@ -12478,14 +14272,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
12478
14272
|
}
|
|
12479
14273
|
} catch {
|
|
12480
14274
|
}
|
|
12481
|
-
existingCreds[profileName] = { apiKey, apiUrl:
|
|
12482
|
-
|
|
14275
|
+
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
14276
|
+
fs35.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
12483
14277
|
if (profileName === "default") {
|
|
12484
|
-
const configPath =
|
|
14278
|
+
const configPath = path38.join(os31.homedir(), ".node9", "config.json");
|
|
12485
14279
|
let config = {};
|
|
12486
14280
|
try {
|
|
12487
|
-
if (
|
|
12488
|
-
config = JSON.parse(
|
|
14281
|
+
if (fs35.existsSync(configPath))
|
|
14282
|
+
config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
12489
14283
|
} catch {
|
|
12490
14284
|
}
|
|
12491
14285
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -12500,39 +14294,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
12500
14294
|
approvers.cloud = false;
|
|
12501
14295
|
}
|
|
12502
14296
|
s.approvers = approvers;
|
|
12503
|
-
if (!
|
|
12504
|
-
|
|
12505
|
-
|
|
14297
|
+
if (!fs35.existsSync(path38.dirname(configPath)))
|
|
14298
|
+
fs35.mkdirSync(path38.dirname(configPath), { recursive: true });
|
|
14299
|
+
fs35.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
12506
14300
|
}
|
|
12507
14301
|
if (options.profile && profileName !== "default") {
|
|
12508
|
-
console.log(
|
|
12509
|
-
console.log(
|
|
14302
|
+
console.log(chalk24.green(`\u2705 Profile "${profileName}" saved`));
|
|
14303
|
+
console.log(chalk24.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
12510
14304
|
} else if (options.local) {
|
|
12511
|
-
console.log(
|
|
12512
|
-
console.log(
|
|
14305
|
+
console.log(chalk24.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
14306
|
+
console.log(chalk24.gray(` All decisions stay on this machine.`));
|
|
12513
14307
|
} else {
|
|
12514
|
-
console.log(
|
|
12515
|
-
console.log(
|
|
14308
|
+
console.log(chalk24.green(`\u2705 Logged in \u2014 agent mode`));
|
|
14309
|
+
console.log(chalk24.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
12516
14310
|
}
|
|
12517
14311
|
});
|
|
12518
|
-
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) => {
|
|
14312
|
+
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) => {
|
|
12519
14313
|
if (target === "gemini") return await setupGemini();
|
|
12520
14314
|
if (target === "claude") return await setupClaude();
|
|
12521
14315
|
if (target === "cursor") return await setupCursor();
|
|
14316
|
+
if (target === "windsurf") return await setupWindsurf();
|
|
14317
|
+
if (target === "vscode") return await setupVSCode();
|
|
12522
14318
|
if (target === "hud") return setupHud();
|
|
12523
|
-
console.error(
|
|
14319
|
+
console.error(
|
|
14320
|
+
chalk24.red(
|
|
14321
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14322
|
+
)
|
|
14323
|
+
);
|
|
12524
14324
|
process.exit(1);
|
|
12525
14325
|
});
|
|
12526
|
-
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) => {
|
|
14326
|
+
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) => {
|
|
12527
14327
|
if (!target) {
|
|
12528
|
-
console.log(
|
|
12529
|
-
console.log(" Usage: " +
|
|
14328
|
+
console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
14329
|
+
console.log(" Usage: " + chalk24.white("node9 setup <target>") + "\n");
|
|
12530
14330
|
console.log(" Targets:");
|
|
12531
|
-
console.log(" " +
|
|
12532
|
-
console.log(" " +
|
|
12533
|
-
console.log(" " +
|
|
14331
|
+
console.log(" " + chalk24.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
14332
|
+
console.log(" " + chalk24.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
14333
|
+
console.log(" " + chalk24.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
14334
|
+
console.log(" " + chalk24.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
14335
|
+
console.log(" " + chalk24.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
12534
14336
|
process.stdout.write(
|
|
12535
|
-
" " +
|
|
14337
|
+
" " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
12536
14338
|
);
|
|
12537
14339
|
console.log("");
|
|
12538
14340
|
return;
|
|
@@ -12541,93 +14343,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
12541
14343
|
if (t === "gemini") return await setupGemini();
|
|
12542
14344
|
if (t === "claude") return await setupClaude();
|
|
12543
14345
|
if (t === "cursor") return await setupCursor();
|
|
14346
|
+
if (t === "windsurf") return await setupWindsurf();
|
|
14347
|
+
if (t === "vscode") return await setupVSCode();
|
|
12544
14348
|
if (t === "hud") return setupHud();
|
|
12545
|
-
console.error(
|
|
14349
|
+
console.error(
|
|
14350
|
+
chalk24.red(
|
|
14351
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14352
|
+
)
|
|
14353
|
+
);
|
|
12546
14354
|
process.exit(1);
|
|
12547
14355
|
});
|
|
12548
|
-
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument(
|
|
14356
|
+
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument(
|
|
14357
|
+
"<target>",
|
|
14358
|
+
"The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
|
|
14359
|
+
).action((target) => {
|
|
12549
14360
|
let fn;
|
|
12550
14361
|
if (target === "claude") fn = teardownClaude;
|
|
12551
14362
|
else if (target === "gemini") fn = teardownGemini;
|
|
12552
14363
|
else if (target === "cursor") fn = teardownCursor;
|
|
14364
|
+
else if (target === "windsurf") fn = teardownWindsurf;
|
|
14365
|
+
else if (target === "vscode") fn = teardownVSCode;
|
|
12553
14366
|
else if (target === "hud") fn = teardownHud;
|
|
12554
14367
|
else {
|
|
12555
14368
|
console.error(
|
|
12556
|
-
|
|
14369
|
+
chalk24.red(
|
|
14370
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14371
|
+
)
|
|
12557
14372
|
);
|
|
12558
14373
|
process.exit(1);
|
|
12559
14374
|
}
|
|
12560
|
-
console.log(
|
|
14375
|
+
console.log(chalk24.cyan(`
|
|
12561
14376
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
12562
14377
|
`));
|
|
12563
14378
|
try {
|
|
12564
14379
|
fn();
|
|
12565
14380
|
} catch (err2) {
|
|
12566
|
-
console.error(
|
|
14381
|
+
console.error(chalk24.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12567
14382
|
process.exit(1);
|
|
12568
14383
|
}
|
|
12569
|
-
console.log(
|
|
14384
|
+
console.log(chalk24.gray("\n Restart the agent for changes to take effect."));
|
|
12570
14385
|
});
|
|
12571
14386
|
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) => {
|
|
12572
|
-
console.log(
|
|
12573
|
-
console.log(
|
|
14387
|
+
console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
14388
|
+
console.log(chalk24.bold("Stopping daemon..."));
|
|
12574
14389
|
try {
|
|
12575
14390
|
stopDaemon();
|
|
12576
|
-
console.log(
|
|
14391
|
+
console.log(chalk24.green(" \u2705 Daemon stopped"));
|
|
12577
14392
|
} catch {
|
|
12578
|
-
console.log(
|
|
14393
|
+
console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
|
|
12579
14394
|
}
|
|
12580
|
-
console.log(
|
|
14395
|
+
console.log(chalk24.bold("\nRemoving hooks..."));
|
|
12581
14396
|
let teardownFailed = false;
|
|
12582
14397
|
for (const [label, fn] of [
|
|
12583
14398
|
["Claude", teardownClaude],
|
|
12584
14399
|
["Gemini", teardownGemini],
|
|
12585
|
-
["Cursor", teardownCursor]
|
|
14400
|
+
["Cursor", teardownCursor],
|
|
14401
|
+
["Windsurf", teardownWindsurf],
|
|
14402
|
+
["VSCode", teardownVSCode]
|
|
12586
14403
|
]) {
|
|
12587
14404
|
try {
|
|
12588
14405
|
fn();
|
|
12589
14406
|
} catch (err2) {
|
|
12590
14407
|
teardownFailed = true;
|
|
12591
14408
|
console.error(
|
|
12592
|
-
|
|
14409
|
+
chalk24.red(
|
|
12593
14410
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
12594
14411
|
)
|
|
12595
14412
|
);
|
|
12596
14413
|
}
|
|
12597
14414
|
}
|
|
12598
14415
|
if (options.purge) {
|
|
12599
|
-
const node9Dir =
|
|
12600
|
-
if (
|
|
14416
|
+
const node9Dir = path38.join(os31.homedir(), ".node9");
|
|
14417
|
+
if (fs35.existsSync(node9Dir)) {
|
|
12601
14418
|
const confirmed = await confirm2({
|
|
12602
14419
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
12603
14420
|
default: false
|
|
12604
14421
|
});
|
|
12605
14422
|
if (confirmed) {
|
|
12606
|
-
|
|
12607
|
-
if (
|
|
14423
|
+
fs35.rmSync(node9Dir, { recursive: true });
|
|
14424
|
+
if (fs35.existsSync(node9Dir)) {
|
|
12608
14425
|
console.error(
|
|
12609
|
-
|
|
14426
|
+
chalk24.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
12610
14427
|
);
|
|
12611
14428
|
} else {
|
|
12612
|
-
console.log(
|
|
14429
|
+
console.log(chalk24.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
12613
14430
|
}
|
|
12614
14431
|
} else {
|
|
12615
|
-
console.log(
|
|
14432
|
+
console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
12616
14433
|
}
|
|
12617
14434
|
} else {
|
|
12618
|
-
console.log(
|
|
14435
|
+
console.log(chalk24.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
12619
14436
|
}
|
|
12620
14437
|
} else {
|
|
12621
14438
|
console.log(
|
|
12622
|
-
|
|
14439
|
+
chalk24.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
12623
14440
|
);
|
|
12624
14441
|
}
|
|
12625
14442
|
if (teardownFailed) {
|
|
12626
|
-
console.error(
|
|
14443
|
+
console.error(chalk24.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
12627
14444
|
process.exit(1);
|
|
12628
14445
|
}
|
|
12629
|
-
console.log(
|
|
12630
|
-
console.log(
|
|
14446
|
+
console.log(chalk24.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
14447
|
+
console.log(chalk24.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
12631
14448
|
});
|
|
12632
14449
|
registerDoctorCommand(program, version);
|
|
12633
14450
|
program.command("explain").description(
|
|
@@ -12640,7 +14457,7 @@ program.command("explain").description(
|
|
|
12640
14457
|
try {
|
|
12641
14458
|
args = JSON.parse(trimmed);
|
|
12642
14459
|
} catch {
|
|
12643
|
-
console.error(
|
|
14460
|
+
console.error(chalk24.red(`
|
|
12644
14461
|
\u274C Invalid JSON: ${trimmed}
|
|
12645
14462
|
`));
|
|
12646
14463
|
process.exit(1);
|
|
@@ -12651,54 +14468,54 @@ program.command("explain").description(
|
|
|
12651
14468
|
}
|
|
12652
14469
|
const result = await explainPolicy(tool, args);
|
|
12653
14470
|
console.log("");
|
|
12654
|
-
console.log(
|
|
14471
|
+
console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
12655
14472
|
console.log("");
|
|
12656
|
-
console.log(` ${
|
|
14473
|
+
console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
|
|
12657
14474
|
if (argsRaw) {
|
|
12658
|
-
const
|
|
12659
|
-
console.log(` ${
|
|
14475
|
+
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
14476
|
+
console.log(` ${chalk24.bold("Input:")} ${chalk24.gray(preview2)}`);
|
|
12660
14477
|
}
|
|
12661
14478
|
console.log("");
|
|
12662
|
-
console.log(
|
|
14479
|
+
console.log(chalk24.bold("Config Sources (Waterfall):"));
|
|
12663
14480
|
for (const tier of result.waterfall) {
|
|
12664
|
-
const
|
|
14481
|
+
const num3 = chalk24.gray(` ${tier.tier}.`);
|
|
12665
14482
|
const label = tier.label.padEnd(16);
|
|
12666
14483
|
let statusStr;
|
|
12667
14484
|
if (tier.tier === 1) {
|
|
12668
|
-
statusStr =
|
|
14485
|
+
statusStr = chalk24.gray(tier.note ?? "");
|
|
12669
14486
|
} else if (tier.status === "active") {
|
|
12670
|
-
const loc = tier.path ?
|
|
12671
|
-
const note = tier.note ?
|
|
12672
|
-
statusStr =
|
|
14487
|
+
const loc = tier.path ? chalk24.gray(tier.path) : "";
|
|
14488
|
+
const note = tier.note ? chalk24.gray(`(${tier.note})`) : "";
|
|
14489
|
+
statusStr = chalk24.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
12673
14490
|
} else {
|
|
12674
|
-
statusStr =
|
|
14491
|
+
statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
|
|
12675
14492
|
}
|
|
12676
|
-
console.log(`${
|
|
14493
|
+
console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
|
|
12677
14494
|
}
|
|
12678
14495
|
console.log("");
|
|
12679
|
-
console.log(
|
|
14496
|
+
console.log(chalk24.bold("Policy Evaluation:"));
|
|
12680
14497
|
for (const step of result.steps) {
|
|
12681
14498
|
const isFinal = step.isFinal;
|
|
12682
14499
|
let icon;
|
|
12683
|
-
if (step.outcome === "allow") icon =
|
|
12684
|
-
else if (step.outcome === "review") icon =
|
|
12685
|
-
else if (step.outcome === "skip") icon =
|
|
12686
|
-
else icon =
|
|
14500
|
+
if (step.outcome === "allow") icon = chalk24.green(" \u2705");
|
|
14501
|
+
else if (step.outcome === "review") icon = chalk24.red(" \u{1F534}");
|
|
14502
|
+
else if (step.outcome === "skip") icon = chalk24.gray(" \u2500 ");
|
|
14503
|
+
else icon = chalk24.gray(" \u25CB ");
|
|
12687
14504
|
const name = step.name.padEnd(18);
|
|
12688
|
-
const nameStr = isFinal ?
|
|
12689
|
-
const detail = isFinal ?
|
|
12690
|
-
const arrow = isFinal ?
|
|
14505
|
+
const nameStr = isFinal ? chalk24.white.bold(name) : chalk24.white(name);
|
|
14506
|
+
const detail = isFinal ? chalk24.white(step.detail) : chalk24.gray(step.detail);
|
|
14507
|
+
const arrow = isFinal ? chalk24.yellow(" \u2190 STOP") : "";
|
|
12691
14508
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
12692
14509
|
}
|
|
12693
14510
|
console.log("");
|
|
12694
14511
|
if (result.decision === "allow") {
|
|
12695
|
-
console.log(
|
|
14512
|
+
console.log(chalk24.green.bold(" Decision: \u2705 ALLOW") + chalk24.gray(" \u2014 no approval needed"));
|
|
12696
14513
|
} else {
|
|
12697
14514
|
console.log(
|
|
12698
|
-
|
|
14515
|
+
chalk24.red.bold(" Decision: \u{1F534} REVIEW") + chalk24.gray(" \u2014 human approval required")
|
|
12699
14516
|
);
|
|
12700
14517
|
if (result.blockedByLabel) {
|
|
12701
|
-
console.log(
|
|
14518
|
+
console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
|
|
12702
14519
|
}
|
|
12703
14520
|
}
|
|
12704
14521
|
console.log("");
|
|
@@ -12713,7 +14530,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
12713
14530
|
try {
|
|
12714
14531
|
await startTail2(options);
|
|
12715
14532
|
} catch (err2) {
|
|
12716
|
-
console.error(
|
|
14533
|
+
console.error(chalk24.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12717
14534
|
process.exit(1);
|
|
12718
14535
|
}
|
|
12719
14536
|
});
|
|
@@ -12745,14 +14562,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
12745
14562
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
12746
14563
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
12747
14564
|
if (subcommand === "debug") {
|
|
12748
|
-
const flagFile =
|
|
14565
|
+
const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
|
|
12749
14566
|
if (state === "on") {
|
|
12750
|
-
|
|
12751
|
-
|
|
14567
|
+
fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
|
|
14568
|
+
fs35.writeFileSync(flagFile, "");
|
|
12752
14569
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
12753
14570
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
12754
14571
|
} else if (state === "off") {
|
|
12755
|
-
if (
|
|
14572
|
+
if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
|
|
12756
14573
|
console.log("HUD debug logging disabled.");
|
|
12757
14574
|
} else {
|
|
12758
14575
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12767,7 +14584,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12767
14584
|
const ms = parseDuration(options.duration);
|
|
12768
14585
|
if (ms === null) {
|
|
12769
14586
|
console.error(
|
|
12770
|
-
|
|
14587
|
+
chalk24.red(`
|
|
12771
14588
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12772
14589
|
`)
|
|
12773
14590
|
);
|
|
@@ -12775,20 +14592,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12775
14592
|
}
|
|
12776
14593
|
pauseNode9(ms, options.duration);
|
|
12777
14594
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12778
|
-
console.log(
|
|
14595
|
+
console.log(chalk24.yellow(`
|
|
12779
14596
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12780
|
-
console.log(
|
|
12781
|
-
console.log(
|
|
14597
|
+
console.log(chalk24.gray(` All tool calls will be allowed without review.`));
|
|
14598
|
+
console.log(chalk24.gray(` Run "node9 resume" to re-enable early.
|
|
12782
14599
|
`));
|
|
12783
14600
|
});
|
|
12784
14601
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12785
14602
|
const { paused } = checkPause();
|
|
12786
14603
|
if (!paused) {
|
|
12787
|
-
console.log(
|
|
14604
|
+
console.log(chalk24.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12788
14605
|
return;
|
|
12789
14606
|
}
|
|
12790
14607
|
resumeNode9();
|
|
12791
|
-
console.log(
|
|
14608
|
+
console.log(chalk24.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12792
14609
|
});
|
|
12793
14610
|
var HOOK_BASED_AGENTS = {
|
|
12794
14611
|
claude: "claude",
|
|
@@ -12801,15 +14618,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12801
14618
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12802
14619
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12803
14620
|
console.error(
|
|
12804
|
-
|
|
14621
|
+
chalk24.yellow(`
|
|
12805
14622
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12806
14623
|
);
|
|
12807
|
-
console.error(
|
|
14624
|
+
console.error(chalk24.white(`
|
|
12808
14625
|
"${target}" uses its own hook system. Use:`));
|
|
12809
14626
|
console.error(
|
|
12810
|
-
|
|
14627
|
+
chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
|
|
12811
14628
|
);
|
|
12812
|
-
console.error(
|
|
14629
|
+
console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
|
|
12813
14630
|
process.exit(1);
|
|
12814
14631
|
}
|
|
12815
14632
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12826,7 +14643,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12826
14643
|
}
|
|
12827
14644
|
);
|
|
12828
14645
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12829
|
-
console.error(
|
|
14646
|
+
console.error(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12830
14647
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12831
14648
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12832
14649
|
}
|
|
@@ -12839,12 +14656,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12839
14656
|
}
|
|
12840
14657
|
if (!result.approved) {
|
|
12841
14658
|
console.error(
|
|
12842
|
-
|
|
14659
|
+
chalk24.red(`
|
|
12843
14660
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12844
14661
|
);
|
|
12845
14662
|
process.exit(1);
|
|
12846
14663
|
}
|
|
12847
|
-
console.error(
|
|
14664
|
+
console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12848
14665
|
await runProxy(fullCommand);
|
|
12849
14666
|
} else {
|
|
12850
14667
|
program.help();
|
|
@@ -12854,14 +14671,18 @@ registerUndoCommand(program);
|
|
|
12854
14671
|
registerShieldCommand(program);
|
|
12855
14672
|
registerConfigShowCommand(program);
|
|
12856
14673
|
registerTrustCommand(program);
|
|
14674
|
+
registerSyncCommand(program);
|
|
14675
|
+
registerAgentsCommand(program);
|
|
14676
|
+
registerScanCommand(program);
|
|
14677
|
+
registerSessionsCommand(program);
|
|
12857
14678
|
if (process.argv[2] !== "daemon") {
|
|
12858
14679
|
process.on("unhandledRejection", (reason) => {
|
|
12859
14680
|
const isCheckHook = process.argv[2] === "check";
|
|
12860
14681
|
if (isCheckHook) {
|
|
12861
14682
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12862
|
-
const logPath =
|
|
14683
|
+
const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
|
|
12863
14684
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12864
|
-
|
|
14685
|
+
fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12865
14686
|
`);
|
|
12866
14687
|
}
|
|
12867
14688
|
process.exit(0);
|