@node9/proxy 1.10.2 → 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 +2284 -499
- package/dist/cli.mjs +2242 -456
- package/dist/index.js +22 -3
- package/dist/index.mjs +22 -3
- 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
|
}
|
|
@@ -2452,8 +2471,8 @@ function isDaemonRunning() {
|
|
|
2452
2471
|
if (port !== DAEMON_PORT) {
|
|
2453
2472
|
return false;
|
|
2454
2473
|
}
|
|
2455
|
-
const
|
|
2456
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid >
|
|
2474
|
+
const MAX_PID2 = 4194304;
|
|
2475
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
|
|
2457
2476
|
return false;
|
|
2458
2477
|
}
|
|
2459
2478
|
try {
|
|
@@ -6243,15 +6262,152 @@ var init_costSync = __esm({
|
|
|
6243
6262
|
}
|
|
6244
6263
|
});
|
|
6245
6264
|
|
|
6246
|
-
// src/daemon/
|
|
6247
|
-
import http from "http";
|
|
6265
|
+
// src/daemon/sync.ts
|
|
6248
6266
|
import fs17 from "fs";
|
|
6267
|
+
import https from "https";
|
|
6268
|
+
import os15 from "os";
|
|
6249
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";
|
|
6250
6405
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6251
6406
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6252
6407
|
import chalk2 from "chalk";
|
|
6253
6408
|
function startDaemon() {
|
|
6254
6409
|
startCostSync();
|
|
6410
|
+
startCloudSync();
|
|
6255
6411
|
loadInsightCounts();
|
|
6256
6412
|
const csrfToken = randomUUID4();
|
|
6257
6413
|
const internalToken = randomUUID4();
|
|
@@ -6267,7 +6423,7 @@ function startDaemon() {
|
|
|
6267
6423
|
idleTimer = setTimeout(() => {
|
|
6268
6424
|
if (autoStarted) {
|
|
6269
6425
|
try {
|
|
6270
|
-
|
|
6426
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6271
6427
|
} catch {
|
|
6272
6428
|
}
|
|
6273
6429
|
}
|
|
@@ -6430,7 +6586,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6430
6586
|
status: "pending"
|
|
6431
6587
|
});
|
|
6432
6588
|
}
|
|
6433
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6589
|
+
const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
|
|
6434
6590
|
const projectConfig = getConfig(projectCwd);
|
|
6435
6591
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6436
6592
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6820,8 +6976,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6820
6976
|
const body = await readBody(req);
|
|
6821
6977
|
const data = body ? JSON.parse(body) : {};
|
|
6822
6978
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6823
|
-
const node9Dir =
|
|
6824
|
-
if (!
|
|
6979
|
+
const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
|
|
6980
|
+
if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
|
|
6825
6981
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6826
6982
|
return res.end(
|
|
6827
6983
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6932,14 +7088,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6932
7088
|
server.on("error", (e) => {
|
|
6933
7089
|
if (e.code === "EADDRINUSE") {
|
|
6934
7090
|
try {
|
|
6935
|
-
if (
|
|
6936
|
-
const { pid } = JSON.parse(
|
|
7091
|
+
if (fs18.existsSync(DAEMON_PID_FILE)) {
|
|
7092
|
+
const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6937
7093
|
process.kill(pid, 0);
|
|
6938
7094
|
return process.exit(0);
|
|
6939
7095
|
}
|
|
6940
7096
|
} catch {
|
|
6941
7097
|
try {
|
|
6942
|
-
|
|
7098
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6943
7099
|
} catch {
|
|
6944
7100
|
}
|
|
6945
7101
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -7008,56 +7164,327 @@ var init_server = __esm({
|
|
|
7008
7164
|
init_patch();
|
|
7009
7165
|
init_config_schema();
|
|
7010
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");
|
|
7011
7415
|
}
|
|
7012
7416
|
});
|
|
7013
7417
|
|
|
7014
7418
|
// src/daemon/index.ts
|
|
7015
|
-
import
|
|
7419
|
+
import fs20 from "fs";
|
|
7016
7420
|
import chalk3 from "chalk";
|
|
7017
|
-
import { spawnSync as
|
|
7421
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
7018
7422
|
function stopDaemon() {
|
|
7019
|
-
if (!
|
|
7423
|
+
if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
7020
7424
|
try {
|
|
7021
|
-
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
|
+
}
|
|
7022
7431
|
process.kill(pid, "SIGTERM");
|
|
7023
7432
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
7024
7433
|
} catch {
|
|
7025
7434
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
7026
7435
|
} finally {
|
|
7027
7436
|
try {
|
|
7028
|
-
|
|
7437
|
+
fs20.unlinkSync(DAEMON_PID_FILE);
|
|
7029
7438
|
} catch {
|
|
7030
7439
|
}
|
|
7031
7440
|
}
|
|
7032
7441
|
}
|
|
7033
7442
|
function daemonStatus() {
|
|
7034
|
-
|
|
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)) {
|
|
7035
7447
|
try {
|
|
7036
|
-
const
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
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
|
+
}
|
|
7040
7459
|
} catch {
|
|
7041
|
-
|
|
7042
|
-
return;
|
|
7460
|
+
processStatus = chalk3.yellow("not running (stale PID file)");
|
|
7043
7461
|
}
|
|
7044
|
-
}
|
|
7045
|
-
const r = spawnSync3("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
7046
|
-
encoding: "utf8",
|
|
7047
|
-
timeout: 500
|
|
7048
|
-
});
|
|
7049
|
-
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
|
|
7050
|
-
console.log(chalk3.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
7051
7462
|
} else {
|
|
7052
|
-
|
|
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
|
+
}
|
|
7053
7472
|
}
|
|
7473
|
+
console.log(`
|
|
7474
|
+
Process : ${processStatus}`);
|
|
7475
|
+
console.log(` Service : ${serviceLabel}
|
|
7476
|
+
`);
|
|
7054
7477
|
}
|
|
7478
|
+
var MAX_PID;
|
|
7055
7479
|
var init_daemon2 = __esm({
|
|
7056
7480
|
"src/daemon/index.ts"() {
|
|
7057
7481
|
"use strict";
|
|
7058
7482
|
init_server();
|
|
7059
7483
|
init_state2();
|
|
7484
|
+
init_service();
|
|
7060
7485
|
init_state2();
|
|
7486
|
+
init_service();
|
|
7487
|
+
MAX_PID = 4194304;
|
|
7061
7488
|
}
|
|
7062
7489
|
});
|
|
7063
7490
|
|
|
@@ -7067,10 +7494,10 @@ __export(tail_exports, {
|
|
|
7067
7494
|
startTail: () => startTail
|
|
7068
7495
|
});
|
|
7069
7496
|
import http2 from "http";
|
|
7070
|
-
import
|
|
7071
|
-
import
|
|
7072
|
-
import
|
|
7073
|
-
import
|
|
7497
|
+
import chalk23 from "chalk";
|
|
7498
|
+
import fs33 from "fs";
|
|
7499
|
+
import os29 from "os";
|
|
7500
|
+
import path36 from "path";
|
|
7074
7501
|
import readline5 from "readline";
|
|
7075
7502
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
7076
7503
|
function getIcon(tool) {
|
|
@@ -7093,22 +7520,22 @@ function formatBase(activity) {
|
|
|
7093
7520
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
7094
7521
|
const icon = getIcon(activity.tool);
|
|
7095
7522
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
7096
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
7523
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os29.homedir(), "~");
|
|
7097
7524
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
7098
|
-
return `${
|
|
7525
|
+
return `${chalk23.gray(time)} ${icon} ${chalk23.white.bold(toolName)} ${chalk23.dim(argsPreview)}`;
|
|
7099
7526
|
}
|
|
7100
7527
|
function renderResult(activity, result) {
|
|
7101
7528
|
const base = formatBase(activity);
|
|
7102
7529
|
let status;
|
|
7103
7530
|
if (result.status === "allow") {
|
|
7104
|
-
status =
|
|
7531
|
+
status = chalk23.green("\u2713 ALLOW");
|
|
7105
7532
|
} else if (result.status === "dlp") {
|
|
7106
|
-
status =
|
|
7533
|
+
status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
7107
7534
|
} else {
|
|
7108
|
-
status =
|
|
7535
|
+
status = chalk23.red("\u2717 BLOCK");
|
|
7109
7536
|
}
|
|
7110
7537
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
7111
|
-
const costSuffix = cost == null ? "" :
|
|
7538
|
+
const costSuffix = cost == null ? "" : chalk23.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
7112
7539
|
if (process.stdout.isTTY) {
|
|
7113
7540
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
7114
7541
|
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -7125,19 +7552,19 @@ function renderResult(activity, result) {
|
|
|
7125
7552
|
}
|
|
7126
7553
|
function renderPending(activity) {
|
|
7127
7554
|
if (!process.stdout.isTTY) return;
|
|
7128
|
-
const line = `${formatBase(activity)} ${
|
|
7555
|
+
const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
|
|
7129
7556
|
pendingShownForId = activity.id;
|
|
7130
7557
|
pendingWrappedLines = wrappedLineCount(line);
|
|
7131
7558
|
process.stdout.write(`${line}\r`);
|
|
7132
7559
|
}
|
|
7133
7560
|
async function ensureDaemon() {
|
|
7134
7561
|
let pidPort = null;
|
|
7135
|
-
if (
|
|
7562
|
+
if (fs33.existsSync(PID_FILE)) {
|
|
7136
7563
|
try {
|
|
7137
|
-
const { port } = JSON.parse(
|
|
7564
|
+
const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
|
|
7138
7565
|
pidPort = port;
|
|
7139
7566
|
} catch {
|
|
7140
|
-
console.error(
|
|
7567
|
+
console.error(chalk23.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
7141
7568
|
}
|
|
7142
7569
|
}
|
|
7143
7570
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -7148,7 +7575,7 @@ async function ensureDaemon() {
|
|
|
7148
7575
|
if (res.ok) return checkPort;
|
|
7149
7576
|
} catch {
|
|
7150
7577
|
}
|
|
7151
|
-
console.log(
|
|
7578
|
+
console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
7152
7579
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
7153
7580
|
detached: true,
|
|
7154
7581
|
stdio: "ignore",
|
|
@@ -7165,7 +7592,7 @@ async function ensureDaemon() {
|
|
|
7165
7592
|
} catch {
|
|
7166
7593
|
}
|
|
7167
7594
|
}
|
|
7168
|
-
console.error(
|
|
7595
|
+
console.error(chalk23.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7169
7596
|
process.exit(1);
|
|
7170
7597
|
}
|
|
7171
7598
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -7197,27 +7624,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
|
7197
7624
|
req.end(body);
|
|
7198
7625
|
});
|
|
7199
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
|
+
}
|
|
7200
7649
|
function buildCardLines(req, localCount = 0) {
|
|
7201
7650
|
if (req.recoveryCommand) {
|
|
7202
7651
|
return buildRecoveryCardLines(req);
|
|
7203
7652
|
}
|
|
7204
|
-
const
|
|
7205
|
-
const argsPreview =
|
|
7206
|
-
const
|
|
7207
|
-
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) : "";
|
|
7208
7661
|
const lines = [
|
|
7209
7662
|
``,
|
|
7210
7663
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
7211
7664
|
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
7212
|
-
`${CYAN}\u2551${RESET2}
|
|
7665
|
+
`${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
|
|
7213
7666
|
];
|
|
7214
|
-
if (
|
|
7215
|
-
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}
|
|
7667
|
+
if (description) {
|
|
7668
|
+
lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
|
|
7216
7669
|
}
|
|
7217
|
-
if (req.riskMetadata?.ruleName &&
|
|
7670
|
+
if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
|
|
7218
7671
|
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
7219
7672
|
}
|
|
7220
|
-
|
|
7673
|
+
if (argsPreview) {
|
|
7674
|
+
const argLabel = req.toolName.toLowerCase().includes("bash") ? "Command" : "Args ";
|
|
7675
|
+
lines.push(`${CYAN}\u2551${RESET2} ${argLabel}: ${GRAY}${argsPreview}${RESET2}`);
|
|
7676
|
+
}
|
|
7221
7677
|
if (localCount >= 2) {
|
|
7222
7678
|
lines.push(
|
|
7223
7679
|
`${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
|
|
@@ -7257,9 +7713,9 @@ function buildRecoveryCardLines(req) {
|
|
|
7257
7713
|
];
|
|
7258
7714
|
}
|
|
7259
7715
|
function readApproversFromDisk() {
|
|
7260
|
-
const configPath =
|
|
7716
|
+
const configPath = path36.join(os29.homedir(), ".node9", "config.json");
|
|
7261
7717
|
try {
|
|
7262
|
-
const raw = JSON.parse(
|
|
7718
|
+
const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
7263
7719
|
const settings = raw.settings ?? {};
|
|
7264
7720
|
return settings.approvers ?? {};
|
|
7265
7721
|
} catch {
|
|
@@ -7270,20 +7726,20 @@ function approverStatusLine() {
|
|
|
7270
7726
|
const a = readApproversFromDisk();
|
|
7271
7727
|
const fmt = (label, key) => {
|
|
7272
7728
|
const on = a[key] !== false;
|
|
7273
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7729
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk23.green("\u2713") : chalk23.dim("\u2717")}`;
|
|
7274
7730
|
};
|
|
7275
7731
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7276
7732
|
}
|
|
7277
7733
|
function toggleApprover(channel) {
|
|
7278
|
-
const configPath =
|
|
7734
|
+
const configPath = path36.join(os29.homedir(), ".node9", "config.json");
|
|
7279
7735
|
try {
|
|
7280
|
-
const raw = JSON.parse(
|
|
7736
|
+
const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
7281
7737
|
const settings = raw.settings ?? {};
|
|
7282
7738
|
const approvers = settings.approvers ?? {};
|
|
7283
7739
|
approvers[channel] = approvers[channel] === false;
|
|
7284
7740
|
settings.approvers = approvers;
|
|
7285
7741
|
raw.settings = settings;
|
|
7286
|
-
|
|
7742
|
+
fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7287
7743
|
} catch (err2) {
|
|
7288
7744
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7289
7745
|
`);
|
|
@@ -7315,7 +7771,7 @@ async function startTail(options = {}) {
|
|
|
7315
7771
|
req2.end();
|
|
7316
7772
|
});
|
|
7317
7773
|
if (result.ok) {
|
|
7318
|
-
console.log(
|
|
7774
|
+
console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
|
|
7319
7775
|
} else if (result.code === "ECONNREFUSED") {
|
|
7320
7776
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
7321
7777
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7359,7 +7815,7 @@ async function startTail(options = {}) {
|
|
|
7359
7815
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7360
7816
|
if (channel) {
|
|
7361
7817
|
toggleApprover(channel);
|
|
7362
|
-
console.log(
|
|
7818
|
+
console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
|
|
7363
7819
|
}
|
|
7364
7820
|
};
|
|
7365
7821
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7425,7 +7881,7 @@ async function startTail(options = {}) {
|
|
|
7425
7881
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7426
7882
|
)
|
|
7427
7883
|
);
|
|
7428
|
-
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");
|
|
7429
7885
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7430
7886
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7431
7887
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7453,8 +7909,8 @@ async function startTail(options = {}) {
|
|
|
7453
7909
|
}
|
|
7454
7910
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7455
7911
|
try {
|
|
7456
|
-
|
|
7457
|
-
|
|
7912
|
+
fs33.appendFileSync(
|
|
7913
|
+
path36.join(os29.homedir(), ".node9", "hook-debug.log"),
|
|
7458
7914
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7459
7915
|
`
|
|
7460
7916
|
);
|
|
@@ -7476,7 +7932,7 @@ async function startTail(options = {}) {
|
|
|
7476
7932
|
);
|
|
7477
7933
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7478
7934
|
if (externalDecision) {
|
|
7479
|
-
const source = externalDecision === "allow" ?
|
|
7935
|
+
const source = externalDecision === "allow" ? chalk23.green("\u2713 ALLOWED") : chalk23.red("\u2717 DENIED");
|
|
7480
7936
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7481
7937
|
}
|
|
7482
7938
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7535,16 +7991,16 @@ async function startTail(options = {}) {
|
|
|
7535
7991
|
}
|
|
7536
7992
|
} catch {
|
|
7537
7993
|
}
|
|
7538
|
-
console.log(
|
|
7539
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7994
|
+
console.log(chalk23.cyan.bold(`
|
|
7995
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk23.dim(`\u2192 ${dashboardUrl}`));
|
|
7540
7996
|
if (canApprove) {
|
|
7541
|
-
console.log(
|
|
7542
|
-
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`));
|
|
7543
7999
|
}
|
|
7544
8000
|
if (options.history) {
|
|
7545
|
-
console.log(
|
|
8001
|
+
console.log(chalk23.dim("Showing history + live events.\n"));
|
|
7546
8002
|
} else {
|
|
7547
|
-
console.log(
|
|
8003
|
+
console.log(chalk23.dim("Showing live events only. Use --history to include past.\n"));
|
|
7548
8004
|
}
|
|
7549
8005
|
process.on("SIGINT", () => {
|
|
7550
8006
|
exitIdleMode();
|
|
@@ -7554,13 +8010,13 @@ async function startTail(options = {}) {
|
|
|
7554
8010
|
readline5.clearLine(process.stdout, 0);
|
|
7555
8011
|
readline5.cursorTo(process.stdout, 0);
|
|
7556
8012
|
}
|
|
7557
|
-
console.log(
|
|
8013
|
+
console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7558
8014
|
process.exit(0);
|
|
7559
8015
|
});
|
|
7560
8016
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7561
8017
|
const req = http2.get(sseUrl, (res) => {
|
|
7562
8018
|
if (res.statusCode !== 200) {
|
|
7563
|
-
console.error(
|
|
8019
|
+
console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7564
8020
|
process.exit(1);
|
|
7565
8021
|
}
|
|
7566
8022
|
if (canApprove) enterIdleMode();
|
|
@@ -7591,7 +8047,7 @@ async function startTail(options = {}) {
|
|
|
7591
8047
|
readline5.clearLine(process.stdout, 0);
|
|
7592
8048
|
readline5.cursorTo(process.stdout, 0);
|
|
7593
8049
|
}
|
|
7594
|
-
console.log(
|
|
8050
|
+
console.log(chalk23.red("\n\u274C Daemon disconnected."));
|
|
7595
8051
|
process.exit(1);
|
|
7596
8052
|
});
|
|
7597
8053
|
});
|
|
@@ -7683,9 +8139,9 @@ async function startTail(options = {}) {
|
|
|
7683
8139
|
const hash = data.hash ?? "";
|
|
7684
8140
|
const summary = data.argsSummary ?? data.tool;
|
|
7685
8141
|
const fileCount = data.fileCount ?? 0;
|
|
7686
|
-
const files = fileCount > 0 ?
|
|
8142
|
+
const files = fileCount > 0 ? chalk23.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7687
8143
|
process.stdout.write(
|
|
7688
|
-
`${
|
|
8144
|
+
`${chalk23.dim(time)} ${chalk23.cyan("\u{1F4F8} snapshot")} ${chalk23.dim(hash)} ${summary}${files}
|
|
7689
8145
|
`
|
|
7690
8146
|
);
|
|
7691
8147
|
return;
|
|
@@ -7702,7 +8158,7 @@ async function startTail(options = {}) {
|
|
|
7702
8158
|
}
|
|
7703
8159
|
req.on("error", (err2) => {
|
|
7704
8160
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7705
|
-
console.error(
|
|
8161
|
+
console.error(chalk23.red(`
|
|
7706
8162
|
\u274C ${msg}`));
|
|
7707
8163
|
process.exit(1);
|
|
7708
8164
|
});
|
|
@@ -7714,7 +8170,7 @@ var init_tail = __esm({
|
|
|
7714
8170
|
init_daemon2();
|
|
7715
8171
|
init_daemon();
|
|
7716
8172
|
init_core();
|
|
7717
|
-
PID_FILE =
|
|
8173
|
+
PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
|
|
7718
8174
|
ICONS = {
|
|
7719
8175
|
bash: "\u{1F4BB}",
|
|
7720
8176
|
shell: "\u{1F4BB}",
|
|
@@ -7755,9 +8211,9 @@ __export(hud_exports, {
|
|
|
7755
8211
|
main: () => main,
|
|
7756
8212
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7757
8213
|
});
|
|
7758
|
-
import
|
|
7759
|
-
import
|
|
7760
|
-
import
|
|
8214
|
+
import fs34 from "fs";
|
|
8215
|
+
import path37 from "path";
|
|
8216
|
+
import os30 from "os";
|
|
7761
8217
|
import http3 from "http";
|
|
7762
8218
|
async function readStdin() {
|
|
7763
8219
|
const chunks = [];
|
|
@@ -7833,9 +8289,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7833
8289
|
return ` (${m}m left)`;
|
|
7834
8290
|
}
|
|
7835
8291
|
function safeReadJson(filePath) {
|
|
7836
|
-
if (!
|
|
8292
|
+
if (!fs34.existsSync(filePath)) return null;
|
|
7837
8293
|
try {
|
|
7838
|
-
return JSON.parse(
|
|
8294
|
+
return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
|
|
7839
8295
|
} catch {
|
|
7840
8296
|
return null;
|
|
7841
8297
|
}
|
|
@@ -7856,12 +8312,12 @@ function countHooksInFile(filePath) {
|
|
|
7856
8312
|
return Object.keys(cfg.hooks).length;
|
|
7857
8313
|
}
|
|
7858
8314
|
function countRulesInDir(rulesDir) {
|
|
7859
|
-
if (!
|
|
8315
|
+
if (!fs34.existsSync(rulesDir)) return 0;
|
|
7860
8316
|
let count = 0;
|
|
7861
8317
|
try {
|
|
7862
|
-
for (const entry of
|
|
8318
|
+
for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7863
8319
|
if (entry.isDirectory()) {
|
|
7864
|
-
count += countRulesInDir(
|
|
8320
|
+
count += countRulesInDir(path37.join(rulesDir, entry.name));
|
|
7865
8321
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7866
8322
|
count++;
|
|
7867
8323
|
}
|
|
@@ -7872,46 +8328,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7872
8328
|
}
|
|
7873
8329
|
function isSamePath(a, b) {
|
|
7874
8330
|
try {
|
|
7875
|
-
return
|
|
8331
|
+
return path37.resolve(a) === path37.resolve(b);
|
|
7876
8332
|
} catch {
|
|
7877
8333
|
return false;
|
|
7878
8334
|
}
|
|
7879
8335
|
}
|
|
7880
8336
|
function countConfigs(cwd) {
|
|
7881
|
-
const homeDir2 =
|
|
7882
|
-
const claudeDir =
|
|
8337
|
+
const homeDir2 = os30.homedir();
|
|
8338
|
+
const claudeDir = path37.join(homeDir2, ".claude");
|
|
7883
8339
|
let claudeMdCount = 0;
|
|
7884
8340
|
let rulesCount = 0;
|
|
7885
8341
|
let hooksCount = 0;
|
|
7886
8342
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7887
8343
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7888
|
-
if (
|
|
7889
|
-
rulesCount += countRulesInDir(
|
|
7890
|
-
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");
|
|
7891
8347
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7892
8348
|
hooksCount += countHooksInFile(userSettings);
|
|
7893
|
-
const userClaudeJson =
|
|
8349
|
+
const userClaudeJson = path37.join(homeDir2, ".claude.json");
|
|
7894
8350
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7895
8351
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7896
8352
|
userMcpServers.delete(name);
|
|
7897
8353
|
}
|
|
7898
8354
|
if (cwd) {
|
|
7899
|
-
if (
|
|
7900
|
-
if (
|
|
7901
|
-
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");
|
|
7902
8358
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7903
8359
|
if (!overlapsUserScope) {
|
|
7904
|
-
if (
|
|
7905
|
-
rulesCount += countRulesInDir(
|
|
7906
|
-
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");
|
|
7907
8363
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7908
8364
|
hooksCount += countHooksInFile(projSettings);
|
|
7909
8365
|
}
|
|
7910
|
-
if (
|
|
7911
|
-
const localSettings =
|
|
8366
|
+
if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8367
|
+
const localSettings = path37.join(projectClaudeDir, "settings.local.json");
|
|
7912
8368
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7913
8369
|
hooksCount += countHooksInFile(localSettings);
|
|
7914
|
-
const mcpJsonServers = getMcpServerNames(
|
|
8370
|
+
const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
|
|
7915
8371
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7916
8372
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7917
8373
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7944,12 +8400,12 @@ function readActiveShieldsHud() {
|
|
|
7944
8400
|
return shieldsCache.value;
|
|
7945
8401
|
}
|
|
7946
8402
|
try {
|
|
7947
|
-
const shieldsPath =
|
|
7948
|
-
if (!
|
|
8403
|
+
const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
|
|
8404
|
+
if (!fs34.existsSync(shieldsPath)) {
|
|
7949
8405
|
shieldsCache = { value: [], ts: now };
|
|
7950
8406
|
return [];
|
|
7951
8407
|
}
|
|
7952
|
-
const parsed = JSON.parse(
|
|
8408
|
+
const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
|
|
7953
8409
|
if (!Array.isArray(parsed.active)) {
|
|
7954
8410
|
shieldsCache = { value: [], ts: now };
|
|
7955
8411
|
return [];
|
|
@@ -8051,17 +8507,17 @@ function renderContextLine(stdin) {
|
|
|
8051
8507
|
async function main() {
|
|
8052
8508
|
try {
|
|
8053
8509
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
8054
|
-
if (
|
|
8510
|
+
if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
|
|
8055
8511
|
try {
|
|
8056
|
-
const logPath =
|
|
8512
|
+
const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
|
|
8057
8513
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8058
8514
|
let size = 0;
|
|
8059
8515
|
try {
|
|
8060
|
-
size =
|
|
8516
|
+
size = fs34.statSync(logPath).size;
|
|
8061
8517
|
} catch {
|
|
8062
8518
|
}
|
|
8063
8519
|
if (size < MAX_LOG_SIZE) {
|
|
8064
|
-
|
|
8520
|
+
fs34.appendFileSync(
|
|
8065
8521
|
logPath,
|
|
8066
8522
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
8067
8523
|
);
|
|
@@ -8082,11 +8538,11 @@ async function main() {
|
|
|
8082
8538
|
try {
|
|
8083
8539
|
const cwd = stdin.cwd ?? process.cwd();
|
|
8084
8540
|
for (const configPath of [
|
|
8085
|
-
|
|
8086
|
-
|
|
8541
|
+
path37.join(cwd, "node9.config.json"),
|
|
8542
|
+
path37.join(os30.homedir(), ".node9", "config.json")
|
|
8087
8543
|
]) {
|
|
8088
|
-
if (!
|
|
8089
|
-
const cfg = JSON.parse(
|
|
8544
|
+
if (!fs34.existsSync(configPath)) continue;
|
|
8545
|
+
const cfg = JSON.parse(fs34.readFileSync(configPath, "utf-8"));
|
|
8090
8546
|
const hud = cfg.settings?.hud;
|
|
8091
8547
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
8092
8548
|
}
|
|
@@ -8517,7 +8973,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
8517
8973
|
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
8518
8974
|
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
8519
8975
|
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
8520
|
-
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"))
|
|
8521
8979
|
};
|
|
8522
8980
|
}
|
|
8523
8981
|
async function setupCursor() {
|
|
@@ -8666,6 +9124,38 @@ async function setupCodex() {
|
|
|
8666
9124
|
printDaemonTip();
|
|
8667
9125
|
}
|
|
8668
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
|
+
}
|
|
8669
9159
|
function setupHud() {
|
|
8670
9160
|
const homeDir2 = os11.homedir();
|
|
8671
9161
|
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8712,43 +9202,315 @@ function teardownHud() {
|
|
|
8712
9202
|
console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
|
|
8713
9203
|
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
8714
9204
|
}
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
if (!m) return null;
|
|
8728
|
-
const n = parseFloat(m[1]);
|
|
8729
|
-
switch ((m[2] ?? "m").toLowerCase()) {
|
|
8730
|
-
case "s":
|
|
8731
|
-
return Math.round(n * 1e3);
|
|
8732
|
-
case "m":
|
|
8733
|
-
return Math.round(n * 6e4);
|
|
8734
|
-
case "h":
|
|
8735
|
-
return Math.round(n * 36e5);
|
|
8736
|
-
case "d":
|
|
8737
|
-
return Math.round(n * 864e5);
|
|
8738
|
-
default:
|
|
8739
|
-
return null;
|
|
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;
|
|
8740
9217
|
}
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
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
|
+
}
|
|
9504
|
+
|
|
9505
|
+
// src/proxy/index.ts
|
|
9506
|
+
init_orchestrator();
|
|
9507
|
+
import readline from "readline";
|
|
9508
|
+
import chalk4 from "chalk";
|
|
9509
|
+
import { spawn as spawn3 } from "child_process";
|
|
9510
|
+
import { execa } from "execa";
|
|
9511
|
+
import { parseCommandString } from "execa";
|
|
9512
|
+
|
|
9513
|
+
// src/policy/negotiation.ts
|
|
8752
9514
|
function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
|
|
8753
9515
|
if (isHumanDecision) {
|
|
8754
9516
|
return `NODE9: The human user rejected this action.
|
|
@@ -8947,19 +9709,19 @@ init_daemon();
|
|
|
8947
9709
|
init_config();
|
|
8948
9710
|
init_policy();
|
|
8949
9711
|
import chalk5 from "chalk";
|
|
8950
|
-
import
|
|
9712
|
+
import fs22 from "fs";
|
|
8951
9713
|
import { spawn as spawn6 } from "child_process";
|
|
8952
|
-
import
|
|
8953
|
-
import
|
|
9714
|
+
import path24 from "path";
|
|
9715
|
+
import os18 from "os";
|
|
8954
9716
|
|
|
8955
9717
|
// src/undo.ts
|
|
8956
|
-
import { spawnSync as
|
|
9718
|
+
import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
|
|
8957
9719
|
import crypto3 from "crypto";
|
|
8958
|
-
import
|
|
9720
|
+
import fs21 from "fs";
|
|
8959
9721
|
import net3 from "net";
|
|
8960
|
-
import
|
|
8961
|
-
import
|
|
8962
|
-
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");
|
|
8963
9725
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8964
9726
|
try {
|
|
8965
9727
|
const payload = JSON.stringify({
|
|
@@ -8979,22 +9741,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8979
9741
|
} catch {
|
|
8980
9742
|
}
|
|
8981
9743
|
}
|
|
8982
|
-
var SNAPSHOT_STACK_PATH =
|
|
8983
|
-
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");
|
|
8984
9746
|
var MAX_SNAPSHOTS = 10;
|
|
8985
9747
|
var GIT_TIMEOUT = 15e3;
|
|
8986
9748
|
function readStack() {
|
|
8987
9749
|
try {
|
|
8988
|
-
if (
|
|
8989
|
-
return JSON.parse(
|
|
9750
|
+
if (fs21.existsSync(SNAPSHOT_STACK_PATH))
|
|
9751
|
+
return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8990
9752
|
} catch {
|
|
8991
9753
|
}
|
|
8992
9754
|
return [];
|
|
8993
9755
|
}
|
|
8994
9756
|
function writeStack(stack) {
|
|
8995
|
-
const dir =
|
|
8996
|
-
if (!
|
|
8997
|
-
|
|
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));
|
|
8998
9760
|
}
|
|
8999
9761
|
function extractFilePath(args) {
|
|
9000
9762
|
if (!args || typeof args !== "object") return null;
|
|
@@ -9014,12 +9776,12 @@ function buildArgsSummary(tool, args) {
|
|
|
9014
9776
|
return "";
|
|
9015
9777
|
}
|
|
9016
9778
|
function findProjectRoot(filePath) {
|
|
9017
|
-
let dir =
|
|
9779
|
+
let dir = path23.dirname(filePath);
|
|
9018
9780
|
while (true) {
|
|
9019
|
-
if (
|
|
9781
|
+
if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
|
|
9020
9782
|
return dir;
|
|
9021
9783
|
}
|
|
9022
|
-
const parent =
|
|
9784
|
+
const parent = path23.dirname(dir);
|
|
9023
9785
|
if (parent === dir) return process.cwd();
|
|
9024
9786
|
dir = parent;
|
|
9025
9787
|
}
|
|
@@ -9027,7 +9789,7 @@ function findProjectRoot(filePath) {
|
|
|
9027
9789
|
function normalizeCwdForHash(cwd) {
|
|
9028
9790
|
let normalized;
|
|
9029
9791
|
try {
|
|
9030
|
-
normalized =
|
|
9792
|
+
normalized = fs21.realpathSync(cwd);
|
|
9031
9793
|
} catch {
|
|
9032
9794
|
normalized = cwd;
|
|
9033
9795
|
}
|
|
@@ -9037,16 +9799,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
9037
9799
|
}
|
|
9038
9800
|
function getShadowRepoDir(cwd) {
|
|
9039
9801
|
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
9040
|
-
return
|
|
9802
|
+
return path23.join(os17.homedir(), ".node9", "snapshots", hash);
|
|
9041
9803
|
}
|
|
9042
9804
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
9043
9805
|
try {
|
|
9044
9806
|
const cutoff = Date.now() - 6e4;
|
|
9045
|
-
for (const f of
|
|
9807
|
+
for (const f of fs21.readdirSync(shadowDir)) {
|
|
9046
9808
|
if (f.startsWith("index_")) {
|
|
9047
|
-
const fp =
|
|
9809
|
+
const fp = path23.join(shadowDir, f);
|
|
9048
9810
|
try {
|
|
9049
|
-
if (
|
|
9811
|
+
if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
|
|
9050
9812
|
} catch {
|
|
9051
9813
|
}
|
|
9052
9814
|
}
|
|
@@ -9058,7 +9820,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
9058
9820
|
const hardcoded = [".git", ".node9"];
|
|
9059
9821
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
9060
9822
|
try {
|
|
9061
|
-
|
|
9823
|
+
fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
9062
9824
|
} catch {
|
|
9063
9825
|
}
|
|
9064
9826
|
}
|
|
@@ -9066,54 +9828,54 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9066
9828
|
cleanOrphanedIndexFiles(shadowDir);
|
|
9067
9829
|
const normalizedCwd = normalizeCwdForHash(cwd);
|
|
9068
9830
|
const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
9069
|
-
const check =
|
|
9831
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9070
9832
|
env: shadowEnvBase,
|
|
9071
9833
|
timeout: 3e3
|
|
9072
9834
|
});
|
|
9073
9835
|
if (check.status === 0) {
|
|
9074
|
-
const ptPath =
|
|
9836
|
+
const ptPath = path23.join(shadowDir, "project-path.txt");
|
|
9075
9837
|
try {
|
|
9076
|
-
const stored =
|
|
9838
|
+
const stored = fs21.readFileSync(ptPath, "utf8").trim();
|
|
9077
9839
|
if (stored === normalizedCwd) return true;
|
|
9078
9840
|
if (process.env.NODE9_DEBUG === "1")
|
|
9079
9841
|
console.error(
|
|
9080
9842
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
9081
9843
|
);
|
|
9082
|
-
|
|
9844
|
+
fs21.rmSync(shadowDir, { recursive: true, force: true });
|
|
9083
9845
|
} catch {
|
|
9084
9846
|
try {
|
|
9085
|
-
|
|
9847
|
+
fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
9086
9848
|
} catch {
|
|
9087
9849
|
}
|
|
9088
9850
|
return true;
|
|
9089
9851
|
}
|
|
9090
9852
|
}
|
|
9091
9853
|
try {
|
|
9092
|
-
|
|
9854
|
+
fs21.mkdirSync(shadowDir, { recursive: true });
|
|
9093
9855
|
} catch {
|
|
9094
9856
|
}
|
|
9095
|
-
const init =
|
|
9857
|
+
const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
9096
9858
|
if (init.status !== 0 || init.error) {
|
|
9097
9859
|
const reason = init.error ? init.error.message : init.stderr?.toString();
|
|
9098
9860
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
9099
9861
|
return false;
|
|
9100
9862
|
}
|
|
9101
|
-
const configFile =
|
|
9102
|
-
|
|
9863
|
+
const configFile = path23.join(shadowDir, "config");
|
|
9864
|
+
spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
9103
9865
|
timeout: 3e3
|
|
9104
9866
|
});
|
|
9105
|
-
|
|
9867
|
+
spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
|
|
9106
9868
|
timeout: 3e3
|
|
9107
9869
|
});
|
|
9108
9870
|
try {
|
|
9109
|
-
|
|
9871
|
+
fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
9110
9872
|
} catch {
|
|
9111
9873
|
}
|
|
9112
9874
|
return true;
|
|
9113
9875
|
}
|
|
9114
9876
|
function buildGitEnv(cwd) {
|
|
9115
9877
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9116
|
-
const check =
|
|
9878
|
+
const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
|
|
9117
9879
|
env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
|
|
9118
9880
|
timeout: 2e3
|
|
9119
9881
|
});
|
|
@@ -9126,23 +9888,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9126
9888
|
let indexFile = null;
|
|
9127
9889
|
try {
|
|
9128
9890
|
const rawFilePath = extractFilePath(args);
|
|
9129
|
-
const absFilePath = rawFilePath &&
|
|
9891
|
+
const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
9130
9892
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
9131
9893
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9132
9894
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
9133
9895
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
9134
|
-
indexFile =
|
|
9896
|
+
indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
9135
9897
|
const shadowEnv = {
|
|
9136
9898
|
...process.env,
|
|
9137
9899
|
GIT_DIR: shadowDir,
|
|
9138
9900
|
GIT_WORK_TREE: cwd,
|
|
9139
9901
|
GIT_INDEX_FILE: indexFile
|
|
9140
9902
|
};
|
|
9141
|
-
|
|
9142
|
-
const treeRes =
|
|
9903
|
+
spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9904
|
+
const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
9143
9905
|
const treeHash = treeRes.stdout?.toString().trim();
|
|
9144
9906
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
9145
|
-
const commitRes =
|
|
9907
|
+
const commitRes = spawnSync5(
|
|
9146
9908
|
"git",
|
|
9147
9909
|
["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
|
|
9148
9910
|
{ env: shadowEnv, timeout: GIT_TIMEOUT }
|
|
@@ -9154,7 +9916,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9154
9916
|
let capturedFiles = [];
|
|
9155
9917
|
let capturedDiff = null;
|
|
9156
9918
|
if (prevEntry) {
|
|
9157
|
-
const filesRes =
|
|
9919
|
+
const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
|
|
9158
9920
|
env: shadowEnv,
|
|
9159
9921
|
timeout: GIT_TIMEOUT
|
|
9160
9922
|
});
|
|
@@ -9164,7 +9926,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9164
9926
|
if (capturedFiles.length === 0) {
|
|
9165
9927
|
return prevEntry.hash;
|
|
9166
9928
|
}
|
|
9167
|
-
const diffRes =
|
|
9929
|
+
const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
|
|
9168
9930
|
env: shadowEnv,
|
|
9169
9931
|
timeout: GIT_TIMEOUT
|
|
9170
9932
|
});
|
|
@@ -9172,7 +9934,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9172
9934
|
capturedDiff = diffRes.stdout?.toString() || null;
|
|
9173
9935
|
}
|
|
9174
9936
|
} else {
|
|
9175
|
-
const filesRes =
|
|
9937
|
+
const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
|
|
9176
9938
|
env: shadowEnv,
|
|
9177
9939
|
timeout: GIT_TIMEOUT
|
|
9178
9940
|
});
|
|
@@ -9203,7 +9965,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9203
9965
|
writeStack(stack);
|
|
9204
9966
|
const entry = stack[stack.length - 1];
|
|
9205
9967
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
9206
|
-
|
|
9968
|
+
fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
9207
9969
|
if (shouldGc) {
|
|
9208
9970
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
9209
9971
|
}
|
|
@@ -9214,7 +9976,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9214
9976
|
} finally {
|
|
9215
9977
|
if (indexFile) {
|
|
9216
9978
|
try {
|
|
9217
|
-
|
|
9979
|
+
fs21.unlinkSync(indexFile);
|
|
9218
9980
|
} catch {
|
|
9219
9981
|
}
|
|
9220
9982
|
}
|
|
@@ -9226,14 +9988,14 @@ function getSnapshotHistory() {
|
|
|
9226
9988
|
function computeUndoDiff(hash, cwd) {
|
|
9227
9989
|
try {
|
|
9228
9990
|
const env = buildGitEnv(cwd);
|
|
9229
|
-
const statRes =
|
|
9991
|
+
const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
|
|
9230
9992
|
cwd,
|
|
9231
9993
|
env,
|
|
9232
9994
|
timeout: GIT_TIMEOUT
|
|
9233
9995
|
});
|
|
9234
9996
|
const stat = statRes.stdout?.toString().trim();
|
|
9235
9997
|
if (!stat || statRes.status !== 0) return null;
|
|
9236
|
-
const diffRes =
|
|
9998
|
+
const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
|
|
9237
9999
|
cwd,
|
|
9238
10000
|
env,
|
|
9239
10001
|
timeout: GIT_TIMEOUT
|
|
@@ -9252,7 +10014,7 @@ function applyUndo(hash, cwd) {
|
|
|
9252
10014
|
try {
|
|
9253
10015
|
const dir = cwd ?? process.cwd();
|
|
9254
10016
|
const env = buildGitEnv(dir);
|
|
9255
|
-
const restore =
|
|
10017
|
+
const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
9256
10018
|
cwd: dir,
|
|
9257
10019
|
env,
|
|
9258
10020
|
timeout: GIT_TIMEOUT
|
|
@@ -9264,7 +10026,7 @@ function applyUndo(hash, cwd) {
|
|
|
9264
10026
|
}
|
|
9265
10027
|
return false;
|
|
9266
10028
|
}
|
|
9267
|
-
const lsTree =
|
|
10029
|
+
const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
9268
10030
|
cwd: dir,
|
|
9269
10031
|
env,
|
|
9270
10032
|
timeout: GIT_TIMEOUT
|
|
@@ -9283,16 +10045,16 @@ function applyUndo(hash, cwd) {
|
|
|
9283
10045
|
`);
|
|
9284
10046
|
return false;
|
|
9285
10047
|
}
|
|
9286
|
-
const tracked =
|
|
9287
|
-
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"], {
|
|
9288
10050
|
cwd: dir,
|
|
9289
10051
|
env,
|
|
9290
10052
|
timeout: GIT_TIMEOUT
|
|
9291
10053
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
9292
10054
|
for (const file of [...tracked, ...untracked]) {
|
|
9293
|
-
const fullPath =
|
|
9294
|
-
if (!snapshotFiles.has(file) &&
|
|
9295
|
-
|
|
10055
|
+
const fullPath = path23.join(dir, file);
|
|
10056
|
+
if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
|
|
10057
|
+
fs21.unlinkSync(fullPath);
|
|
9296
10058
|
}
|
|
9297
10059
|
}
|
|
9298
10060
|
return true;
|
|
@@ -9316,9 +10078,9 @@ function registerCheckCommand(program2) {
|
|
|
9316
10078
|
} catch (err2) {
|
|
9317
10079
|
const tempConfig = getConfig();
|
|
9318
10080
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
9319
|
-
const logPath =
|
|
10081
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
9320
10082
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9321
|
-
|
|
10083
|
+
fs22.appendFileSync(
|
|
9322
10084
|
logPath,
|
|
9323
10085
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
9324
10086
|
RAW: ${raw}
|
|
@@ -9331,11 +10093,11 @@ RAW: ${raw}
|
|
|
9331
10093
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
9332
10094
|
try {
|
|
9333
10095
|
const scriptPath = process.argv[1];
|
|
9334
|
-
if (typeof scriptPath !== "string" || !
|
|
10096
|
+
if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
|
|
9335
10097
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
9336
|
-
const resolvedScript =
|
|
9337
|
-
const packageDist =
|
|
9338
|
-
if (!resolvedScript.startsWith(packageDist +
|
|
10098
|
+
const resolvedScript = fs22.realpathSync(scriptPath);
|
|
10099
|
+
const packageDist = fs22.realpathSync(path24.resolve(__dirname, "../.."));
|
|
10100
|
+
if (!resolvedScript.startsWith(packageDist + path24.sep) && resolvedScript !== packageDist)
|
|
9339
10101
|
throw new Error(
|
|
9340
10102
|
`node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
|
|
9341
10103
|
);
|
|
@@ -9357,10 +10119,10 @@ RAW: ${raw}
|
|
|
9357
10119
|
});
|
|
9358
10120
|
d.unref();
|
|
9359
10121
|
} catch (spawnErr) {
|
|
9360
|
-
const logPath =
|
|
10122
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
9361
10123
|
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
9362
10124
|
try {
|
|
9363
|
-
|
|
10125
|
+
fs22.appendFileSync(
|
|
9364
10126
|
logPath,
|
|
9365
10127
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
|
|
9366
10128
|
`
|
|
@@ -9370,10 +10132,10 @@ RAW: ${raw}
|
|
|
9370
10132
|
}
|
|
9371
10133
|
}
|
|
9372
10134
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9373
|
-
const logPath =
|
|
9374
|
-
if (!
|
|
9375
|
-
|
|
9376
|
-
|
|
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}
|
|
9377
10139
|
`);
|
|
9378
10140
|
}
|
|
9379
10141
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9386,8 +10148,8 @@ RAW: ${raw}
|
|
|
9386
10148
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9387
10149
|
let ttyFd = null;
|
|
9388
10150
|
try {
|
|
9389
|
-
ttyFd =
|
|
9390
|
-
const writeTty = (line) =>
|
|
10151
|
+
ttyFd = fs22.openSync("/dev/tty", "w");
|
|
10152
|
+
const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
|
|
9391
10153
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9392
10154
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9393
10155
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9406,7 +10168,7 @@ RAW: ${raw}
|
|
|
9406
10168
|
} finally {
|
|
9407
10169
|
if (ttyFd !== null)
|
|
9408
10170
|
try {
|
|
9409
|
-
|
|
10171
|
+
fs22.closeSync(ttyFd);
|
|
9410
10172
|
} catch {
|
|
9411
10173
|
}
|
|
9412
10174
|
}
|
|
@@ -9438,7 +10200,7 @@ RAW: ${raw}
|
|
|
9438
10200
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9439
10201
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9440
10202
|
}
|
|
9441
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
10203
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path24.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9442
10204
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9443
10205
|
cwd: safeCwdForAuth
|
|
9444
10206
|
});
|
|
@@ -9450,12 +10212,12 @@ RAW: ${raw}
|
|
|
9450
10212
|
}
|
|
9451
10213
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9452
10214
|
try {
|
|
9453
|
-
const tty =
|
|
9454
|
-
|
|
10215
|
+
const tty = fs22.openSync("/dev/tty", "w");
|
|
10216
|
+
fs22.writeSync(
|
|
9455
10217
|
tty,
|
|
9456
10218
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9457
10219
|
);
|
|
9458
|
-
|
|
10220
|
+
fs22.closeSync(tty);
|
|
9459
10221
|
} catch {
|
|
9460
10222
|
}
|
|
9461
10223
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9482,9 +10244,9 @@ RAW: ${raw}
|
|
|
9482
10244
|
});
|
|
9483
10245
|
} catch (err2) {
|
|
9484
10246
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9485
|
-
const logPath =
|
|
10247
|
+
const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
|
|
9486
10248
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9487
|
-
|
|
10249
|
+
fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9488
10250
|
`);
|
|
9489
10251
|
}
|
|
9490
10252
|
process.exit(0);
|
|
@@ -9521,9 +10283,9 @@ RAW: ${raw}
|
|
|
9521
10283
|
init_audit();
|
|
9522
10284
|
init_config();
|
|
9523
10285
|
init_policy();
|
|
9524
|
-
import
|
|
9525
|
-
import
|
|
9526
|
-
import
|
|
10286
|
+
import fs23 from "fs";
|
|
10287
|
+
import path25 from "path";
|
|
10288
|
+
import os19 from "os";
|
|
9527
10289
|
init_daemon();
|
|
9528
10290
|
|
|
9529
10291
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9596,10 +10358,10 @@ function registerLogCommand(program2) {
|
|
|
9596
10358
|
decision: "allowed",
|
|
9597
10359
|
source: "post-hook"
|
|
9598
10360
|
};
|
|
9599
|
-
const logPath =
|
|
9600
|
-
if (!
|
|
9601
|
-
|
|
9602
|
-
|
|
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");
|
|
9603
10365
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9604
10366
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9605
10367
|
if (command) {
|
|
@@ -9632,7 +10394,7 @@ function registerLogCommand(program2) {
|
|
|
9632
10394
|
}
|
|
9633
10395
|
}
|
|
9634
10396
|
}
|
|
9635
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
10397
|
+
const safeCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9636
10398
|
const config = getConfig(safeCwd);
|
|
9637
10399
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9638
10400
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9641,9 +10403,9 @@ function registerLogCommand(program2) {
|
|
|
9641
10403
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9642
10404
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9643
10405
|
`);
|
|
9644
|
-
const debugPath =
|
|
10406
|
+
const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
|
|
9645
10407
|
try {
|
|
9646
|
-
|
|
10408
|
+
fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9647
10409
|
`);
|
|
9648
10410
|
} catch {
|
|
9649
10411
|
}
|
|
@@ -9673,10 +10435,10 @@ init_config();
|
|
|
9673
10435
|
import chalk6 from "chalk";
|
|
9674
10436
|
|
|
9675
10437
|
// src/utils/https-fetch.ts
|
|
9676
|
-
import
|
|
10438
|
+
import https2 from "https";
|
|
9677
10439
|
function httpsFetch(url) {
|
|
9678
10440
|
return new Promise((resolve, reject) => {
|
|
9679
|
-
|
|
10441
|
+
https2.get(url, (res) => {
|
|
9680
10442
|
if (res.statusCode !== 200) {
|
|
9681
10443
|
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9682
10444
|
res.resume();
|
|
@@ -10044,13 +10806,13 @@ function registerConfigShowCommand(program2) {
|
|
|
10044
10806
|
// src/cli/commands/doctor.ts
|
|
10045
10807
|
init_daemon();
|
|
10046
10808
|
import chalk7 from "chalk";
|
|
10047
|
-
import
|
|
10048
|
-
import
|
|
10049
|
-
import
|
|
10809
|
+
import fs24 from "fs";
|
|
10810
|
+
import path26 from "path";
|
|
10811
|
+
import os20 from "os";
|
|
10050
10812
|
import { execSync as execSync2 } from "child_process";
|
|
10051
10813
|
function registerDoctorCommand(program2, version2) {
|
|
10052
10814
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
10053
|
-
const homeDir2 =
|
|
10815
|
+
const homeDir2 = os20.homedir();
|
|
10054
10816
|
let failures = 0;
|
|
10055
10817
|
function pass(msg) {
|
|
10056
10818
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -10099,10 +10861,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10099
10861
|
);
|
|
10100
10862
|
}
|
|
10101
10863
|
section("Configuration");
|
|
10102
|
-
const globalConfigPath =
|
|
10103
|
-
if (
|
|
10864
|
+
const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
|
|
10865
|
+
if (fs24.existsSync(globalConfigPath)) {
|
|
10104
10866
|
try {
|
|
10105
|
-
JSON.parse(
|
|
10867
|
+
JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
|
|
10106
10868
|
pass("~/.node9/config.json found and valid");
|
|
10107
10869
|
} catch {
|
|
10108
10870
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -10110,10 +10872,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10110
10872
|
} else {
|
|
10111
10873
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
10112
10874
|
}
|
|
10113
|
-
const projectConfigPath =
|
|
10114
|
-
if (
|
|
10875
|
+
const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
|
|
10876
|
+
if (fs24.existsSync(projectConfigPath)) {
|
|
10115
10877
|
try {
|
|
10116
|
-
JSON.parse(
|
|
10878
|
+
JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
|
|
10117
10879
|
pass("node9.config.json found and valid (project)");
|
|
10118
10880
|
} catch {
|
|
10119
10881
|
fail(
|
|
@@ -10122,8 +10884,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10122
10884
|
);
|
|
10123
10885
|
}
|
|
10124
10886
|
}
|
|
10125
|
-
const credsPath =
|
|
10126
|
-
if (
|
|
10887
|
+
const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
|
|
10888
|
+
if (fs24.existsSync(credsPath)) {
|
|
10127
10889
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
10128
10890
|
} else {
|
|
10129
10891
|
warn(
|
|
@@ -10132,10 +10894,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10132
10894
|
);
|
|
10133
10895
|
}
|
|
10134
10896
|
section("Agent Hooks");
|
|
10135
|
-
const claudeSettingsPath =
|
|
10136
|
-
if (
|
|
10897
|
+
const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
|
|
10898
|
+
if (fs24.existsSync(claudeSettingsPath)) {
|
|
10137
10899
|
try {
|
|
10138
|
-
const cs = JSON.parse(
|
|
10900
|
+
const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
|
|
10139
10901
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
10140
10902
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10141
10903
|
);
|
|
@@ -10151,10 +10913,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10151
10913
|
} else {
|
|
10152
10914
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
10153
10915
|
}
|
|
10154
|
-
const geminiSettingsPath =
|
|
10155
|
-
if (
|
|
10916
|
+
const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
|
|
10917
|
+
if (fs24.existsSync(geminiSettingsPath)) {
|
|
10156
10918
|
try {
|
|
10157
|
-
const gs = JSON.parse(
|
|
10919
|
+
const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
|
|
10158
10920
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
10159
10921
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10160
10922
|
);
|
|
@@ -10170,10 +10932,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10170
10932
|
} else {
|
|
10171
10933
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
10172
10934
|
}
|
|
10173
|
-
const cursorHooksPath =
|
|
10174
|
-
if (
|
|
10935
|
+
const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
|
|
10936
|
+
if (fs24.existsSync(cursorHooksPath)) {
|
|
10175
10937
|
try {
|
|
10176
|
-
const cur = JSON.parse(
|
|
10938
|
+
const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
|
|
10177
10939
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
10178
10940
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
10179
10941
|
);
|
|
@@ -10211,9 +10973,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10211
10973
|
|
|
10212
10974
|
// src/cli/commands/audit.ts
|
|
10213
10975
|
import chalk8 from "chalk";
|
|
10214
|
-
import
|
|
10215
|
-
import
|
|
10216
|
-
import
|
|
10976
|
+
import fs25 from "fs";
|
|
10977
|
+
import path27 from "path";
|
|
10978
|
+
import os21 from "os";
|
|
10217
10979
|
function formatRelativeTime(timestamp) {
|
|
10218
10980
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
10219
10981
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -10226,14 +10988,14 @@ function formatRelativeTime(timestamp) {
|
|
|
10226
10988
|
}
|
|
10227
10989
|
function registerAuditCommand(program2) {
|
|
10228
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) => {
|
|
10229
|
-
const logPath =
|
|
10230
|
-
if (!
|
|
10991
|
+
const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
|
|
10992
|
+
if (!fs25.existsSync(logPath)) {
|
|
10231
10993
|
console.log(
|
|
10232
10994
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
10233
10995
|
);
|
|
10234
10996
|
return;
|
|
10235
10997
|
}
|
|
10236
|
-
const raw =
|
|
10998
|
+
const raw = fs25.readFileSync(logPath, "utf-8");
|
|
10237
10999
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
10238
11000
|
let entries = lines.flatMap((line) => {
|
|
10239
11001
|
try {
|
|
@@ -10287,9 +11049,9 @@ function registerAuditCommand(program2) {
|
|
|
10287
11049
|
|
|
10288
11050
|
// src/cli/commands/report.ts
|
|
10289
11051
|
import chalk9 from "chalk";
|
|
10290
|
-
import
|
|
10291
|
-
import
|
|
10292
|
-
import
|
|
11052
|
+
import fs26 from "fs";
|
|
11053
|
+
import path28 from "path";
|
|
11054
|
+
import os22 from "os";
|
|
10293
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;
|
|
10294
11056
|
function buildTestTimestamps(allEntries) {
|
|
10295
11057
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -10336,8 +11098,8 @@ function getDateRange(period) {
|
|
|
10336
11098
|
}
|
|
10337
11099
|
}
|
|
10338
11100
|
function parseAuditLog(logPath) {
|
|
10339
|
-
if (!
|
|
10340
|
-
const raw =
|
|
11101
|
+
if (!fs26.existsSync(logPath)) return [];
|
|
11102
|
+
const raw = fs26.readFileSync(logPath, "utf-8");
|
|
10341
11103
|
return raw.split("\n").flatMap((line) => {
|
|
10342
11104
|
if (!line.trim()) return [];
|
|
10343
11105
|
try {
|
|
@@ -10363,9 +11125,9 @@ function colorBar(value, max, width) {
|
|
|
10363
11125
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
10364
11126
|
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
10365
11127
|
}
|
|
10366
|
-
function pct(
|
|
11128
|
+
function pct(num3, total) {
|
|
10367
11129
|
if (total === 0) return "\u2013";
|
|
10368
|
-
return Math.round(
|
|
11130
|
+
return Math.round(num3 / total * 100) + "%";
|
|
10369
11131
|
}
|
|
10370
11132
|
function fmtDate(d) {
|
|
10371
11133
|
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
@@ -10406,11 +11168,11 @@ function loadClaudeCost(start, end) {
|
|
|
10406
11168
|
inputTokens: 0,
|
|
10407
11169
|
cacheReadTokens: 0
|
|
10408
11170
|
};
|
|
10409
|
-
const projectsDir =
|
|
10410
|
-
if (!
|
|
11171
|
+
const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
|
|
11172
|
+
if (!fs26.existsSync(projectsDir)) return empty;
|
|
10411
11173
|
let dirs;
|
|
10412
11174
|
try {
|
|
10413
|
-
dirs =
|
|
11175
|
+
dirs = fs26.readdirSync(projectsDir);
|
|
10414
11176
|
} catch {
|
|
10415
11177
|
return empty;
|
|
10416
11178
|
}
|
|
@@ -10420,18 +11182,18 @@ function loadClaudeCost(start, end) {
|
|
|
10420
11182
|
const byDay = /* @__PURE__ */ new Map();
|
|
10421
11183
|
const byModel = /* @__PURE__ */ new Map();
|
|
10422
11184
|
for (const proj of dirs) {
|
|
10423
|
-
const projPath =
|
|
11185
|
+
const projPath = path28.join(projectsDir, proj);
|
|
10424
11186
|
let files;
|
|
10425
11187
|
try {
|
|
10426
|
-
const stat =
|
|
11188
|
+
const stat = fs26.statSync(projPath);
|
|
10427
11189
|
if (!stat.isDirectory()) continue;
|
|
10428
|
-
files =
|
|
11190
|
+
files = fs26.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
10429
11191
|
} catch {
|
|
10430
11192
|
continue;
|
|
10431
11193
|
}
|
|
10432
11194
|
for (const file of files) {
|
|
10433
11195
|
try {
|
|
10434
|
-
const raw =
|
|
11196
|
+
const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
|
|
10435
11197
|
for (const line of raw.split("\n")) {
|
|
10436
11198
|
if (!line.trim()) continue;
|
|
10437
11199
|
let entry;
|
|
@@ -10474,7 +11236,7 @@ function registerReportCommand(program2) {
|
|
|
10474
11236
|
const period = ["today", "7d", "30d", "month"].includes(
|
|
10475
11237
|
options.period
|
|
10476
11238
|
) ? options.period : "7d";
|
|
10477
|
-
const logPath =
|
|
11239
|
+
const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
|
|
10478
11240
|
const allEntries = parseAuditLog(logPath);
|
|
10479
11241
|
if (allEntries.length === 0) {
|
|
10480
11242
|
console.log(
|
|
@@ -10725,19 +11487,60 @@ init_daemon2();
|
|
|
10725
11487
|
init_daemon();
|
|
10726
11488
|
import chalk10 from "chalk";
|
|
10727
11489
|
import { spawn as spawn7 } from "child_process";
|
|
11490
|
+
var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
|
|
10728
11491
|
function registerDaemonCommand(program2) {
|
|
10729
|
-
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(
|
|
10730
11493
|
"-w, --watch",
|
|
10731
11494
|
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
10732
11495
|
).action(
|
|
10733
11496
|
async (action, options) => {
|
|
10734
11497
|
const cmd = (action ?? "start").toLowerCase();
|
|
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);
|
|
11511
|
+
}
|
|
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
|
+
}
|
|
10735
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
|
+
}
|
|
10736
11541
|
if (cmd === "status") return daemonStatus();
|
|
10737
11542
|
if (cmd !== "start" && action !== void 0) {
|
|
10738
|
-
console.error(
|
|
10739
|
-
chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
|
|
10740
|
-
);
|
|
11543
|
+
console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
|
|
10741
11544
|
process.exit(1);
|
|
10742
11545
|
}
|
|
10743
11546
|
if (options.watch) {
|
|
@@ -10788,12 +11591,12 @@ function registerDaemonCommand(program2) {
|
|
|
10788
11591
|
init_core();
|
|
10789
11592
|
init_daemon();
|
|
10790
11593
|
import chalk11 from "chalk";
|
|
10791
|
-
import
|
|
10792
|
-
import
|
|
10793
|
-
import
|
|
10794
|
-
function readJson2(filePath) {
|
|
11594
|
+
import fs27 from "fs";
|
|
11595
|
+
import path29 from "path";
|
|
11596
|
+
import os23 from "os";
|
|
11597
|
+
function readJson2(filePath) {
|
|
10795
11598
|
try {
|
|
10796
|
-
if (
|
|
11599
|
+
if (fs27.existsSync(filePath)) return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
|
|
10797
11600
|
} catch {
|
|
10798
11601
|
}
|
|
10799
11602
|
return null;
|
|
@@ -10858,28 +11661,28 @@ function registerStatusCommand(program2) {
|
|
|
10858
11661
|
console.log("");
|
|
10859
11662
|
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10860
11663
|
console.log(` Mode: ${modeLabel}`);
|
|
10861
|
-
const projectConfig =
|
|
10862
|
-
const globalConfig =
|
|
11664
|
+
const projectConfig = path29.join(process.cwd(), "node9.config.json");
|
|
11665
|
+
const globalConfig = path29.join(os23.homedir(), ".node9", "config.json");
|
|
10863
11666
|
console.log(
|
|
10864
|
-
` Local: ${
|
|
11667
|
+
` Local: ${fs27.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10865
11668
|
);
|
|
10866
11669
|
console.log(
|
|
10867
|
-
` Global: ${
|
|
11670
|
+
` Global: ${fs27.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10868
11671
|
);
|
|
10869
11672
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10870
11673
|
console.log(
|
|
10871
11674
|
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10872
11675
|
);
|
|
10873
11676
|
}
|
|
10874
|
-
const homeDir2 =
|
|
11677
|
+
const homeDir2 = os23.homedir();
|
|
10875
11678
|
const claudeSettings = readJson2(
|
|
10876
|
-
|
|
11679
|
+
path29.join(homeDir2, ".claude", "settings.json")
|
|
10877
11680
|
);
|
|
10878
|
-
const claudeConfig = readJson2(
|
|
11681
|
+
const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
|
|
10879
11682
|
const geminiSettings = readJson2(
|
|
10880
|
-
|
|
11683
|
+
path29.join(homeDir2, ".gemini", "settings.json")
|
|
10881
11684
|
);
|
|
10882
|
-
const cursorConfig = readJson2(
|
|
11685
|
+
const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
|
|
10883
11686
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10884
11687
|
if (agentFound) {
|
|
10885
11688
|
console.log("");
|
|
@@ -10939,11 +11742,12 @@ function registerStatusCommand(program2) {
|
|
|
10939
11742
|
// src/cli/commands/init.ts
|
|
10940
11743
|
init_core();
|
|
10941
11744
|
import chalk12 from "chalk";
|
|
10942
|
-
import
|
|
10943
|
-
import
|
|
10944
|
-
import
|
|
10945
|
-
import
|
|
11745
|
+
import fs28 from "fs";
|
|
11746
|
+
import path30 from "path";
|
|
11747
|
+
import os24 from "os";
|
|
11748
|
+
import https3 from "https";
|
|
10946
11749
|
init_shields();
|
|
11750
|
+
init_service();
|
|
10947
11751
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10948
11752
|
function fireTelemetryPing(agents) {
|
|
10949
11753
|
try {
|
|
@@ -10953,7 +11757,7 @@ function fireTelemetryPing(agents) {
|
|
|
10953
11757
|
os: process.platform,
|
|
10954
11758
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10955
11759
|
});
|
|
10956
|
-
const req =
|
|
11760
|
+
const req = https3.request(
|
|
10957
11761
|
{
|
|
10958
11762
|
hostname: "api.node9.ai",
|
|
10959
11763
|
path: "/api/v1/telemetry",
|
|
@@ -11000,15 +11804,15 @@ function registerInitCommand(program2) {
|
|
|
11000
11804
|
}
|
|
11001
11805
|
console.log("");
|
|
11002
11806
|
}
|
|
11003
|
-
const configPath =
|
|
11004
|
-
if (
|
|
11807
|
+
const configPath = path30.join(os24.homedir(), ".node9", "config.json");
|
|
11808
|
+
if (fs28.existsSync(configPath) && !options.force) {
|
|
11005
11809
|
try {
|
|
11006
|
-
const existing = JSON.parse(
|
|
11810
|
+
const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
11007
11811
|
const settings = existing.settings ?? {};
|
|
11008
11812
|
if (settings.mode !== chosenMode) {
|
|
11009
11813
|
settings.mode = chosenMode;
|
|
11010
11814
|
existing.settings = settings;
|
|
11011
|
-
|
|
11815
|
+
fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
11012
11816
|
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
11013
11817
|
} else {
|
|
11014
11818
|
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
@@ -11021,9 +11825,9 @@ function registerInitCommand(program2) {
|
|
|
11021
11825
|
...DEFAULT_CONFIG,
|
|
11022
11826
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
11023
11827
|
};
|
|
11024
|
-
const dir =
|
|
11025
|
-
if (!
|
|
11026
|
-
|
|
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");
|
|
11027
11831
|
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
11028
11832
|
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
11029
11833
|
}
|
|
@@ -11035,9 +11839,13 @@ function registerInitCommand(program2) {
|
|
|
11035
11839
|
);
|
|
11036
11840
|
if (found.length === 0) {
|
|
11037
11841
|
console.log(
|
|
11038
|
-
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>")
|
|
11039
11848
|
);
|
|
11040
|
-
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
11041
11849
|
return;
|
|
11042
11850
|
}
|
|
11043
11851
|
console.log(chalk12.bold("Detected agents:"));
|
|
@@ -11051,6 +11859,32 @@ function registerInitCommand(program2) {
|
|
|
11051
11859
|
else if (agent === "gemini") await setupGemini();
|
|
11052
11860
|
else if (agent === "cursor") await setupCursor();
|
|
11053
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
|
+
}
|
|
11054
11888
|
console.log("");
|
|
11055
11889
|
}
|
|
11056
11890
|
{
|
|
@@ -11077,7 +11911,7 @@ function registerInitCommand(program2) {
|
|
|
11077
11911
|
}
|
|
11078
11912
|
|
|
11079
11913
|
// src/cli/commands/undo.ts
|
|
11080
|
-
import
|
|
11914
|
+
import path31 from "path";
|
|
11081
11915
|
import chalk14 from "chalk";
|
|
11082
11916
|
|
|
11083
11917
|
// src/tui/undo-navigator.ts
|
|
@@ -11236,7 +12070,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
11236
12070
|
let dir = startDir;
|
|
11237
12071
|
while (true) {
|
|
11238
12072
|
if (cwds.has(dir)) return dir;
|
|
11239
|
-
const parent =
|
|
12073
|
+
const parent = path31.dirname(dir);
|
|
11240
12074
|
if (parent === dir) return null;
|
|
11241
12075
|
dir = parent;
|
|
11242
12076
|
}
|
|
@@ -11365,7 +12199,7 @@ function registerUndoCommand(program2) {
|
|
|
11365
12199
|
// src/cli/commands/watch.ts
|
|
11366
12200
|
init_daemon();
|
|
11367
12201
|
import chalk15 from "chalk";
|
|
11368
|
-
import { spawn as spawn8, spawnSync as
|
|
12202
|
+
import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
|
|
11369
12203
|
function registerWatchCommand(program2) {
|
|
11370
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) => {
|
|
11371
12205
|
let port = DAEMON_PORT;
|
|
@@ -11411,7 +12245,7 @@ function registerWatchCommand(program2) {
|
|
|
11411
12245
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
11412
12246
|
)
|
|
11413
12247
|
);
|
|
11414
|
-
const result =
|
|
12248
|
+
const result = spawnSync6(cmd, args, {
|
|
11415
12249
|
stdio: "inherit",
|
|
11416
12250
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
11417
12251
|
});
|
|
@@ -11432,12 +12266,12 @@ import { execa as execa2 } from "execa";
|
|
|
11432
12266
|
init_provenance();
|
|
11433
12267
|
|
|
11434
12268
|
// src/mcp-pin.ts
|
|
11435
|
-
import
|
|
11436
|
-
import
|
|
11437
|
-
import
|
|
12269
|
+
import fs29 from "fs";
|
|
12270
|
+
import path32 from "path";
|
|
12271
|
+
import os25 from "os";
|
|
11438
12272
|
import crypto4 from "crypto";
|
|
11439
12273
|
function getPinsFilePath() {
|
|
11440
|
-
return
|
|
12274
|
+
return path32.join(os25.homedir(), ".node9", "mcp-pins.json");
|
|
11441
12275
|
}
|
|
11442
12276
|
function hashToolDefinitions(tools) {
|
|
11443
12277
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -11454,7 +12288,7 @@ function getServerKey(upstreamCommand) {
|
|
|
11454
12288
|
function readMcpPinsSafe() {
|
|
11455
12289
|
const filePath = getPinsFilePath();
|
|
11456
12290
|
try {
|
|
11457
|
-
const raw =
|
|
12291
|
+
const raw = fs29.readFileSync(filePath, "utf-8");
|
|
11458
12292
|
if (!raw.trim()) {
|
|
11459
12293
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
11460
12294
|
}
|
|
@@ -11478,10 +12312,10 @@ function readMcpPins() {
|
|
|
11478
12312
|
}
|
|
11479
12313
|
function writeMcpPins(data) {
|
|
11480
12314
|
const filePath = getPinsFilePath();
|
|
11481
|
-
|
|
12315
|
+
fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
|
|
11482
12316
|
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
11483
|
-
|
|
11484
|
-
|
|
12317
|
+
fs29.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
12318
|
+
fs29.renameSync(tmp, filePath);
|
|
11485
12319
|
}
|
|
11486
12320
|
function checkPin(serverKey, currentHash) {
|
|
11487
12321
|
const result = readMcpPinsSafe();
|
|
@@ -11853,9 +12687,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11853
12687
|
|
|
11854
12688
|
// src/mcp-server/index.ts
|
|
11855
12689
|
import readline4 from "readline";
|
|
11856
|
-
import
|
|
11857
|
-
import
|
|
11858
|
-
import
|
|
12690
|
+
import fs30 from "fs";
|
|
12691
|
+
import os26 from "os";
|
|
12692
|
+
import path33 from "path";
|
|
11859
12693
|
init_core();
|
|
11860
12694
|
init_daemon();
|
|
11861
12695
|
init_shields();
|
|
@@ -12030,13 +12864,13 @@ function handleStatus() {
|
|
|
12030
12864
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
12031
12865
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
12032
12866
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
12033
|
-
const projectConfig =
|
|
12034
|
-
const globalConfig =
|
|
12867
|
+
const projectConfig = path33.join(process.cwd(), "node9.config.json");
|
|
12868
|
+
const globalConfig = path33.join(os26.homedir(), ".node9", "config.json");
|
|
12035
12869
|
lines.push(
|
|
12036
|
-
`Project config (node9.config.json): ${
|
|
12870
|
+
`Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
|
|
12037
12871
|
);
|
|
12038
12872
|
lines.push(
|
|
12039
|
-
`Global config (~/.node9/config.json): ${
|
|
12873
|
+
`Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
|
|
12040
12874
|
);
|
|
12041
12875
|
return lines.join("\n");
|
|
12042
12876
|
}
|
|
@@ -12110,21 +12944,21 @@ function handleShieldDisable(args) {
|
|
|
12110
12944
|
writeActiveShields(active.filter((s) => s !== name));
|
|
12111
12945
|
return `Shield "${name}" disabled.`;
|
|
12112
12946
|
}
|
|
12113
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
12947
|
+
var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
|
|
12114
12948
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
12115
12949
|
function readGlobalConfigRaw() {
|
|
12116
12950
|
try {
|
|
12117
|
-
if (
|
|
12118
|
-
return JSON.parse(
|
|
12951
|
+
if (fs30.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
12952
|
+
return JSON.parse(fs30.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
12119
12953
|
}
|
|
12120
12954
|
} catch {
|
|
12121
12955
|
}
|
|
12122
12956
|
return {};
|
|
12123
12957
|
}
|
|
12124
12958
|
function writeGlobalConfigRaw(data) {
|
|
12125
|
-
const dir =
|
|
12126
|
-
if (!
|
|
12127
|
-
|
|
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");
|
|
12128
12962
|
}
|
|
12129
12963
|
function handleApproverList() {
|
|
12130
12964
|
const config = getConfig();
|
|
@@ -12167,9 +13001,9 @@ function handleApproverSet(args) {
|
|
|
12167
13001
|
}
|
|
12168
13002
|
function handleAuditGet(args) {
|
|
12169
13003
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
12170
|
-
const auditPath =
|
|
12171
|
-
if (!
|
|
12172
|
-
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);
|
|
12173
13007
|
const recent = lines.slice(-limit);
|
|
12174
13008
|
const entries = recent.map((line) => {
|
|
12175
13009
|
try {
|
|
@@ -12487,25 +13321,950 @@ function registerMcpPinCommand(program2) {
|
|
|
12487
13321
|
});
|
|
12488
13322
|
}
|
|
12489
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
|
+
|
|
12490
14249
|
// src/cli.ts
|
|
12491
14250
|
var { version } = JSON.parse(
|
|
12492
|
-
|
|
14251
|
+
fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
|
|
12493
14252
|
);
|
|
12494
14253
|
var program = new Command();
|
|
12495
14254
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
12496
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) => {
|
|
12497
|
-
const
|
|
12498
|
-
const credPath =
|
|
12499
|
-
if (!
|
|
12500
|
-
|
|
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 });
|
|
12501
14260
|
const profileName = options.profile || "default";
|
|
12502
14261
|
let existingCreds = {};
|
|
12503
14262
|
try {
|
|
12504
|
-
if (
|
|
12505
|
-
const raw = JSON.parse(
|
|
14263
|
+
if (fs35.existsSync(credPath)) {
|
|
14264
|
+
const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
|
|
12506
14265
|
if (raw.apiKey) {
|
|
12507
14266
|
existingCreds = {
|
|
12508
|
-
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl ||
|
|
14267
|
+
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
12509
14268
|
};
|
|
12510
14269
|
} else {
|
|
12511
14270
|
existingCreds = raw;
|
|
@@ -12513,14 +14272,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
12513
14272
|
}
|
|
12514
14273
|
} catch {
|
|
12515
14274
|
}
|
|
12516
|
-
existingCreds[profileName] = { apiKey, apiUrl:
|
|
12517
|
-
|
|
14275
|
+
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
14276
|
+
fs35.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
12518
14277
|
if (profileName === "default") {
|
|
12519
|
-
const configPath =
|
|
14278
|
+
const configPath = path38.join(os31.homedir(), ".node9", "config.json");
|
|
12520
14279
|
let config = {};
|
|
12521
14280
|
try {
|
|
12522
|
-
if (
|
|
12523
|
-
config = JSON.parse(
|
|
14281
|
+
if (fs35.existsSync(configPath))
|
|
14282
|
+
config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
12524
14283
|
} catch {
|
|
12525
14284
|
}
|
|
12526
14285
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -12535,39 +14294,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
12535
14294
|
approvers.cloud = false;
|
|
12536
14295
|
}
|
|
12537
14296
|
s.approvers = approvers;
|
|
12538
|
-
if (!
|
|
12539
|
-
|
|
12540
|
-
|
|
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 });
|
|
12541
14300
|
}
|
|
12542
14301
|
if (options.profile && profileName !== "default") {
|
|
12543
|
-
console.log(
|
|
12544
|
-
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`));
|
|
12545
14304
|
} else if (options.local) {
|
|
12546
|
-
console.log(
|
|
12547
|
-
console.log(
|
|
14305
|
+
console.log(chalk24.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
14306
|
+
console.log(chalk24.gray(` All decisions stay on this machine.`));
|
|
12548
14307
|
} else {
|
|
12549
|
-
console.log(
|
|
12550
|
-
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.`));
|
|
12551
14310
|
}
|
|
12552
14311
|
});
|
|
12553
|
-
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
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) => {
|
|
12554
14313
|
if (target === "gemini") return await setupGemini();
|
|
12555
14314
|
if (target === "claude") return await setupClaude();
|
|
12556
14315
|
if (target === "cursor") return await setupCursor();
|
|
14316
|
+
if (target === "windsurf") return await setupWindsurf();
|
|
14317
|
+
if (target === "vscode") return await setupVSCode();
|
|
12557
14318
|
if (target === "hud") return setupHud();
|
|
12558
|
-
console.error(
|
|
14319
|
+
console.error(
|
|
14320
|
+
chalk24.red(
|
|
14321
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14322
|
+
)
|
|
14323
|
+
);
|
|
12559
14324
|
process.exit(1);
|
|
12560
14325
|
});
|
|
12561
|
-
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
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) => {
|
|
12562
14327
|
if (!target) {
|
|
12563
|
-
console.log(
|
|
12564
|
-
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");
|
|
12565
14330
|
console.log(" Targets:");
|
|
12566
|
-
console.log(" " +
|
|
12567
|
-
console.log(" " +
|
|
12568
|
-
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)");
|
|
12569
14336
|
process.stdout.write(
|
|
12570
|
-
" " +
|
|
14337
|
+
" " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
12571
14338
|
);
|
|
12572
14339
|
console.log("");
|
|
12573
14340
|
return;
|
|
@@ -12576,93 +14343,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
12576
14343
|
if (t === "gemini") return await setupGemini();
|
|
12577
14344
|
if (t === "claude") return await setupClaude();
|
|
12578
14345
|
if (t === "cursor") return await setupCursor();
|
|
14346
|
+
if (t === "windsurf") return await setupWindsurf();
|
|
14347
|
+
if (t === "vscode") return await setupVSCode();
|
|
12579
14348
|
if (t === "hud") return setupHud();
|
|
12580
|
-
console.error(
|
|
14349
|
+
console.error(
|
|
14350
|
+
chalk24.red(
|
|
14351
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14352
|
+
)
|
|
14353
|
+
);
|
|
12581
14354
|
process.exit(1);
|
|
12582
14355
|
});
|
|
12583
|
-
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) => {
|
|
12584
14360
|
let fn;
|
|
12585
14361
|
if (target === "claude") fn = teardownClaude;
|
|
12586
14362
|
else if (target === "gemini") fn = teardownGemini;
|
|
12587
14363
|
else if (target === "cursor") fn = teardownCursor;
|
|
14364
|
+
else if (target === "windsurf") fn = teardownWindsurf;
|
|
14365
|
+
else if (target === "vscode") fn = teardownVSCode;
|
|
12588
14366
|
else if (target === "hud") fn = teardownHud;
|
|
12589
14367
|
else {
|
|
12590
14368
|
console.error(
|
|
12591
|
-
|
|
14369
|
+
chalk24.red(
|
|
14370
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
14371
|
+
)
|
|
12592
14372
|
);
|
|
12593
14373
|
process.exit(1);
|
|
12594
14374
|
}
|
|
12595
|
-
console.log(
|
|
14375
|
+
console.log(chalk24.cyan(`
|
|
12596
14376
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
12597
14377
|
`));
|
|
12598
14378
|
try {
|
|
12599
14379
|
fn();
|
|
12600
14380
|
} catch (err2) {
|
|
12601
|
-
console.error(
|
|
14381
|
+
console.error(chalk24.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12602
14382
|
process.exit(1);
|
|
12603
14383
|
}
|
|
12604
|
-
console.log(
|
|
14384
|
+
console.log(chalk24.gray("\n Restart the agent for changes to take effect."));
|
|
12605
14385
|
});
|
|
12606
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) => {
|
|
12607
|
-
console.log(
|
|
12608
|
-
console.log(
|
|
14387
|
+
console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
14388
|
+
console.log(chalk24.bold("Stopping daemon..."));
|
|
12609
14389
|
try {
|
|
12610
14390
|
stopDaemon();
|
|
12611
|
-
console.log(
|
|
14391
|
+
console.log(chalk24.green(" \u2705 Daemon stopped"));
|
|
12612
14392
|
} catch {
|
|
12613
|
-
console.log(
|
|
14393
|
+
console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
|
|
12614
14394
|
}
|
|
12615
|
-
console.log(
|
|
14395
|
+
console.log(chalk24.bold("\nRemoving hooks..."));
|
|
12616
14396
|
let teardownFailed = false;
|
|
12617
14397
|
for (const [label, fn] of [
|
|
12618
14398
|
["Claude", teardownClaude],
|
|
12619
14399
|
["Gemini", teardownGemini],
|
|
12620
|
-
["Cursor", teardownCursor]
|
|
14400
|
+
["Cursor", teardownCursor],
|
|
14401
|
+
["Windsurf", teardownWindsurf],
|
|
14402
|
+
["VSCode", teardownVSCode]
|
|
12621
14403
|
]) {
|
|
12622
14404
|
try {
|
|
12623
14405
|
fn();
|
|
12624
14406
|
} catch (err2) {
|
|
12625
14407
|
teardownFailed = true;
|
|
12626
14408
|
console.error(
|
|
12627
|
-
|
|
14409
|
+
chalk24.red(
|
|
12628
14410
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
12629
14411
|
)
|
|
12630
14412
|
);
|
|
12631
14413
|
}
|
|
12632
14414
|
}
|
|
12633
14415
|
if (options.purge) {
|
|
12634
|
-
const node9Dir =
|
|
12635
|
-
if (
|
|
14416
|
+
const node9Dir = path38.join(os31.homedir(), ".node9");
|
|
14417
|
+
if (fs35.existsSync(node9Dir)) {
|
|
12636
14418
|
const confirmed = await confirm2({
|
|
12637
14419
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
12638
14420
|
default: false
|
|
12639
14421
|
});
|
|
12640
14422
|
if (confirmed) {
|
|
12641
|
-
|
|
12642
|
-
if (
|
|
14423
|
+
fs35.rmSync(node9Dir, { recursive: true });
|
|
14424
|
+
if (fs35.existsSync(node9Dir)) {
|
|
12643
14425
|
console.error(
|
|
12644
|
-
|
|
14426
|
+
chalk24.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
12645
14427
|
);
|
|
12646
14428
|
} else {
|
|
12647
|
-
console.log(
|
|
14429
|
+
console.log(chalk24.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
12648
14430
|
}
|
|
12649
14431
|
} else {
|
|
12650
|
-
console.log(
|
|
14432
|
+
console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
12651
14433
|
}
|
|
12652
14434
|
} else {
|
|
12653
|
-
console.log(
|
|
14435
|
+
console.log(chalk24.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
12654
14436
|
}
|
|
12655
14437
|
} else {
|
|
12656
14438
|
console.log(
|
|
12657
|
-
|
|
14439
|
+
chalk24.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
12658
14440
|
);
|
|
12659
14441
|
}
|
|
12660
14442
|
if (teardownFailed) {
|
|
12661
|
-
console.error(
|
|
14443
|
+
console.error(chalk24.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
12662
14444
|
process.exit(1);
|
|
12663
14445
|
}
|
|
12664
|
-
console.log(
|
|
12665
|
-
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"));
|
|
12666
14448
|
});
|
|
12667
14449
|
registerDoctorCommand(program, version);
|
|
12668
14450
|
program.command("explain").description(
|
|
@@ -12675,7 +14457,7 @@ program.command("explain").description(
|
|
|
12675
14457
|
try {
|
|
12676
14458
|
args = JSON.parse(trimmed);
|
|
12677
14459
|
} catch {
|
|
12678
|
-
console.error(
|
|
14460
|
+
console.error(chalk24.red(`
|
|
12679
14461
|
\u274C Invalid JSON: ${trimmed}
|
|
12680
14462
|
`));
|
|
12681
14463
|
process.exit(1);
|
|
@@ -12686,54 +14468,54 @@ program.command("explain").description(
|
|
|
12686
14468
|
}
|
|
12687
14469
|
const result = await explainPolicy(tool, args);
|
|
12688
14470
|
console.log("");
|
|
12689
|
-
console.log(
|
|
14471
|
+
console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
12690
14472
|
console.log("");
|
|
12691
|
-
console.log(` ${
|
|
14473
|
+
console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
|
|
12692
14474
|
if (argsRaw) {
|
|
12693
|
-
const
|
|
12694
|
-
console.log(` ${
|
|
14475
|
+
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
14476
|
+
console.log(` ${chalk24.bold("Input:")} ${chalk24.gray(preview2)}`);
|
|
12695
14477
|
}
|
|
12696
14478
|
console.log("");
|
|
12697
|
-
console.log(
|
|
14479
|
+
console.log(chalk24.bold("Config Sources (Waterfall):"));
|
|
12698
14480
|
for (const tier of result.waterfall) {
|
|
12699
|
-
const
|
|
14481
|
+
const num3 = chalk24.gray(` ${tier.tier}.`);
|
|
12700
14482
|
const label = tier.label.padEnd(16);
|
|
12701
14483
|
let statusStr;
|
|
12702
14484
|
if (tier.tier === 1) {
|
|
12703
|
-
statusStr =
|
|
14485
|
+
statusStr = chalk24.gray(tier.note ?? "");
|
|
12704
14486
|
} else if (tier.status === "active") {
|
|
12705
|
-
const loc = tier.path ?
|
|
12706
|
-
const note = tier.note ?
|
|
12707
|
-
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 : "");
|
|
12708
14490
|
} else {
|
|
12709
|
-
statusStr =
|
|
14491
|
+
statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
|
|
12710
14492
|
}
|
|
12711
|
-
console.log(`${
|
|
14493
|
+
console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
|
|
12712
14494
|
}
|
|
12713
14495
|
console.log("");
|
|
12714
|
-
console.log(
|
|
14496
|
+
console.log(chalk24.bold("Policy Evaluation:"));
|
|
12715
14497
|
for (const step of result.steps) {
|
|
12716
14498
|
const isFinal = step.isFinal;
|
|
12717
14499
|
let icon;
|
|
12718
|
-
if (step.outcome === "allow") icon =
|
|
12719
|
-
else if (step.outcome === "review") icon =
|
|
12720
|
-
else if (step.outcome === "skip") icon =
|
|
12721
|
-
else icon =
|
|
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 ");
|
|
12722
14504
|
const name = step.name.padEnd(18);
|
|
12723
|
-
const nameStr = isFinal ?
|
|
12724
|
-
const detail = isFinal ?
|
|
12725
|
-
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") : "";
|
|
12726
14508
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
12727
14509
|
}
|
|
12728
14510
|
console.log("");
|
|
12729
14511
|
if (result.decision === "allow") {
|
|
12730
|
-
console.log(
|
|
14512
|
+
console.log(chalk24.green.bold(" Decision: \u2705 ALLOW") + chalk24.gray(" \u2014 no approval needed"));
|
|
12731
14513
|
} else {
|
|
12732
14514
|
console.log(
|
|
12733
|
-
|
|
14515
|
+
chalk24.red.bold(" Decision: \u{1F534} REVIEW") + chalk24.gray(" \u2014 human approval required")
|
|
12734
14516
|
);
|
|
12735
14517
|
if (result.blockedByLabel) {
|
|
12736
|
-
console.log(
|
|
14518
|
+
console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
|
|
12737
14519
|
}
|
|
12738
14520
|
}
|
|
12739
14521
|
console.log("");
|
|
@@ -12748,7 +14530,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
12748
14530
|
try {
|
|
12749
14531
|
await startTail2(options);
|
|
12750
14532
|
} catch (err2) {
|
|
12751
|
-
console.error(
|
|
14533
|
+
console.error(chalk24.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
12752
14534
|
process.exit(1);
|
|
12753
14535
|
}
|
|
12754
14536
|
});
|
|
@@ -12780,14 +14562,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
12780
14562
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
12781
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) => {
|
|
12782
14564
|
if (subcommand === "debug") {
|
|
12783
|
-
const flagFile =
|
|
14565
|
+
const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
|
|
12784
14566
|
if (state === "on") {
|
|
12785
|
-
|
|
12786
|
-
|
|
14567
|
+
fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
|
|
14568
|
+
fs35.writeFileSync(flagFile, "");
|
|
12787
14569
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
12788
14570
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
12789
14571
|
} else if (state === "off") {
|
|
12790
|
-
if (
|
|
14572
|
+
if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
|
|
12791
14573
|
console.log("HUD debug logging disabled.");
|
|
12792
14574
|
} else {
|
|
12793
14575
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12802,7 +14584,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12802
14584
|
const ms = parseDuration(options.duration);
|
|
12803
14585
|
if (ms === null) {
|
|
12804
14586
|
console.error(
|
|
12805
|
-
|
|
14587
|
+
chalk24.red(`
|
|
12806
14588
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12807
14589
|
`)
|
|
12808
14590
|
);
|
|
@@ -12810,20 +14592,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12810
14592
|
}
|
|
12811
14593
|
pauseNode9(ms, options.duration);
|
|
12812
14594
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12813
|
-
console.log(
|
|
14595
|
+
console.log(chalk24.yellow(`
|
|
12814
14596
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12815
|
-
console.log(
|
|
12816
|
-
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.
|
|
12817
14599
|
`));
|
|
12818
14600
|
});
|
|
12819
14601
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12820
14602
|
const { paused } = checkPause();
|
|
12821
14603
|
if (!paused) {
|
|
12822
|
-
console.log(
|
|
14604
|
+
console.log(chalk24.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12823
14605
|
return;
|
|
12824
14606
|
}
|
|
12825
14607
|
resumeNode9();
|
|
12826
|
-
console.log(
|
|
14608
|
+
console.log(chalk24.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12827
14609
|
});
|
|
12828
14610
|
var HOOK_BASED_AGENTS = {
|
|
12829
14611
|
claude: "claude",
|
|
@@ -12836,15 +14618,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12836
14618
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12837
14619
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12838
14620
|
console.error(
|
|
12839
|
-
|
|
14621
|
+
chalk24.yellow(`
|
|
12840
14622
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12841
14623
|
);
|
|
12842
|
-
console.error(
|
|
14624
|
+
console.error(chalk24.white(`
|
|
12843
14625
|
"${target}" uses its own hook system. Use:`));
|
|
12844
14626
|
console.error(
|
|
12845
|
-
|
|
14627
|
+
chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
|
|
12846
14628
|
);
|
|
12847
|
-
console.error(
|
|
14629
|
+
console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
|
|
12848
14630
|
process.exit(1);
|
|
12849
14631
|
}
|
|
12850
14632
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12861,7 +14643,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12861
14643
|
}
|
|
12862
14644
|
);
|
|
12863
14645
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12864
|
-
console.error(
|
|
14646
|
+
console.error(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12865
14647
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12866
14648
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12867
14649
|
}
|
|
@@ -12874,12 +14656,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12874
14656
|
}
|
|
12875
14657
|
if (!result.approved) {
|
|
12876
14658
|
console.error(
|
|
12877
|
-
|
|
14659
|
+
chalk24.red(`
|
|
12878
14660
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12879
14661
|
);
|
|
12880
14662
|
process.exit(1);
|
|
12881
14663
|
}
|
|
12882
|
-
console.error(
|
|
14664
|
+
console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12883
14665
|
await runProxy(fullCommand);
|
|
12884
14666
|
} else {
|
|
12885
14667
|
program.help();
|
|
@@ -12889,14 +14671,18 @@ registerUndoCommand(program);
|
|
|
12889
14671
|
registerShieldCommand(program);
|
|
12890
14672
|
registerConfigShowCommand(program);
|
|
12891
14673
|
registerTrustCommand(program);
|
|
14674
|
+
registerSyncCommand(program);
|
|
14675
|
+
registerAgentsCommand(program);
|
|
14676
|
+
registerScanCommand(program);
|
|
14677
|
+
registerSessionsCommand(program);
|
|
12892
14678
|
if (process.argv[2] !== "daemon") {
|
|
12893
14679
|
process.on("unhandledRejection", (reason) => {
|
|
12894
14680
|
const isCheckHook = process.argv[2] === "check";
|
|
12895
14681
|
if (isCheckHook) {
|
|
12896
14682
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12897
|
-
const logPath =
|
|
14683
|
+
const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
|
|
12898
14684
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12899
|
-
|
|
14685
|
+
fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12900
14686
|
`);
|
|
12901
14687
|
}
|
|
12902
14688
|
process.exit(0);
|