@node9/proxy 1.11.2 → 1.11.4
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 +39 -31
- package/dist/cli.js +2029 -286
- package/dist/cli.mjs +2023 -280
- package/dist/index.js +466 -76
- package/dist/index.mjs +466 -76
- package/dist/shields/builtin/bash-safe.json +18 -4
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -97,7 +97,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
97
97
|
}
|
|
98
98
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
99
99
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
100
|
-
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
100
|
+
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
101
101
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
102
102
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
103
103
|
tool: toolName,
|
|
@@ -764,7 +764,7 @@ var init_config = __esm({
|
|
|
764
764
|
// 120-second auto-deny timeout
|
|
765
765
|
flightRecorder: true,
|
|
766
766
|
auditHashArgs: true,
|
|
767
|
-
approvers: { native: true, browser:
|
|
767
|
+
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
768
768
|
cloudSyncIntervalHours: 5
|
|
769
769
|
},
|
|
770
770
|
policy: {
|
|
@@ -871,7 +871,7 @@ var init_config = __esm({
|
|
|
871
871
|
},
|
|
872
872
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
873
873
|
{
|
|
874
|
-
name: "
|
|
874
|
+
name: "review-force-push",
|
|
875
875
|
tool: "bash",
|
|
876
876
|
conditions: [
|
|
877
877
|
{
|
|
@@ -884,8 +884,8 @@ var init_config = __esm({
|
|
|
884
884
|
}
|
|
885
885
|
],
|
|
886
886
|
conditionMode: "all",
|
|
887
|
-
verdict: "
|
|
888
|
-
reason: "Force push
|
|
887
|
+
verdict: "review",
|
|
888
|
+
reason: "Force push rewrites remote history \u2014 confirm this is intentional",
|
|
889
889
|
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
890
890
|
},
|
|
891
891
|
{
|
|
@@ -895,14 +895,16 @@ var init_config = __esm({
|
|
|
895
895
|
{
|
|
896
896
|
field: "command",
|
|
897
897
|
op: "matches",
|
|
898
|
-
|
|
898
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
899
|
+
// "git reset --hard" as a string don't false-positive.
|
|
900
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
899
901
|
flags: "i"
|
|
900
902
|
},
|
|
901
903
|
{
|
|
902
904
|
field: "command",
|
|
903
905
|
op: "notMatches",
|
|
904
|
-
// Exclude recovery ops
|
|
905
|
-
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
|
|
906
|
+
// Exclude recovery ops and routine branch-surgery (--onto) — these are not destructive.
|
|
907
|
+
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
|
|
906
908
|
flags: "i"
|
|
907
909
|
}
|
|
908
910
|
],
|
|
@@ -1130,8 +1132,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1130
1132
|
}
|
|
1131
1133
|
if (typeof args === "string") {
|
|
1132
1134
|
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
1135
|
+
const textLower = text.toLowerCase();
|
|
1133
1136
|
for (const pattern of DLP_PATTERNS) {
|
|
1137
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1134
1140
|
if (pattern.regex.test(text)) {
|
|
1141
|
+
const matchedValue = (text.match(pattern.regex)?.[0] ?? "").toLowerCase();
|
|
1142
|
+
if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
|
|
1135
1143
|
return {
|
|
1136
1144
|
patternName: pattern.name,
|
|
1137
1145
|
fieldPath,
|
|
@@ -1156,8 +1164,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1156
1164
|
}
|
|
1157
1165
|
function scanText(text) {
|
|
1158
1166
|
const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
|
|
1167
|
+
const tLower = t.toLowerCase();
|
|
1159
1168
|
for (const pattern of DLP_PATTERNS) {
|
|
1169
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => tLower.includes(kw.toLowerCase()))) {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1160
1172
|
if (pattern.regex.test(t)) {
|
|
1173
|
+
const matchedValue = (t.match(pattern.regex)?.[0] ?? "").toLowerCase();
|
|
1174
|
+
if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
|
|
1161
1175
|
return {
|
|
1162
1176
|
patternName: pattern.name,
|
|
1163
1177
|
fieldPath: "response-text",
|
|
@@ -1168,36 +1182,313 @@ function scanText(text) {
|
|
|
1168
1182
|
}
|
|
1169
1183
|
return null;
|
|
1170
1184
|
}
|
|
1171
|
-
var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
1185
|
+
var DLP_STOPWORDS, DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
1172
1186
|
var init_dlp = __esm({
|
|
1173
1187
|
"src/dlp.ts"() {
|
|
1174
1188
|
"use strict";
|
|
1189
|
+
DLP_STOPWORDS = [
|
|
1190
|
+
"example",
|
|
1191
|
+
"placeholder",
|
|
1192
|
+
"changeme",
|
|
1193
|
+
"your_key",
|
|
1194
|
+
"your_token",
|
|
1195
|
+
"your_secret",
|
|
1196
|
+
"replace_me",
|
|
1197
|
+
"insert_key",
|
|
1198
|
+
"put_your",
|
|
1199
|
+
"fake",
|
|
1200
|
+
"dummy",
|
|
1201
|
+
"sample",
|
|
1202
|
+
"xxxxxxxx",
|
|
1203
|
+
"aaaaaa",
|
|
1204
|
+
"bbbbbb",
|
|
1205
|
+
"00000000",
|
|
1206
|
+
"${",
|
|
1207
|
+
"{{",
|
|
1208
|
+
"%{",
|
|
1209
|
+
"<your",
|
|
1210
|
+
"test_key",
|
|
1211
|
+
"test_token"
|
|
1212
|
+
];
|
|
1175
1213
|
DLP_PATTERNS = [
|
|
1176
|
-
|
|
1177
|
-
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
1178
|
-
// Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
|
|
1179
|
-
// lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
|
|
1180
|
-
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
|
|
1181
|
-
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
1182
|
-
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
1214
|
+
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
1183
1215
|
{
|
|
1184
|
-
name: "
|
|
1185
|
-
regex:
|
|
1186
|
-
severity: "block"
|
|
1216
|
+
name: "AWS Access Key ID",
|
|
1217
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
1218
|
+
severity: "block",
|
|
1219
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"]
|
|
1220
|
+
},
|
|
1221
|
+
// ── GitHub ────────────────────────────────────────────────────────────────
|
|
1222
|
+
{
|
|
1223
|
+
name: "GitHub Token",
|
|
1224
|
+
regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
|
|
1225
|
+
severity: "block",
|
|
1226
|
+
keywords: ["ghp_", "gho_", "ghu_", "ghs_"]
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
name: "GitHub Fine-Grained PAT",
|
|
1230
|
+
regex: /\bgithub_pat_\w{82}\b/,
|
|
1231
|
+
severity: "block",
|
|
1232
|
+
keywords: ["github_pat_"]
|
|
1233
|
+
},
|
|
1234
|
+
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
1235
|
+
{
|
|
1236
|
+
name: "Slack Bot Token",
|
|
1237
|
+
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
1238
|
+
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
1239
|
+
severity: "block",
|
|
1240
|
+
keywords: ["xoxb-"]
|
|
1241
|
+
},
|
|
1242
|
+
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
1243
|
+
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
1244
|
+
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
1245
|
+
{
|
|
1246
|
+
name: "Anthropic API Key",
|
|
1247
|
+
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1248
|
+
severity: "block",
|
|
1249
|
+
keywords: ["sk-ant-api03"]
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "Anthropic Admin Key",
|
|
1253
|
+
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1254
|
+
severity: "block",
|
|
1255
|
+
keywords: ["sk-ant-admin01"]
|
|
1256
|
+
},
|
|
1257
|
+
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
1258
|
+
{
|
|
1259
|
+
name: "OpenAI API Key",
|
|
1260
|
+
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
1261
|
+
severity: "block",
|
|
1262
|
+
keywords: ["sk-"]
|
|
1263
|
+
},
|
|
1264
|
+
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
1265
|
+
{
|
|
1266
|
+
name: "Stripe Secret Key",
|
|
1267
|
+
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
1268
|
+
severity: "block",
|
|
1269
|
+
keywords: ["sk_live_", "sk_test_"]
|
|
1270
|
+
},
|
|
1271
|
+
// ── GCP ───────────────────────────────────────────────────────────────────
|
|
1272
|
+
{
|
|
1273
|
+
name: "GCP API Key",
|
|
1274
|
+
regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
1275
|
+
severity: "block",
|
|
1276
|
+
keywords: ["aiza"]
|
|
1187
1277
|
},
|
|
1188
|
-
// GCP service account JSON (detects the type field that uniquely identifies it)
|
|
1189
1278
|
{
|
|
1190
1279
|
name: "GCP Service Account",
|
|
1191
1280
|
regex: /"type"\s*:\s*"service_account"/,
|
|
1192
|
-
severity: "block"
|
|
1281
|
+
severity: "block",
|
|
1282
|
+
keywords: ["service_account"]
|
|
1283
|
+
},
|
|
1284
|
+
// ── Azure ─────────────────────────────────────────────────────────────────
|
|
1285
|
+
// Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
|
|
1286
|
+
{
|
|
1287
|
+
name: "Azure AD Client Secret",
|
|
1288
|
+
regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
|
|
1289
|
+
severity: "block",
|
|
1290
|
+
keywords: ["q~"]
|
|
1291
|
+
},
|
|
1292
|
+
// ── Databricks ────────────────────────────────────────────────────────────
|
|
1293
|
+
{
|
|
1294
|
+
name: "Databricks API Token",
|
|
1295
|
+
regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
|
|
1296
|
+
severity: "block",
|
|
1297
|
+
keywords: ["dapi"]
|
|
1193
1298
|
},
|
|
1194
|
-
//
|
|
1299
|
+
// ── DigitalOcean ──────────────────────────────────────────────────────────
|
|
1300
|
+
{
|
|
1301
|
+
name: "DigitalOcean PAT",
|
|
1302
|
+
regex: /\bdop_v1_[a-f0-9]{64}\b/,
|
|
1303
|
+
severity: "block",
|
|
1304
|
+
keywords: ["dop_v1_"]
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
name: "DigitalOcean Access Token",
|
|
1308
|
+
regex: /\bdoo_v1_[a-f0-9]{64}\b/,
|
|
1309
|
+
severity: "block",
|
|
1310
|
+
keywords: ["doo_v1_"]
|
|
1311
|
+
},
|
|
1312
|
+
// ── Doppler ───────────────────────────────────────────────────────────────
|
|
1313
|
+
{
|
|
1314
|
+
name: "Doppler Token",
|
|
1315
|
+
regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
|
|
1316
|
+
severity: "block",
|
|
1317
|
+
keywords: ["dp.pt."]
|
|
1318
|
+
},
|
|
1319
|
+
// ── HashiCorp Vault ───────────────────────────────────────────────────────
|
|
1320
|
+
{
|
|
1321
|
+
name: "HashiCorp Vault Service Token",
|
|
1322
|
+
regex: /\bhvs\.[\w-]{90,120}\b/,
|
|
1323
|
+
severity: "block",
|
|
1324
|
+
keywords: ["hvs."]
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
name: "HashiCorp Vault Batch Token",
|
|
1328
|
+
regex: /\bhvb\.[\w-]{138,300}\b/,
|
|
1329
|
+
severity: "block",
|
|
1330
|
+
keywords: ["hvb."]
|
|
1331
|
+
},
|
|
1332
|
+
// ── Hugging Face ──────────────────────────────────────────────────────────
|
|
1333
|
+
{ name: "HuggingFace Token", regex: /\bhf_[A-Za-z]{34}\b/, severity: "block", keywords: ["hf_"] },
|
|
1334
|
+
// ── Postman ───────────────────────────────────────────────────────────────
|
|
1335
|
+
{
|
|
1336
|
+
name: "Postman API Token",
|
|
1337
|
+
regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
|
|
1338
|
+
severity: "block",
|
|
1339
|
+
keywords: ["pmak-"]
|
|
1340
|
+
},
|
|
1341
|
+
// ── Pulumi ────────────────────────────────────────────────────────────────
|
|
1342
|
+
{
|
|
1343
|
+
name: "Pulumi Access Token",
|
|
1344
|
+
regex: /\bpul-[a-f0-9]{40}\b/,
|
|
1345
|
+
severity: "block",
|
|
1346
|
+
keywords: ["pul-"]
|
|
1347
|
+
},
|
|
1348
|
+
// ── SendGrid ──────────────────────────────────────────────────────────────
|
|
1349
|
+
{
|
|
1350
|
+
name: "SendGrid API Key",
|
|
1351
|
+
regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
|
|
1352
|
+
severity: "block",
|
|
1353
|
+
keywords: ["sg."]
|
|
1354
|
+
},
|
|
1355
|
+
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
1356
|
+
{
|
|
1357
|
+
name: "Private Key (PEM)",
|
|
1358
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
1359
|
+
severity: "block",
|
|
1360
|
+
keywords: ["-----begin"]
|
|
1361
|
+
},
|
|
1362
|
+
// ── NPM ───────────────────────────────────────────────────────────────────
|
|
1195
1363
|
{
|
|
1196
1364
|
name: "NPM Auth Token",
|
|
1197
|
-
regex: /_authToken\s*=\s*[A-Za-z0-9_
|
|
1198
|
-
severity: "block"
|
|
1365
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
|
|
1366
|
+
severity: "block",
|
|
1367
|
+
keywords: ["_authtoken"]
|
|
1368
|
+
},
|
|
1369
|
+
// ── JWT ───────────────────────────────────────────────────────────────────
|
|
1370
|
+
// review (not block): JWTs appear legitimately in API calls; flag for human approval
|
|
1371
|
+
{
|
|
1372
|
+
name: "JWT",
|
|
1373
|
+
regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
|
|
1374
|
+
severity: "review",
|
|
1375
|
+
keywords: ["eyj"]
|
|
1376
|
+
},
|
|
1377
|
+
// ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
|
|
1378
|
+
{
|
|
1379
|
+
name: "Stripe Restricted Key",
|
|
1380
|
+
regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
|
|
1381
|
+
severity: "block",
|
|
1382
|
+
keywords: ["rk_live_", "rk_test_", "rk_prod_"]
|
|
1383
|
+
},
|
|
1384
|
+
// ── Slack (app token) ─────────────────────────────────────────────────────
|
|
1385
|
+
{
|
|
1386
|
+
name: "Slack App Token",
|
|
1387
|
+
regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
|
|
1388
|
+
severity: "block",
|
|
1389
|
+
keywords: ["xapp-"]
|
|
1390
|
+
},
|
|
1391
|
+
// ── GitLab ────────────────────────────────────────────────────────────────
|
|
1392
|
+
{ name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
|
|
1393
|
+
{
|
|
1394
|
+
name: "GitLab Deploy Token",
|
|
1395
|
+
regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
|
|
1396
|
+
severity: "block",
|
|
1397
|
+
keywords: ["gldt-"]
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
name: "GitLab CI Job Token",
|
|
1401
|
+
regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
|
|
1402
|
+
severity: "block",
|
|
1403
|
+
keywords: ["glcbt-"]
|
|
1404
|
+
},
|
|
1405
|
+
// ── npm (publish token) ───────────────────────────────────────────────────
|
|
1406
|
+
{
|
|
1407
|
+
name: "npm Access Token",
|
|
1408
|
+
regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
|
|
1409
|
+
severity: "block",
|
|
1410
|
+
keywords: ["npm_"]
|
|
1411
|
+
},
|
|
1412
|
+
// ── Shopify ───────────────────────────────────────────────────────────────
|
|
1413
|
+
{
|
|
1414
|
+
name: "Shopify Access Token",
|
|
1415
|
+
regex: /\bshpat_[a-fA-F0-9]{32}\b/,
|
|
1416
|
+
severity: "block",
|
|
1417
|
+
keywords: ["shpat_"]
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
name: "Shopify Custom Access Token",
|
|
1421
|
+
regex: /\bshpca_[a-fA-F0-9]{32}\b/,
|
|
1422
|
+
severity: "block",
|
|
1423
|
+
keywords: ["shpca_"]
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
name: "Shopify Private App Token",
|
|
1427
|
+
regex: /\bshppa_[a-fA-F0-9]{32}\b/,
|
|
1428
|
+
severity: "block",
|
|
1429
|
+
keywords: ["shppa_"]
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
name: "Shopify Shared Secret",
|
|
1433
|
+
regex: /\bshpss_[a-fA-F0-9]{32}\b/,
|
|
1434
|
+
severity: "block",
|
|
1435
|
+
keywords: ["shpss_"]
|
|
1436
|
+
},
|
|
1437
|
+
// ── Linear ────────────────────────────────────────────────────────────────
|
|
1438
|
+
{
|
|
1439
|
+
name: "Linear API Key",
|
|
1440
|
+
regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
|
|
1441
|
+
severity: "block",
|
|
1442
|
+
keywords: ["lin_api_"]
|
|
1443
|
+
},
|
|
1444
|
+
// ── PlanetScale ───────────────────────────────────────────────────────────
|
|
1445
|
+
{
|
|
1446
|
+
name: "PlanetScale API Token",
|
|
1447
|
+
regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
|
|
1448
|
+
severity: "block",
|
|
1449
|
+
keywords: ["pscale_tkn_"]
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: "PlanetScale Password",
|
|
1453
|
+
regex: /\bpscale_pw_[\w.-]{32,64}\b/,
|
|
1454
|
+
severity: "block",
|
|
1455
|
+
keywords: ["pscale_pw_"]
|
|
1456
|
+
},
|
|
1457
|
+
// ── Sentry ────────────────────────────────────────────────────────────────
|
|
1458
|
+
{
|
|
1459
|
+
name: "Sentry User Token",
|
|
1460
|
+
regex: /\bsntryu_[a-f0-9]{64}\b/,
|
|
1461
|
+
severity: "block",
|
|
1462
|
+
keywords: ["sntryu_"]
|
|
1463
|
+
},
|
|
1464
|
+
// ── Grafana ───────────────────────────────────────────────────────────────
|
|
1465
|
+
{
|
|
1466
|
+
name: "Grafana Service Account Token",
|
|
1467
|
+
regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
|
|
1468
|
+
severity: "block",
|
|
1469
|
+
keywords: ["glsa_"]
|
|
1470
|
+
},
|
|
1471
|
+
// ── Heroku ────────────────────────────────────────────────────────────────
|
|
1472
|
+
{
|
|
1473
|
+
name: "Heroku API Key",
|
|
1474
|
+
regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
|
|
1475
|
+
severity: "block",
|
|
1476
|
+
keywords: ["hrku-aa"]
|
|
1477
|
+
},
|
|
1478
|
+
// ── PyPI ──────────────────────────────────────────────────────────────────
|
|
1479
|
+
{
|
|
1480
|
+
name: "PyPI Upload Token",
|
|
1481
|
+
regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
|
|
1482
|
+
severity: "block",
|
|
1483
|
+
keywords: ["pypi-"]
|
|
1199
1484
|
},
|
|
1200
|
-
|
|
1485
|
+
// ── Bearer Token ─────────────────────────────────────────────────────────
|
|
1486
|
+
{
|
|
1487
|
+
name: "Bearer Token",
|
|
1488
|
+
regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
|
|
1489
|
+
severity: "review",
|
|
1490
|
+
keywords: ["bearer"]
|
|
1491
|
+
}
|
|
1201
1492
|
];
|
|
1202
1493
|
SENSITIVE_PATH_PATTERNS = [
|
|
1203
1494
|
/[/\\]\.ssh[/\\]/i,
|
|
@@ -1751,7 +2042,7 @@ import fs7 from "fs";
|
|
|
1751
2042
|
import path8 from "path";
|
|
1752
2043
|
import os6 from "os";
|
|
1753
2044
|
import pm from "picomatch";
|
|
1754
|
-
import
|
|
2045
|
+
import mvdanSh from "mvdan-sh";
|
|
1755
2046
|
function tokenize2(toolName) {
|
|
1756
2047
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1757
2048
|
}
|
|
@@ -1769,17 +2060,97 @@ function getNestedValue(obj, path43) {
|
|
|
1769
2060
|
if (!obj || typeof obj !== "object") return null;
|
|
1770
2061
|
return path43.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1771
2062
|
}
|
|
1772
|
-
function
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2063
|
+
function normalizeCommandForPolicy(command) {
|
|
2064
|
+
try {
|
|
2065
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2066
|
+
const strips = [];
|
|
2067
|
+
syntax.Walk(f, (node) => {
|
|
2068
|
+
if (!node) return false;
|
|
2069
|
+
const n = node;
|
|
2070
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2071
|
+
const args = n.Args || [];
|
|
2072
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
2073
|
+
const argParts = args[i].Parts || [];
|
|
2074
|
+
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
2075
|
+
const flagVal = argParts[0].Value || "";
|
|
2076
|
+
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
2077
|
+
const next = args[i + 1];
|
|
2078
|
+
const nextParts = next.Parts || [];
|
|
2079
|
+
if (nextParts.length !== 1) continue;
|
|
2080
|
+
const quotedNode = nextParts[0];
|
|
2081
|
+
const nt = syntax.NodeType(quotedNode);
|
|
2082
|
+
if (nt === "SglQuoted") {
|
|
2083
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2084
|
+
} else if (nt === "DblQuoted") {
|
|
2085
|
+
const innerParts = quotedNode.Parts || [];
|
|
2086
|
+
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
2087
|
+
if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return true;
|
|
2091
|
+
});
|
|
2092
|
+
if (strips.length === 0) return command;
|
|
2093
|
+
strips.sort((a, b) => b[0] - a[0]);
|
|
2094
|
+
let result = command;
|
|
2095
|
+
for (const [start, end] of strips) {
|
|
2096
|
+
result = result.slice(0, start) + '""' + result.slice(end);
|
|
2097
|
+
}
|
|
2098
|
+
return result;
|
|
2099
|
+
} catch {
|
|
2100
|
+
return command;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
function scanArgsForDynamicExec(args, startIdx) {
|
|
2104
|
+
let hasCmdSubst = false;
|
|
2105
|
+
let hasParamExp = false;
|
|
2106
|
+
let hasCurl = false;
|
|
2107
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
2108
|
+
syntax.Walk(args[i], (inner) => {
|
|
2109
|
+
if (!inner) return false;
|
|
2110
|
+
const inn = inner;
|
|
2111
|
+
const it = syntax.NodeType(inn);
|
|
2112
|
+
if (it === "CmdSubst") hasCmdSubst = true;
|
|
2113
|
+
if (it === "ParamExp") hasParamExp = true;
|
|
2114
|
+
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
2115
|
+
return true;
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
if (hasCmdSubst && hasCurl) return "block";
|
|
2119
|
+
if (hasCmdSubst || hasParamExp) return "review";
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
function detectDangerousShellExec(command) {
|
|
2123
|
+
try {
|
|
2124
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2125
|
+
let result = null;
|
|
2126
|
+
syntax.Walk(f, (node) => {
|
|
2127
|
+
if (!node || result === "block") return false;
|
|
2128
|
+
const n = node;
|
|
2129
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2130
|
+
const args = n.Args || [];
|
|
2131
|
+
if (args.length === 0) return true;
|
|
2132
|
+
const firstParts = args[0].Parts || [];
|
|
2133
|
+
if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
|
|
2134
|
+
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
2135
|
+
if (cmdName === "eval") {
|
|
2136
|
+
const v = scanArgsForDynamicExec(args, 1);
|
|
2137
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
2138
|
+
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
2139
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
2140
|
+
const flagParts = args[i].Parts || [];
|
|
2141
|
+
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
2142
|
+
continue;
|
|
2143
|
+
const v = scanArgsForDynamicExec(args, i + 1);
|
|
2144
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
2145
|
+
break;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return true;
|
|
2149
|
+
});
|
|
2150
|
+
return result;
|
|
2151
|
+
} catch {
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
1783
2154
|
}
|
|
1784
2155
|
function shouldSnapshot(toolName, args, config) {
|
|
1785
2156
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1799,7 +2170,7 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1799
2170
|
const results = rule.conditions.map((cond) => {
|
|
1800
2171
|
const rawVal = getNestedValue(args, cond.field);
|
|
1801
2172
|
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
1802
|
-
const val = cond.field === "command" && normalized !== null ?
|
|
2173
|
+
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1803
2174
|
switch (cond.op) {
|
|
1804
2175
|
case "exists":
|
|
1805
2176
|
return val !== null && val !== "";
|
|
@@ -1847,52 +2218,35 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
1847
2218
|
const fieldName = toolInspection[matchingPattern];
|
|
1848
2219
|
return fieldName === "sql" || fieldName === "query";
|
|
1849
2220
|
}
|
|
1850
|
-
|
|
2221
|
+
function analyzeShellCommand(command) {
|
|
1851
2222
|
const actions = [];
|
|
1852
2223
|
const paths = [];
|
|
1853
2224
|
const allTokens = [];
|
|
1854
2225
|
const addToken = (token) => {
|
|
1855
2226
|
const lower = token.toLowerCase();
|
|
1856
2227
|
allTokens.push(lower);
|
|
1857
|
-
if (lower.includes("/"))
|
|
1858
|
-
|
|
1859
|
-
allTokens.push(...segments);
|
|
1860
|
-
}
|
|
1861
|
-
if (lower.startsWith("-")) {
|
|
1862
|
-
allTokens.push(lower.replace(/^-+/, ""));
|
|
1863
|
-
}
|
|
2228
|
+
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
2229
|
+
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
1864
2230
|
};
|
|
1865
2231
|
try {
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
if (!node) return;
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
for (const key in node) {
|
|
1882
|
-
if (key === "Parent") continue;
|
|
1883
|
-
const val = node[key];
|
|
1884
|
-
if (Array.isArray(val)) {
|
|
1885
|
-
val.forEach((child) => {
|
|
1886
|
-
if (child && typeof child === "object" && "type" in child) {
|
|
1887
|
-
walk(child);
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
} else if (val && typeof val === "object" && "type" in val) {
|
|
1891
|
-
walk(val);
|
|
1892
|
-
}
|
|
2232
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2233
|
+
syntax.Walk(f, (node) => {
|
|
2234
|
+
if (!node) return false;
|
|
2235
|
+
const n = node;
|
|
2236
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2237
|
+
const wordValues = (n.Args || []).map((arg) => {
|
|
2238
|
+
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
2239
|
+
}).filter((s) => s.length > 0);
|
|
2240
|
+
if (wordValues.length > 0) {
|
|
2241
|
+
const cmd = wordValues[0].toLowerCase();
|
|
2242
|
+
if (!actions.includes(cmd)) actions.push(cmd);
|
|
2243
|
+
wordValues.forEach((w) => addToken(w));
|
|
2244
|
+
wordValues.slice(1).forEach((w) => {
|
|
2245
|
+
if (!w.startsWith("-")) paths.push(w);
|
|
2246
|
+
});
|
|
1893
2247
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
2248
|
+
return true;
|
|
2249
|
+
});
|
|
1896
2250
|
} catch {
|
|
1897
2251
|
}
|
|
1898
2252
|
if (allTokens.length === 0) {
|
|
@@ -1917,7 +2271,18 @@ async function analyzeShellCommand(command) {
|
|
|
1917
2271
|
}
|
|
1918
2272
|
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1919
2273
|
const config = getConfig();
|
|
1920
|
-
|
|
2274
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
2275
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
2276
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2277
|
+
if (dlpMatch) {
|
|
2278
|
+
return {
|
|
2279
|
+
decision: dlpMatch.severity,
|
|
2280
|
+
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2281
|
+
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (wouldBeIgnored) return { decision: "allow" };
|
|
1921
2286
|
if (config.policy.smartRules.length > 0) {
|
|
1922
2287
|
const matchedRule = config.policy.smartRules.find(
|
|
1923
2288
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
@@ -1947,13 +2312,30 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1947
2312
|
let pathTokens = [];
|
|
1948
2313
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1949
2314
|
if (shellCommand) {
|
|
1950
|
-
const analyzed =
|
|
2315
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
1951
2316
|
allTokens = analyzed.allTokens;
|
|
1952
2317
|
pathTokens = analyzed.paths;
|
|
1953
2318
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
1954
2319
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1955
2320
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1956
2321
|
}
|
|
2322
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2323
|
+
if (evalVerdict === "block") {
|
|
2324
|
+
return {
|
|
2325
|
+
decision: "block",
|
|
2326
|
+
blockedByLabel: "Node9: Eval Remote Execution",
|
|
2327
|
+
reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
|
|
2328
|
+
tier: 3
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
if (evalVerdict === "review") {
|
|
2332
|
+
return {
|
|
2333
|
+
decision: "review",
|
|
2334
|
+
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2335
|
+
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2336
|
+
tier: 3
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
1957
2339
|
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1958
2340
|
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1959
2341
|
const sinks = pipeAnalysis.sinkTargets;
|
|
@@ -2207,7 +2589,7 @@ async function explainPolicy(toolName, args) {
|
|
|
2207
2589
|
let pathTokens = [];
|
|
2208
2590
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
2209
2591
|
if (shellCommand) {
|
|
2210
|
-
const analyzed =
|
|
2592
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
2211
2593
|
allTokens = analyzed.allTokens;
|
|
2212
2594
|
pathTokens = analyzed.paths;
|
|
2213
2595
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
@@ -2240,6 +2622,25 @@ async function explainPolicy(toolName, args) {
|
|
|
2240
2622
|
outcome: "checked",
|
|
2241
2623
|
detail: "No inline execution pattern detected"
|
|
2242
2624
|
});
|
|
2625
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2626
|
+
if (evalVerdict) {
|
|
2627
|
+
const label = evalVerdict === "block" ? "Node9: Eval Remote Execution" : "Node9: Eval Dynamic Content";
|
|
2628
|
+
const detail = evalVerdict === "block" ? "eval of remote download (curl/wget) \u2014 near-certain supply-chain attack" : "eval of dynamic content (variable or subshell expansion) \u2014 requires approval";
|
|
2629
|
+
steps.push({ name: "AST eval detection", outcome: evalVerdict, detail, isFinal: true });
|
|
2630
|
+
return {
|
|
2631
|
+
tool: toolName,
|
|
2632
|
+
args,
|
|
2633
|
+
waterfall,
|
|
2634
|
+
steps,
|
|
2635
|
+
decision: evalVerdict,
|
|
2636
|
+
blockedByLabel: label
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
steps.push({
|
|
2640
|
+
name: "AST eval detection",
|
|
2641
|
+
outcome: "checked",
|
|
2642
|
+
detail: "No dangerous eval detected"
|
|
2643
|
+
});
|
|
2243
2644
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
2244
2645
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
2245
2646
|
steps.push({
|
|
@@ -2354,7 +2755,7 @@ function isIgnoredTool(toolName) {
|
|
|
2354
2755
|
const config = getConfig();
|
|
2355
2756
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
2356
2757
|
}
|
|
2357
|
-
var SQL_DML_KEYWORDS;
|
|
2758
|
+
var syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, SQL_DML_KEYWORDS;
|
|
2358
2759
|
var init_policy = __esm({
|
|
2359
2760
|
"src/policy/index.ts"() {
|
|
2360
2761
|
"use strict";
|
|
@@ -2365,6 +2766,20 @@ var init_policy = __esm({
|
|
|
2365
2766
|
init_pipe_chain();
|
|
2366
2767
|
init_ssh_parser();
|
|
2367
2768
|
init_trusted_hosts();
|
|
2769
|
+
({ syntax } = mvdanSh);
|
|
2770
|
+
sharedParser = syntax.NewParser();
|
|
2771
|
+
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
2772
|
+
"-m",
|
|
2773
|
+
"--message",
|
|
2774
|
+
"--body",
|
|
2775
|
+
"--title",
|
|
2776
|
+
"--description",
|
|
2777
|
+
"--comment",
|
|
2778
|
+
"--subject",
|
|
2779
|
+
"--summary"
|
|
2780
|
+
]);
|
|
2781
|
+
SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
2782
|
+
DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
2368
2783
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2369
2784
|
}
|
|
2370
2785
|
});
|
|
@@ -8627,10 +9042,10 @@ function bold(s) {
|
|
|
8627
9042
|
function color(c, s) {
|
|
8628
9043
|
return `${c}${s}${RESET3}`;
|
|
8629
9044
|
}
|
|
8630
|
-
function progressBar(
|
|
8631
|
-
const filled = Math.round(Math.min(
|
|
9045
|
+
function progressBar(pct, warnAt = 70, critAt = 85) {
|
|
9046
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
|
|
8632
9047
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
8633
|
-
const c =
|
|
9048
|
+
const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
|
|
8634
9049
|
return `${c}${bar}${RESET3}`;
|
|
8635
9050
|
}
|
|
8636
9051
|
function formatTimeLeft(resetsAt) {
|
|
@@ -8846,15 +9261,15 @@ function renderContextLine(stdin) {
|
|
|
8846
9261
|
}
|
|
8847
9262
|
const rl = stdin.rate_limits;
|
|
8848
9263
|
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
8849
|
-
const
|
|
8850
|
-
const bar = progressBar(
|
|
9264
|
+
const pct = Math.round(rl.five_hour.used_percentage);
|
|
9265
|
+
const bar = progressBar(pct, 60, 80);
|
|
8851
9266
|
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
8852
|
-
parts.push(`${dim("\u2502")} 5h ${bar} ${
|
|
9267
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
|
|
8853
9268
|
}
|
|
8854
9269
|
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
8855
|
-
const
|
|
8856
|
-
const bar = progressBar(
|
|
8857
|
-
parts.push(`${dim("\u2502")} 7d ${bar} ${
|
|
9270
|
+
const pct = Math.round(rl.seven_day.used_percentage);
|
|
9271
|
+
const bar = progressBar(pct, 60, 80);
|
|
9272
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
|
|
8858
9273
|
}
|
|
8859
9274
|
if (parts.length === 0) return null;
|
|
8860
9275
|
return parts.join(" ");
|
|
@@ -9311,6 +9726,25 @@ async function setupGemini() {
|
|
|
9311
9726
|
printDaemonTip();
|
|
9312
9727
|
}
|
|
9313
9728
|
}
|
|
9729
|
+
function claudeDesktopConfigPath(homeDir2 = os11.homedir()) {
|
|
9730
|
+
if (process.platform === "darwin") {
|
|
9731
|
+
return path15.join(
|
|
9732
|
+
homeDir2,
|
|
9733
|
+
"Library",
|
|
9734
|
+
"Application Support",
|
|
9735
|
+
"Claude",
|
|
9736
|
+
"claude_desktop_config.json"
|
|
9737
|
+
);
|
|
9738
|
+
}
|
|
9739
|
+
if (process.platform === "linux") {
|
|
9740
|
+
return path15.join(homeDir2, ".config", "Claude", "claude_desktop_config.json");
|
|
9741
|
+
}
|
|
9742
|
+
if (process.platform === "win32") {
|
|
9743
|
+
const appData = process.env.APPDATA ?? path15.join(homeDir2, "AppData", "Roaming");
|
|
9744
|
+
return path15.join(appData, "Claude", "claude_desktop_config.json");
|
|
9745
|
+
}
|
|
9746
|
+
return null;
|
|
9747
|
+
}
|
|
9314
9748
|
function detectAgents(homeDir2 = os11.homedir()) {
|
|
9315
9749
|
const exists = (p) => {
|
|
9316
9750
|
try {
|
|
@@ -9324,13 +9758,15 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
9324
9758
|
return false;
|
|
9325
9759
|
}
|
|
9326
9760
|
};
|
|
9761
|
+
const desktopPath = claudeDesktopConfigPath(homeDir2);
|
|
9327
9762
|
return {
|
|
9328
9763
|
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
9329
9764
|
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
9330
9765
|
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
9331
9766
|
codex: exists(path15.join(homeDir2, ".codex")),
|
|
9332
9767
|
windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
|
|
9333
|
-
vscode: exists(path15.join(homeDir2, ".vscode"))
|
|
9768
|
+
vscode: exists(path15.join(homeDir2, ".vscode")),
|
|
9769
|
+
claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath))
|
|
9334
9770
|
};
|
|
9335
9771
|
}
|
|
9336
9772
|
async function setupCursor() {
|
|
@@ -9756,6 +10192,105 @@ function teardownVSCode() {
|
|
|
9756
10192
|
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
|
|
9757
10193
|
}
|
|
9758
10194
|
}
|
|
10195
|
+
async function setupClaudeDesktop() {
|
|
10196
|
+
const configPath = claudeDesktopConfigPath();
|
|
10197
|
+
if (!configPath) {
|
|
10198
|
+
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
10199
|
+
return;
|
|
10200
|
+
}
|
|
10201
|
+
const config = readJson(configPath) ?? {};
|
|
10202
|
+
const servers = config.mcpServers ?? {};
|
|
10203
|
+
let anythingChanged = false;
|
|
10204
|
+
if (!hasNode9McpServer(servers)) {
|
|
10205
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
10206
|
+
config.mcpServers = servers;
|
|
10207
|
+
writeJson(configPath, config);
|
|
10208
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
10209
|
+
anythingChanged = true;
|
|
10210
|
+
}
|
|
10211
|
+
const serversToWrap = [];
|
|
10212
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
10213
|
+
if (!server.command || server.command === "node9") continue;
|
|
10214
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
10215
|
+
}
|
|
10216
|
+
if (serversToWrap.length > 0) {
|
|
10217
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
10218
|
+
console.log(chalk.white(` ${configPath}`));
|
|
10219
|
+
for (const { name, upstream } of serversToWrap) {
|
|
10220
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
10221
|
+
}
|
|
10222
|
+
console.log("");
|
|
10223
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
10224
|
+
if (proceed) {
|
|
10225
|
+
for (const { name, upstream } of serversToWrap) {
|
|
10226
|
+
servers[name] = {
|
|
10227
|
+
...servers[name],
|
|
10228
|
+
command: "node9",
|
|
10229
|
+
args: ["mcp", "--upstream", upstream]
|
|
10230
|
+
};
|
|
10231
|
+
}
|
|
10232
|
+
config.mcpServers = servers;
|
|
10233
|
+
writeJson(configPath, config);
|
|
10234
|
+
console.log(chalk.green(`
|
|
10235
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
10236
|
+
anythingChanged = true;
|
|
10237
|
+
} else {
|
|
10238
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
10239
|
+
}
|
|
10240
|
+
console.log("");
|
|
10241
|
+
}
|
|
10242
|
+
console.log(
|
|
10243
|
+
chalk.yellow(
|
|
10244
|
+
" \u26A0\uFE0F Note: Claude Desktop does not support pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode."
|
|
10245
|
+
)
|
|
10246
|
+
);
|
|
10247
|
+
console.log("");
|
|
10248
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
10249
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Claude Desktop."));
|
|
10250
|
+
printDaemonTip();
|
|
10251
|
+
return;
|
|
10252
|
+
}
|
|
10253
|
+
if (anythingChanged) {
|
|
10254
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Desktop via MCP proxy!"));
|
|
10255
|
+
console.log(chalk.gray(" Restart Claude Desktop for changes to take effect."));
|
|
10256
|
+
printDaemonTip();
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
function teardownClaudeDesktop() {
|
|
10260
|
+
const configPath = claudeDesktopConfigPath();
|
|
10261
|
+
if (!configPath) {
|
|
10262
|
+
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
10263
|
+
return;
|
|
10264
|
+
}
|
|
10265
|
+
const config = readJson(configPath);
|
|
10266
|
+
if (!config?.mcpServers) {
|
|
10267
|
+
console.log(chalk.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
|
|
10268
|
+
return;
|
|
10269
|
+
}
|
|
10270
|
+
let changed = false;
|
|
10271
|
+
if (removeNode9McpServer(config.mcpServers)) {
|
|
10272
|
+
changed = true;
|
|
10273
|
+
console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${configPath}`));
|
|
10274
|
+
}
|
|
10275
|
+
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
10276
|
+
const args = server.args;
|
|
10277
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
10278
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
10279
|
+
config.mcpServers[name] = {
|
|
10280
|
+
...server,
|
|
10281
|
+
command: originalCmd,
|
|
10282
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
10283
|
+
};
|
|
10284
|
+
changed = true;
|
|
10285
|
+
}
|
|
10286
|
+
}
|
|
10287
|
+
if (changed) {
|
|
10288
|
+
writeJson(configPath, config);
|
|
10289
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
|
|
10290
|
+
} else {
|
|
10291
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
|
|
10292
|
+
}
|
|
10293
|
+
}
|
|
9759
10294
|
function getAgentsStatus(homeDir2 = os11.homedir()) {
|
|
9760
10295
|
const detected = detectAgents(homeDir2);
|
|
9761
10296
|
const claudeWired = (() => {
|
|
@@ -9826,6 +10361,18 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
|
|
|
9826
10361
|
installed: detected.codex,
|
|
9827
10362
|
wired: codexWired,
|
|
9828
10363
|
mode: detected.codex ? "mcp" : null
|
|
10364
|
+
},
|
|
10365
|
+
{
|
|
10366
|
+
name: "claudeDesktop",
|
|
10367
|
+
label: "Claude Desktop",
|
|
10368
|
+
installed: detected.claudeDesktop,
|
|
10369
|
+
wired: (() => {
|
|
10370
|
+
const cfgPath = claudeDesktopConfigPath(homeDir2);
|
|
10371
|
+
if (!cfgPath) return false;
|
|
10372
|
+
const cfg = readJson(cfgPath);
|
|
10373
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
10374
|
+
})(),
|
|
10375
|
+
mode: detected.claudeDesktop ? "mcp" : null
|
|
9829
10376
|
}
|
|
9830
10377
|
];
|
|
9831
10378
|
}
|
|
@@ -10939,7 +11486,6 @@ RAW: ${raw}
|
|
|
10939
11486
|
// src/cli/commands/log.ts
|
|
10940
11487
|
init_audit();
|
|
10941
11488
|
init_config();
|
|
10942
|
-
init_policy();
|
|
10943
11489
|
import fs25 from "fs";
|
|
10944
11490
|
import path27 from "path";
|
|
10945
11491
|
import os21 from "os";
|
|
@@ -11053,8 +11599,20 @@ function registerLogCommand(program2) {
|
|
|
11053
11599
|
}
|
|
11054
11600
|
const safeCwd = typeof payload.cwd === "string" && path27.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
11055
11601
|
const config = getConfig(safeCwd);
|
|
11056
|
-
if (
|
|
11057
|
-
|
|
11602
|
+
if ((tool === "Bash" || tool === "bash") && config.settings.enableUndo !== false) {
|
|
11603
|
+
const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
11604
|
+
if (bashCommand) {
|
|
11605
|
+
const effectiveCwd = safeCwd ?? process.cwd();
|
|
11606
|
+
const history = getSnapshotHistory();
|
|
11607
|
+
const hasPrior = history.some((e) => e.cwd === effectiveCwd);
|
|
11608
|
+
if (hasPrior) {
|
|
11609
|
+
await createShadowSnapshot(
|
|
11610
|
+
"Bash",
|
|
11611
|
+
{ command: bashCommand },
|
|
11612
|
+
config.policy.snapshot.ignorePaths
|
|
11613
|
+
);
|
|
11614
|
+
}
|
|
11615
|
+
}
|
|
11058
11616
|
}
|
|
11059
11617
|
} catch (err2) {
|
|
11060
11618
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
@@ -11723,8 +12281,8 @@ function buildTestTimestamps(allEntries) {
|
|
|
11723
12281
|
return testTs;
|
|
11724
12282
|
}
|
|
11725
12283
|
function isTestEntry(entry, testTs) {
|
|
11726
|
-
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
11727
12284
|
if (entry.testRun === true) return true;
|
|
12285
|
+
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
11728
12286
|
const cmd = entry.args?.command;
|
|
11729
12287
|
if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
|
|
11730
12288
|
const t = new Date(entry.ts).getTime();
|
|
@@ -11772,6 +12330,18 @@ function isAllow(decision) {
|
|
|
11772
12330
|
function isDlp(checkedBy) {
|
|
11773
12331
|
return !!checkedBy?.includes("dlp");
|
|
11774
12332
|
}
|
|
12333
|
+
var BLOCK_REASON_LABELS = {
|
|
12334
|
+
timeout: "Popup timeout",
|
|
12335
|
+
"smart-rule-block": "Smart rule",
|
|
12336
|
+
"observe-mode-dlp-would-block": "DLP (observe)",
|
|
12337
|
+
"persistent-deny": "Persistent deny",
|
|
12338
|
+
"local-decision": "User denied",
|
|
12339
|
+
"dlp-block": "DLP block",
|
|
12340
|
+
"loop-detected": "Loop detected"
|
|
12341
|
+
};
|
|
12342
|
+
function humanBlockReason(reason) {
|
|
12343
|
+
return BLOCK_REASON_LABELS[reason] ?? reason;
|
|
12344
|
+
}
|
|
11775
12345
|
function barStr(value, max, width) {
|
|
11776
12346
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
11777
12347
|
const filled = Math.max(1, Math.round(value / max * width));
|
|
@@ -11782,10 +12352,6 @@ function colorBar(value, max, width) {
|
|
|
11782
12352
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
11783
12353
|
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
11784
12354
|
}
|
|
11785
|
-
function pct(num3, total) {
|
|
11786
|
-
if (total === 0) return "\u2013";
|
|
11787
|
-
return Math.round(num3 / total * 100) + "%";
|
|
11788
|
-
}
|
|
11789
12355
|
function fmtDate(d) {
|
|
11790
12356
|
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
11791
12357
|
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
@@ -11894,18 +12460,104 @@ function loadClaudeCost(start, end) {
|
|
|
11894
12460
|
}
|
|
11895
12461
|
return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
|
|
11896
12462
|
}
|
|
11897
|
-
function
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
12463
|
+
function loadCodexCost(start, end) {
|
|
12464
|
+
const sessionsBase = path30.join(os24.homedir(), ".codex", "sessions");
|
|
12465
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
12466
|
+
let total = 0;
|
|
12467
|
+
let toolCalls = 0;
|
|
12468
|
+
if (!fs28.existsSync(sessionsBase)) return { total, byDay, toolCalls };
|
|
12469
|
+
const jsonlFiles = [];
|
|
12470
|
+
try {
|
|
12471
|
+
for (const year of fs28.readdirSync(sessionsBase)) {
|
|
12472
|
+
const yearPath = path30.join(sessionsBase, year);
|
|
12473
|
+
try {
|
|
12474
|
+
if (!fs28.statSync(yearPath).isDirectory()) continue;
|
|
12475
|
+
} catch {
|
|
12476
|
+
continue;
|
|
12477
|
+
}
|
|
12478
|
+
for (const month of fs28.readdirSync(yearPath)) {
|
|
12479
|
+
const monthPath = path30.join(yearPath, month);
|
|
12480
|
+
try {
|
|
12481
|
+
if (!fs28.statSync(monthPath).isDirectory()) continue;
|
|
12482
|
+
} catch {
|
|
12483
|
+
continue;
|
|
12484
|
+
}
|
|
12485
|
+
for (const day of fs28.readdirSync(monthPath)) {
|
|
12486
|
+
const dayPath = path30.join(monthPath, day);
|
|
12487
|
+
try {
|
|
12488
|
+
if (!fs28.statSync(dayPath).isDirectory()) continue;
|
|
12489
|
+
} catch {
|
|
12490
|
+
continue;
|
|
12491
|
+
}
|
|
12492
|
+
for (const file of fs28.readdirSync(dayPath)) {
|
|
12493
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path30.join(dayPath, file));
|
|
12494
|
+
}
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
}
|
|
12498
|
+
} catch {
|
|
12499
|
+
return { total, byDay, toolCalls };
|
|
12500
|
+
}
|
|
12501
|
+
for (const filePath of jsonlFiles) {
|
|
12502
|
+
let lines;
|
|
12503
|
+
try {
|
|
12504
|
+
lines = fs28.readFileSync(filePath, "utf-8").split("\n");
|
|
12505
|
+
} catch {
|
|
12506
|
+
continue;
|
|
12507
|
+
}
|
|
12508
|
+
let sessionStart2 = "";
|
|
12509
|
+
let lastTotalInput = 0;
|
|
12510
|
+
let lastTotalCached = 0;
|
|
12511
|
+
let lastTotalOutput = 0;
|
|
12512
|
+
let sessionToolCalls = 0;
|
|
12513
|
+
for (const line of lines) {
|
|
12514
|
+
if (!line.trim()) continue;
|
|
12515
|
+
let entry;
|
|
12516
|
+
try {
|
|
12517
|
+
entry = JSON.parse(line);
|
|
12518
|
+
} catch {
|
|
12519
|
+
continue;
|
|
12520
|
+
}
|
|
12521
|
+
const p = entry.payload ?? {};
|
|
12522
|
+
if (entry.type === "session_meta") {
|
|
12523
|
+
sessionStart2 = String(p["timestamp"] ?? "");
|
|
12524
|
+
continue;
|
|
12525
|
+
}
|
|
12526
|
+
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
12527
|
+
const info = p["info"] ?? {};
|
|
12528
|
+
const usage = info["total_token_usage"] ?? {};
|
|
12529
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
12530
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
12531
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
12532
|
+
}
|
|
12533
|
+
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
12534
|
+
sessionToolCalls++;
|
|
12535
|
+
}
|
|
12536
|
+
}
|
|
12537
|
+
if (!sessionStart2) continue;
|
|
12538
|
+
const ts = new Date(sessionStart2);
|
|
12539
|
+
if (ts < start || ts > end) continue;
|
|
12540
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
12541
|
+
const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
12542
|
+
total += cost;
|
|
12543
|
+
toolCalls += sessionToolCalls;
|
|
12544
|
+
const dateKey = sessionStart2.slice(0, 10);
|
|
12545
|
+
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
12546
|
+
}
|
|
12547
|
+
return { total, byDay, toolCalls };
|
|
12548
|
+
}
|
|
12549
|
+
function registerReportCommand(program2) {
|
|
12550
|
+
program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").action((options) => {
|
|
12551
|
+
const period = ["today", "7d", "30d", "month"].includes(
|
|
12552
|
+
options.period
|
|
12553
|
+
) ? options.period : "7d";
|
|
12554
|
+
const logPath = path30.join(os24.homedir(), ".node9", "audit.log");
|
|
12555
|
+
const allEntries = parseAuditLog(logPath);
|
|
12556
|
+
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
12557
|
+
if (unackedDlp.length > 0) {
|
|
12558
|
+
console.log("");
|
|
12559
|
+
console.log(
|
|
12560
|
+
chalk9.bgRed.white.bold(
|
|
11909
12561
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
|
|
11910
12562
|
) + " " + chalk9.yellow("\u2192 run: node9 dlp")
|
|
11911
12563
|
);
|
|
@@ -11918,7 +12570,7 @@ function registerReportCommand(program2) {
|
|
|
11918
12570
|
}
|
|
11919
12571
|
const { start, end } = getDateRange(period);
|
|
11920
12572
|
const {
|
|
11921
|
-
total:
|
|
12573
|
+
total: claudeCostUSD,
|
|
11922
12574
|
byDay: costByDay,
|
|
11923
12575
|
byModel: costByModel,
|
|
11924
12576
|
inputTokens: costInputTokens,
|
|
@@ -11926,6 +12578,15 @@ function registerReportCommand(program2) {
|
|
|
11926
12578
|
cacheWriteTokens: costCacheWrite,
|
|
11927
12579
|
cacheReadTokens: costCacheRead
|
|
11928
12580
|
} = loadClaudeCost(start, end);
|
|
12581
|
+
const {
|
|
12582
|
+
total: codexCostUSD,
|
|
12583
|
+
byDay: codexCostByDay,
|
|
12584
|
+
toolCalls: codexToolCalls
|
|
12585
|
+
} = loadCodexCost(start, end);
|
|
12586
|
+
const costUSD = claudeCostUSD + codexCostUSD;
|
|
12587
|
+
for (const [day, c] of codexCostByDay) {
|
|
12588
|
+
costByDay.set(day, (costByDay.get(day) ?? 0) + c);
|
|
12589
|
+
}
|
|
11929
12590
|
const periodMs = end.getTime() - start.getTime();
|
|
11930
12591
|
const priorEnd = new Date(start.getTime() - 1);
|
|
11931
12592
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
@@ -11956,9 +12617,12 @@ function registerReportCommand(program2) {
|
|
|
11956
12617
|
`));
|
|
11957
12618
|
return;
|
|
11958
12619
|
}
|
|
11959
|
-
let
|
|
11960
|
-
let
|
|
11961
|
-
let
|
|
12620
|
+
let userApproved = 0;
|
|
12621
|
+
let userDenied = 0;
|
|
12622
|
+
let timedOut = 0;
|
|
12623
|
+
let hardBlocked = 0;
|
|
12624
|
+
let dlpBlocked = 0;
|
|
12625
|
+
let observeDlp = 0;
|
|
11962
12626
|
let loopHits = 0;
|
|
11963
12627
|
let testPasses = 0;
|
|
11964
12628
|
let testFails = 0;
|
|
@@ -11971,9 +12635,16 @@ function registerReportCommand(program2) {
|
|
|
11971
12635
|
for (const e of entries) {
|
|
11972
12636
|
const allow = isAllow(e.decision);
|
|
11973
12637
|
const dateKey = e.ts.slice(0, 10);
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
12638
|
+
const userInteracted = e.source === "daemon";
|
|
12639
|
+
if (userInteracted) {
|
|
12640
|
+
if (allow) userApproved++;
|
|
12641
|
+
else userDenied++;
|
|
12642
|
+
} else if (!allow) {
|
|
12643
|
+
if (e.checkedBy === "timeout") timedOut++;
|
|
12644
|
+
else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
12645
|
+
else if (isDlp(e.checkedBy)) dlpBlocked++;
|
|
12646
|
+
else if (e.checkedBy !== "loop-detected") hardBlocked++;
|
|
12647
|
+
}
|
|
11977
12648
|
if (e.checkedBy === "loop-detected") loopHits++;
|
|
11978
12649
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
11979
12650
|
t.calls++;
|
|
@@ -11998,6 +12669,7 @@ function registerReportCommand(program2) {
|
|
|
11998
12669
|
if (e.testResult === "pass") testPasses++;
|
|
11999
12670
|
else if (e.testResult === "fail") testFails++;
|
|
12000
12671
|
}
|
|
12672
|
+
if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
|
|
12001
12673
|
const total = entries.length;
|
|
12002
12674
|
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
12003
12675
|
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
@@ -12024,25 +12696,84 @@ function registerReportCommand(program2) {
|
|
|
12024
12696
|
" " + chalk9.bold.cyan("\u{1F6E1} node9 Report") + chalk9.dim(" \xB7 ") + chalk9.white(periodLabel[period]) + chalk9.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk9.dim(` ${num(total)} events`) + (excludeTests ? chalk9.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
|
|
12025
12697
|
);
|
|
12026
12698
|
console.log(" " + line);
|
|
12027
|
-
|
|
12028
|
-
const
|
|
12029
|
-
const dlpLabel = dlpHits > 0 ? chalk9.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : chalk9.dim("\u{1F6A8} 0 DLP hits");
|
|
12030
|
-
const loopLabel = loopHits > 0 ? chalk9.yellow(`\u{1F504} ${loopHits} loops`) : chalk9.dim("\u{1F504} 0 loops");
|
|
12031
|
-
const currentRate = total > 0 ? blocked / total : 0;
|
|
12699
|
+
const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
|
|
12700
|
+
const currentRate = total > 0 ? totalBlocked / total : 0;
|
|
12032
12701
|
const trendLabel = (() => {
|
|
12033
|
-
if (priorBlockRate === null) return
|
|
12702
|
+
if (priorBlockRate === null) return "";
|
|
12034
12703
|
const delta = Math.round((currentRate - priorBlockRate) * 100);
|
|
12035
|
-
|
|
12036
|
-
return chalk9.
|
|
12704
|
+
if (delta === 0) return "";
|
|
12705
|
+
return " " + (delta > 0 ? chalk9.red(`\u25B2${delta}%`) : chalk9.green(`\u25BC${Math.abs(delta)}%`)) + chalk9.dim(" vs prior");
|
|
12037
12706
|
})();
|
|
12038
12707
|
const reads = toolMap.get("Read")?.calls ?? 0;
|
|
12039
12708
|
const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
|
|
12040
12709
|
const ratioLabel = reads > 0 ? chalk9.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk9.dim("edit/read \u2013");
|
|
12041
12710
|
const testLabel = testPasses + testFails > 0 ? chalk9.dim("tests ") + chalk9.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk9.red(`${testFails}\u2717`) : "") : chalk9.dim("tests \u2013");
|
|
12711
|
+
console.log("");
|
|
12712
|
+
console.log(" " + chalk9.bold("Protection Summary"));
|
|
12713
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12042
12714
|
console.log(
|
|
12043
|
-
" " + chalk9.
|
|
12715
|
+
" " + chalk9.dim("Intercepted") + " " + chalk9.white(num(total)) + chalk9.dim(" tool calls")
|
|
12716
|
+
);
|
|
12717
|
+
console.log("");
|
|
12718
|
+
const COL1 = 18;
|
|
12719
|
+
const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
|
|
12720
|
+
const countStr = colorFn(num(count));
|
|
12721
|
+
const noteStr = note ? chalk9.dim(" " + note) : "";
|
|
12722
|
+
console.log(" " + icon + " " + chalk9.white(label.padEnd(COL1)) + countStr + noteStr);
|
|
12723
|
+
};
|
|
12724
|
+
summaryRow(
|
|
12725
|
+
userApproved > 0 ? chalk9.green("\u2705") : chalk9.dim("\u2705"),
|
|
12726
|
+
"User approved",
|
|
12727
|
+
userApproved,
|
|
12728
|
+
userApproved === 0 ? "no popups this period" : void 0,
|
|
12729
|
+
userApproved > 0 ? (s) => chalk9.green(s) : (s) => chalk9.dim(s)
|
|
12730
|
+
);
|
|
12731
|
+
summaryRow(
|
|
12732
|
+
userDenied > 0 ? chalk9.red("\u{1F6AB}") : chalk9.dim("\u{1F6AB}"),
|
|
12733
|
+
"User denied",
|
|
12734
|
+
userDenied,
|
|
12735
|
+
void 0,
|
|
12736
|
+
userDenied > 0 ? (s) => chalk9.red(s) : (s) => chalk9.dim(s)
|
|
12737
|
+
);
|
|
12738
|
+
summaryRow(
|
|
12739
|
+
timedOut > 0 ? chalk9.yellow("\u23F1") : chalk9.dim("\u23F1"),
|
|
12740
|
+
"Timed out",
|
|
12741
|
+
timedOut,
|
|
12742
|
+
timedOut > 0 ? "no approval response" : void 0,
|
|
12743
|
+
timedOut > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
|
|
12744
|
+
);
|
|
12745
|
+
summaryRow(
|
|
12746
|
+
hardBlocked > 0 ? chalk9.red("\u{1F6D1}") : chalk9.dim("\u{1F6D1}"),
|
|
12747
|
+
"Auto-blocked",
|
|
12748
|
+
hardBlocked,
|
|
12749
|
+
void 0,
|
|
12750
|
+
hardBlocked > 0 ? (s) => chalk9.red(s) : (s) => chalk9.dim(s)
|
|
12751
|
+
);
|
|
12752
|
+
summaryRow(
|
|
12753
|
+
dlpBlocked > 0 ? chalk9.yellow("\u{1F6A8}") : chalk9.dim("\u{1F6A8}"),
|
|
12754
|
+
"DLP blocked",
|
|
12755
|
+
dlpBlocked,
|
|
12756
|
+
void 0,
|
|
12757
|
+
dlpBlocked > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
|
|
12758
|
+
);
|
|
12759
|
+
summaryRow(
|
|
12760
|
+
observeDlp > 0 ? chalk9.blue("\u{1F441}") : chalk9.dim("\u{1F441}"),
|
|
12761
|
+
"DLP (observe)",
|
|
12762
|
+
observeDlp,
|
|
12763
|
+
observeDlp > 0 ? "would-block in strict mode" : void 0,
|
|
12764
|
+
observeDlp > 0 ? (s) => chalk9.blue(s) : (s) => chalk9.dim(s)
|
|
12765
|
+
);
|
|
12766
|
+
summaryRow(
|
|
12767
|
+
loopHits > 0 ? chalk9.yellow("\u{1F504}") : chalk9.dim("\u{1F504}"),
|
|
12768
|
+
"Loops detected",
|
|
12769
|
+
loopHits,
|
|
12770
|
+
void 0,
|
|
12771
|
+
loopHits > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
|
|
12044
12772
|
);
|
|
12045
|
-
|
|
12773
|
+
if (trendLabel || ratioLabel || testPasses + testFails > 0) {
|
|
12774
|
+
console.log("");
|
|
12775
|
+
console.log(" " + ratioLabel + " " + testLabel + trendLabel);
|
|
12776
|
+
}
|
|
12046
12777
|
console.log("");
|
|
12047
12778
|
const toolHeaderRaw = "Top Tools";
|
|
12048
12779
|
const blockHeaderRaw = "Top Blocks";
|
|
@@ -12065,7 +12796,8 @@ function registerReportCommand(program2) {
|
|
|
12065
12796
|
let rightStyled = "";
|
|
12066
12797
|
if (i < topBlocks.length) {
|
|
12067
12798
|
const [reason, count] = topBlocks[i];
|
|
12068
|
-
const
|
|
12799
|
+
const readable = humanBlockReason(reason);
|
|
12800
|
+
const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
12069
12801
|
const countStr = num(count).padStart(BLOCK_COUNT_W);
|
|
12070
12802
|
const b = colorBar(count, maxBlock, BAR);
|
|
12071
12803
|
rightStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.red(countStr);
|
|
@@ -12131,31 +12863,24 @@ function registerReportCommand(program2) {
|
|
|
12131
12863
|
console.log("");
|
|
12132
12864
|
console.log(" " + chalk9.bold("Tokens") + " " + chalk9.dim(`${num(totalTokens)} total`));
|
|
12133
12865
|
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12134
|
-
const
|
|
12866
|
+
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12867
|
+
const TOK_LABEL = 14;
|
|
12868
|
+
const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
|
|
12869
|
+
const nonCacheRows = [
|
|
12135
12870
|
["Input", costInputTokens, chalk9.cyan(num(costInputTokens))],
|
|
12136
12871
|
["Output", costOutputTokens, chalk9.white(num(costOutputTokens))],
|
|
12137
|
-
["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))]
|
|
12138
|
-
["Cache read", costCacheRead, chalk9.green(num(costCacheRead))]
|
|
12872
|
+
["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))]
|
|
12139
12873
|
];
|
|
12140
|
-
const
|
|
12141
|
-
costInputTokens,
|
|
12142
|
-
costOutputTokens,
|
|
12143
|
-
costCacheWrite,
|
|
12144
|
-
costCacheRead,
|
|
12145
|
-
1
|
|
12146
|
-
);
|
|
12147
|
-
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12148
|
-
const TOK_LABEL = 14;
|
|
12149
|
-
for (const [label, count, colored] of tokenRows) {
|
|
12874
|
+
for (const [label, count, colored] of nonCacheRows) {
|
|
12150
12875
|
if (count === 0) continue;
|
|
12151
|
-
const b = colorBar(count,
|
|
12876
|
+
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
12152
12877
|
console.log(" " + chalk9.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
12153
12878
|
}
|
|
12154
|
-
if (
|
|
12879
|
+
if (costCacheRead > 0) {
|
|
12880
|
+
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
12881
|
+
const pct = cacheHitPct > 0 ? chalk9.dim(` ${cacheHitPct}% hit rate`) : "";
|
|
12155
12882
|
console.log(
|
|
12156
|
-
" " + chalk9.
|
|
12157
|
-
`Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
|
|
12158
|
-
)
|
|
12883
|
+
" " + chalk9.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk9.green(num(costCacheRead)) + pct
|
|
12159
12884
|
);
|
|
12160
12885
|
}
|
|
12161
12886
|
}
|
|
@@ -12171,6 +12896,11 @@ function registerReportCommand(program2) {
|
|
|
12171
12896
|
console.log("");
|
|
12172
12897
|
console.log(" " + chalk9.bold("Cost") + " " + costHeaderRight);
|
|
12173
12898
|
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12899
|
+
if (codexCostUSD > 0)
|
|
12900
|
+
costByModel.set(
|
|
12901
|
+
"codex (openai)",
|
|
12902
|
+
(costByModel.get("codex (openai)") ?? 0) + codexCostUSD
|
|
12903
|
+
);
|
|
12174
12904
|
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
12175
12905
|
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
12176
12906
|
const MODEL_LABEL = 22;
|
|
@@ -12597,6 +13327,7 @@ function registerInitCommand(program2) {
|
|
|
12597
13327
|
else if (agent === "codex") await setupCodex();
|
|
12598
13328
|
else if (agent === "windsurf") await setupWindsurf();
|
|
12599
13329
|
else if (agent === "vscode") await setupVSCode();
|
|
13330
|
+
else if (agent === "claudeDesktop") await setupClaudeDesktop();
|
|
12600
13331
|
console.log("");
|
|
12601
13332
|
}
|
|
12602
13333
|
if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
|
|
@@ -13426,6 +14157,7 @@ import readline4 from "readline";
|
|
|
13426
14157
|
import fs32 from "fs";
|
|
13427
14158
|
import os28 from "os";
|
|
13428
14159
|
import path35 from "path";
|
|
14160
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
13429
14161
|
init_core();
|
|
13430
14162
|
init_daemon();
|
|
13431
14163
|
init_shields();
|
|
@@ -13505,8 +14237,31 @@ var TOOLS = [
|
|
|
13505
14237
|
},
|
|
13506
14238
|
{
|
|
13507
14239
|
name: "node9_undo_list",
|
|
13508
|
-
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
|
|
13509
|
-
inputSchema: {
|
|
14240
|
+
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert or node9_undo_detail.",
|
|
14241
|
+
inputSchema: {
|
|
14242
|
+
type: "object",
|
|
14243
|
+
properties: {
|
|
14244
|
+
cwd: {
|
|
14245
|
+
type: "string",
|
|
14246
|
+
description: "Filter to snapshots for a specific project directory. Omit to show all projects."
|
|
14247
|
+
}
|
|
14248
|
+
},
|
|
14249
|
+
required: []
|
|
14250
|
+
}
|
|
14251
|
+
},
|
|
14252
|
+
{
|
|
14253
|
+
name: "node9_undo_detail",
|
|
14254
|
+
description: "Show the full details of a specific node9 snapshot: unified diff, exact files changed, tool that triggered it, command summary, working directory, and timestamp. Use this to understand exactly what a snapshot contains before deciding to revert.",
|
|
14255
|
+
inputSchema: {
|
|
14256
|
+
type: "object",
|
|
14257
|
+
properties: {
|
|
14258
|
+
hash: {
|
|
14259
|
+
type: "string",
|
|
14260
|
+
description: "The git commit hash (full or 7-char prefix) from node9_undo_list."
|
|
14261
|
+
}
|
|
14262
|
+
},
|
|
14263
|
+
required: ["hash"]
|
|
14264
|
+
}
|
|
13510
14265
|
},
|
|
13511
14266
|
{
|
|
13512
14267
|
name: "node9_undo_revert",
|
|
@@ -13528,13 +14283,18 @@ var TOOLS = [
|
|
|
13528
14283
|
},
|
|
13529
14284
|
{
|
|
13530
14285
|
name: "node9_audit_get",
|
|
13531
|
-
description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), and agent. Use this to review what AI actions have been taken recently.",
|
|
14286
|
+
description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), command/args, and agent. Use this to review what AI actions have been taken recently, especially blocked or reviewed ops.",
|
|
13532
14287
|
inputSchema: {
|
|
13533
14288
|
type: "object",
|
|
13534
14289
|
properties: {
|
|
13535
14290
|
limit: {
|
|
13536
14291
|
type: "number",
|
|
13537
14292
|
description: "Number of recent entries to return (default: 20, max: 100)."
|
|
14293
|
+
},
|
|
14294
|
+
filter: {
|
|
14295
|
+
type: "string",
|
|
14296
|
+
enum: ["all", "block", "review"],
|
|
14297
|
+
description: 'Filter by decision. Omit or use "all" to show every entry.'
|
|
13538
14298
|
}
|
|
13539
14299
|
},
|
|
13540
14300
|
required: []
|
|
@@ -13545,6 +14305,53 @@ var TOOLS = [
|
|
|
13545
14305
|
description: "Show all active smart rules in detail \u2014 name, tool, verdict, conditions, and reason. Includes default rules, shield rules, and any custom project rules. Use this to understand exactly what is being blocked or reviewed.",
|
|
13546
14306
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
13547
14307
|
},
|
|
14308
|
+
{
|
|
14309
|
+
name: "node9_scan",
|
|
14310
|
+
description: "Scan all AI agent history (Claude + Gemini) and report what node9 would have blocked or flagged. Shows blocked operations, reviewed commands, credential leaks, and agent spend. Use this to audit past activity and find security gaps before they become incidents.",
|
|
14311
|
+
inputSchema: {
|
|
14312
|
+
type: "object",
|
|
14313
|
+
properties: {
|
|
14314
|
+
drill_down: {
|
|
14315
|
+
type: "boolean",
|
|
14316
|
+
description: "Show full commands and session IDs for every finding (default: false for a clean summary)."
|
|
14317
|
+
}
|
|
14318
|
+
},
|
|
14319
|
+
required: []
|
|
14320
|
+
}
|
|
14321
|
+
},
|
|
14322
|
+
{
|
|
14323
|
+
name: "node9_report",
|
|
14324
|
+
description: "Show an activity and security report: tool call counts, blocks, DLP findings, agent cost, and daily trends for a chosen period. Covers all AI agents (Claude, Gemini, etc.).",
|
|
14325
|
+
inputSchema: {
|
|
14326
|
+
type: "object",
|
|
14327
|
+
properties: {
|
|
14328
|
+
period: {
|
|
14329
|
+
type: "string",
|
|
14330
|
+
enum: ["today", "7d", "30d", "month"],
|
|
14331
|
+
description: "Time period for the report (default: 7d)."
|
|
14332
|
+
},
|
|
14333
|
+
no_tests: {
|
|
14334
|
+
type: "boolean",
|
|
14335
|
+
description: "Exclude test runner calls (npm test, vitest, pytest\u2026) from stats."
|
|
14336
|
+
}
|
|
14337
|
+
},
|
|
14338
|
+
required: []
|
|
14339
|
+
}
|
|
14340
|
+
},
|
|
14341
|
+
{
|
|
14342
|
+
name: "node9_session",
|
|
14343
|
+
description: "List recent AI agent sessions with per-session summaries: tool calls, cost, modified files, and any blocked operations. Pass a session_id to see the full tool trace for that session.",
|
|
14344
|
+
inputSchema: {
|
|
14345
|
+
type: "object",
|
|
14346
|
+
properties: {
|
|
14347
|
+
detail: {
|
|
14348
|
+
type: "string",
|
|
14349
|
+
description: "Session ID to show the full tool trace for. Omit to list all recent sessions."
|
|
14350
|
+
}
|
|
14351
|
+
},
|
|
14352
|
+
required: []
|
|
14353
|
+
}
|
|
14354
|
+
},
|
|
13548
14355
|
{
|
|
13549
14356
|
name: "node9_rule_add",
|
|
13550
14357
|
description: 'Add a new protective smart rule to the global node9 config (~/.node9/config.json). Rules can block or send dangerous commands for human review based on regex conditions. IMPORTANT: only "block" and "review" verdicts are permitted \u2014 "allow" rules are never accepted because they would weaken node9 security. Rules can only be added, never removed.',
|
|
@@ -13737,21 +14544,40 @@ function handleApproverSet(args) {
|
|
|
13737
14544
|
}
|
|
13738
14545
|
function handleAuditGet(args) {
|
|
13739
14546
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
14547
|
+
const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
|
|
13740
14548
|
const auditPath = path35.join(os28.homedir(), ".node9", "audit.log");
|
|
13741
14549
|
if (!fs32.existsSync(auditPath)) return "No audit log found.";
|
|
13742
|
-
const
|
|
13743
|
-
const
|
|
13744
|
-
const
|
|
14550
|
+
const rawLines = fs32.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
14551
|
+
const parsed = [];
|
|
14552
|
+
for (const line of rawLines) {
|
|
13745
14553
|
try {
|
|
13746
14554
|
const e = JSON.parse(line);
|
|
13747
|
-
|
|
14555
|
+
const decision = String(e.decision ?? "allow");
|
|
14556
|
+
if (filter && decision !== filter) continue;
|
|
14557
|
+
const argsObj = e.args;
|
|
14558
|
+
let detail = "";
|
|
14559
|
+
if (argsObj) {
|
|
14560
|
+
const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
|
|
14561
|
+
if (typeof cmd === "string" && cmd) {
|
|
14562
|
+
detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
|
|
14563
|
+
}
|
|
14564
|
+
}
|
|
14565
|
+
const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
|
|
14566
|
+
const toolPad = String(e.tool ?? "").padEnd(20);
|
|
14567
|
+
const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
|
|
14568
|
+
parsed.push({ raw: line, decision, formatted: line2 });
|
|
13748
14569
|
} catch {
|
|
13749
|
-
|
|
14570
|
+
parsed.push({ raw: line, decision: "allow", formatted: line });
|
|
13750
14571
|
}
|
|
13751
|
-
}
|
|
13752
|
-
|
|
14572
|
+
}
|
|
14573
|
+
const recent = parsed.slice(-limit);
|
|
14574
|
+
if (recent.length === 0) {
|
|
14575
|
+
return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
|
|
14576
|
+
}
|
|
14577
|
+
const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
|
|
14578
|
+
return `${header}
|
|
13753
14579
|
|
|
13754
|
-
${
|
|
14580
|
+
${recent.map((e) => e.formatted).join("\n")}`;
|
|
13755
14581
|
}
|
|
13756
14582
|
function handlePolicyGet() {
|
|
13757
14583
|
const config = getConfig();
|
|
@@ -13804,10 +14630,43 @@ function handleRuleAdd(args) {
|
|
|
13804
14630
|
writeGlobalConfigRaw(raw);
|
|
13805
14631
|
return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
|
|
13806
14632
|
}
|
|
13807
|
-
function
|
|
13808
|
-
const
|
|
14633
|
+
function runCliCommand(subArgs) {
|
|
14634
|
+
const result = spawnSync7(process.execPath, [process.argv[1], ...subArgs], {
|
|
14635
|
+
encoding: "utf-8",
|
|
14636
|
+
timeout: 6e4,
|
|
14637
|
+
// Disable colors — stdout is piped (not a TTY), chalk auto-detects, but be explicit
|
|
14638
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" }
|
|
14639
|
+
});
|
|
14640
|
+
if (result.error) throw result.error;
|
|
14641
|
+
const out = (result.stdout ?? "").trimEnd();
|
|
14642
|
+
if (!out && result.stderr) throw new Error(result.stderr.trimEnd());
|
|
14643
|
+
return out || "(no output)";
|
|
14644
|
+
}
|
|
14645
|
+
function handleScanMcp(args) {
|
|
14646
|
+
const cliArgs = ["scan"];
|
|
14647
|
+
if (args.drill_down === true) cliArgs.push("--drill-down");
|
|
14648
|
+
return runCliCommand(cliArgs);
|
|
14649
|
+
}
|
|
14650
|
+
function handleReportMcp(args) {
|
|
14651
|
+
const cliArgs = ["report"];
|
|
14652
|
+
if (typeof args.period === "string") cliArgs.push("--period", args.period);
|
|
14653
|
+
if (args.no_tests === true) cliArgs.push("--no-tests");
|
|
14654
|
+
return runCliCommand(cliArgs);
|
|
14655
|
+
}
|
|
14656
|
+
function handleSessionMcp(args) {
|
|
14657
|
+
const cliArgs = ["sessions"];
|
|
14658
|
+
if (typeof args.detail === "string" && args.detail) cliArgs.push("--detail", args.detail);
|
|
14659
|
+
return runCliCommand(cliArgs);
|
|
14660
|
+
}
|
|
14661
|
+
function handleUndoList(args) {
|
|
14662
|
+
const cwdFilter = typeof args.cwd === "string" && args.cwd ? args.cwd : null;
|
|
14663
|
+
let history = getSnapshotHistory();
|
|
14664
|
+
if (cwdFilter) {
|
|
14665
|
+
history = history.filter((e) => e.cwd === cwdFilter);
|
|
14666
|
+
}
|
|
13809
14667
|
if (history.length === 0) {
|
|
13810
|
-
|
|
14668
|
+
const hint = cwdFilter ? ` for cwd: ${cwdFilter}` : "";
|
|
14669
|
+
return `No snapshots found${hint}. Node9 captures snapshots automatically before file edits.`;
|
|
13811
14670
|
}
|
|
13812
14671
|
const lines = history.slice().reverse().map((entry, i) => {
|
|
13813
14672
|
const date = new Date(entry.timestamp).toLocaleString();
|
|
@@ -13816,7 +14675,39 @@ function handleUndoList() {
|
|
|
13816
14675
|
return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
|
|
13817
14676
|
full hash: ${entry.hash}`;
|
|
13818
14677
|
});
|
|
13819
|
-
|
|
14678
|
+
const header = cwdFilter ? `${lines.length} snapshot(s) for ${cwdFilter}:` : `${lines.length} snapshot(s) across all projects:`;
|
|
14679
|
+
return `${header}
|
|
14680
|
+
|
|
14681
|
+
${lines.join("\n\n")}`;
|
|
14682
|
+
}
|
|
14683
|
+
function handleUndoDetail(args) {
|
|
14684
|
+
const hash = args.hash;
|
|
14685
|
+
if (typeof hash !== "string" || !hash) {
|
|
14686
|
+
throw new Error("hash is required");
|
|
14687
|
+
}
|
|
14688
|
+
const history = getSnapshotHistory();
|
|
14689
|
+
const entry = history.find((e) => e.hash === hash || e.hash.startsWith(hash));
|
|
14690
|
+
if (!entry) {
|
|
14691
|
+
throw new Error(`Snapshot ${hash} not found. Run node9_undo_list to see available snapshots.`);
|
|
14692
|
+
}
|
|
14693
|
+
const lines = [];
|
|
14694
|
+
lines.push(`Hash: ${entry.hash}`);
|
|
14695
|
+
lines.push(`Tool: ${entry.tool}`);
|
|
14696
|
+
lines.push(`Summary: ${entry.argsSummary || "(none)"}`);
|
|
14697
|
+
lines.push(`CWD: ${entry.cwd}`);
|
|
14698
|
+
lines.push(`Time: ${new Date(entry.timestamp).toLocaleString()}`);
|
|
14699
|
+
lines.push(`Files: ${entry.files?.length ? entry.files.join(", ") : "(none recorded)"}`);
|
|
14700
|
+
if (entry.diff) {
|
|
14701
|
+
lines.push("");
|
|
14702
|
+
lines.push("\u2500\u2500 Diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
14703
|
+
lines.push(entry.diff);
|
|
14704
|
+
} else {
|
|
14705
|
+
lines.push("");
|
|
14706
|
+
lines.push(
|
|
14707
|
+
"No diff available (first snapshot for this project, or snapshot predates diff capture)."
|
|
14708
|
+
);
|
|
14709
|
+
}
|
|
14710
|
+
return lines.join("\n");
|
|
13820
14711
|
}
|
|
13821
14712
|
function handleUndoRevert(args) {
|
|
13822
14713
|
const hash = args.hash;
|
|
@@ -13884,7 +14775,9 @@ function runMcpServer() {
|
|
|
13884
14775
|
} else if (toolName === "node9_approver_set") {
|
|
13885
14776
|
text = handleApproverSet(toolArgs);
|
|
13886
14777
|
} else if (toolName === "node9_undo_list") {
|
|
13887
|
-
text = handleUndoList();
|
|
14778
|
+
text = handleUndoList(toolArgs);
|
|
14779
|
+
} else if (toolName === "node9_undo_detail") {
|
|
14780
|
+
text = handleUndoDetail(toolArgs);
|
|
13888
14781
|
} else if (toolName === "node9_undo_revert") {
|
|
13889
14782
|
text = handleUndoRevert(toolArgs);
|
|
13890
14783
|
} else if (toolName === "node9_audit_get") {
|
|
@@ -13893,6 +14786,12 @@ function runMcpServer() {
|
|
|
13893
14786
|
text = handlePolicyGet();
|
|
13894
14787
|
} else if (toolName === "node9_rule_add") {
|
|
13895
14788
|
text = handleRuleAdd(toolArgs);
|
|
14789
|
+
} else if (toolName === "node9_scan") {
|
|
14790
|
+
text = handleScanMcp(toolArgs);
|
|
14791
|
+
} else if (toolName === "node9_report") {
|
|
14792
|
+
text = handleReportMcp(toolArgs);
|
|
14793
|
+
} else if (toolName === "node9_session") {
|
|
14794
|
+
text = handleSessionMcp(toolArgs);
|
|
13896
14795
|
} else {
|
|
13897
14796
|
process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
|
|
13898
14797
|
return;
|
|
@@ -14131,7 +15030,8 @@ var SETUP_FN = {
|
|
|
14131
15030
|
cursor: setupCursor,
|
|
14132
15031
|
codex: setupCodex,
|
|
14133
15032
|
windsurf: setupWindsurf,
|
|
14134
|
-
vscode: setupVSCode
|
|
15033
|
+
vscode: setupVSCode,
|
|
15034
|
+
claudeDesktop: setupClaudeDesktop
|
|
14135
15035
|
};
|
|
14136
15036
|
var TEARDOWN_FN = {
|
|
14137
15037
|
claude: teardownClaude,
|
|
@@ -14139,7 +15039,8 @@ var TEARDOWN_FN = {
|
|
|
14139
15039
|
cursor: teardownCursor,
|
|
14140
15040
|
codex: teardownCodex,
|
|
14141
15041
|
windsurf: teardownWindsurf,
|
|
14142
|
-
vscode: teardownVSCode
|
|
15042
|
+
vscode: teardownVSCode,
|
|
15043
|
+
claudeDesktop: teardownClaudeDesktop
|
|
14143
15044
|
};
|
|
14144
15045
|
var AGENT_NAMES = Object.keys(SETUP_FN);
|
|
14145
15046
|
function registerAgentsCommand(program2) {
|
|
@@ -14223,6 +15124,22 @@ function claudeModelPrice2(model) {
|
|
|
14223
15124
|
}
|
|
14224
15125
|
return null;
|
|
14225
15126
|
}
|
|
15127
|
+
var GEMINI_PRICING = {
|
|
15128
|
+
"gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
|
|
15129
|
+
"gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
|
|
15130
|
+
"gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
|
|
15131
|
+
"gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
|
|
15132
|
+
"gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
|
|
15133
|
+
"gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
|
|
15134
|
+
};
|
|
15135
|
+
function geminiModelPrice(model) {
|
|
15136
|
+
const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
15137
|
+
for (const [key, p] of Object.entries(GEMINI_PRICING)) {
|
|
15138
|
+
if (base === key || base.startsWith(key)) return p;
|
|
15139
|
+
}
|
|
15140
|
+
if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
|
|
15141
|
+
return null;
|
|
15142
|
+
}
|
|
14226
15143
|
function num2(n) {
|
|
14227
15144
|
return n.toLocaleString();
|
|
14228
15145
|
}
|
|
@@ -14247,11 +15164,18 @@ function preview(input, max) {
|
|
|
14247
15164
|
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
14248
15165
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
14249
15166
|
}
|
|
15167
|
+
function fullCommand(input) {
|
|
15168
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
15169
|
+
return String(cmd).replace(/\s+/g, " ").trim();
|
|
15170
|
+
}
|
|
15171
|
+
var DEFAULT_RULE_NAMES = new Set(
|
|
15172
|
+
DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
|
|
15173
|
+
);
|
|
14250
15174
|
function buildRuleSources() {
|
|
14251
15175
|
const sources = [];
|
|
14252
15176
|
for (const [shieldName, shield] of Object.entries(SHIELDS)) {
|
|
14253
15177
|
for (const rule of shield.smartRules) {
|
|
14254
|
-
sources.push({ shieldName, shieldLabel: shieldName, rule });
|
|
15178
|
+
sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
|
|
14255
15179
|
}
|
|
14256
15180
|
}
|
|
14257
15181
|
try {
|
|
@@ -14260,9 +15184,12 @@ function buildRuleSources() {
|
|
|
14260
15184
|
if (!rule.name) continue;
|
|
14261
15185
|
if (rule.name.startsWith("shield:")) continue;
|
|
14262
15186
|
const isCloud = rule.name.startsWith("cloud:");
|
|
15187
|
+
const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
|
|
15188
|
+
const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
|
|
14263
15189
|
sources.push({
|
|
14264
|
-
shieldName: isCloud ? "cloud" : "custom",
|
|
14265
|
-
shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
|
|
15190
|
+
shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
|
|
15191
|
+
shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
|
|
15192
|
+
sourceType,
|
|
14266
15193
|
rule
|
|
14267
15194
|
});
|
|
14268
15195
|
}
|
|
@@ -14308,6 +15235,7 @@ function scanClaudeHistory(startDate) {
|
|
|
14308
15235
|
for (const file of files) {
|
|
14309
15236
|
result.filesScanned++;
|
|
14310
15237
|
result.sessions++;
|
|
15238
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
14311
15239
|
let raw;
|
|
14312
15240
|
try {
|
|
14313
15241
|
raw = fs33.readFileSync(path36.join(projPath, file), "utf-8");
|
|
@@ -14365,10 +15293,13 @@ function scanClaudeHistory(startDate) {
|
|
|
14365
15293
|
redactedSample: dlpMatch.redactedSample,
|
|
14366
15294
|
toolName,
|
|
14367
15295
|
timestamp: entry.timestamp ?? "",
|
|
14368
|
-
project: projLabel
|
|
15296
|
+
project: projLabel,
|
|
15297
|
+
sessionId,
|
|
15298
|
+
agent: "claude"
|
|
14369
15299
|
});
|
|
14370
15300
|
}
|
|
14371
15301
|
}
|
|
15302
|
+
let ruleMatched = false;
|
|
14372
15303
|
for (const source of ruleSources) {
|
|
14373
15304
|
const { rule } = source;
|
|
14374
15305
|
if (rule.verdict === "allow") continue;
|
|
@@ -14384,130 +15315,642 @@ function scanClaudeHistory(startDate) {
|
|
|
14384
15315
|
toolName,
|
|
14385
15316
|
input,
|
|
14386
15317
|
timestamp: entry.timestamp ?? "",
|
|
14387
|
-
project: projLabel
|
|
15318
|
+
project: projLabel,
|
|
15319
|
+
sessionId,
|
|
15320
|
+
agent: "claude"
|
|
14388
15321
|
});
|
|
14389
15322
|
}
|
|
15323
|
+
ruleMatched = true;
|
|
14390
15324
|
break;
|
|
14391
15325
|
}
|
|
15326
|
+
if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
|
|
15327
|
+
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
15328
|
+
if (shellVerdict) {
|
|
15329
|
+
const astRule = {
|
|
15330
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15331
|
+
tool: "bash",
|
|
15332
|
+
conditions: [],
|
|
15333
|
+
verdict: shellVerdict,
|
|
15334
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15335
|
+
};
|
|
15336
|
+
const inputPreview = preview(input, 120);
|
|
15337
|
+
const isDupe = result.findings.some(
|
|
15338
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15339
|
+
);
|
|
15340
|
+
if (!isDupe) {
|
|
15341
|
+
result.findings.push({
|
|
15342
|
+
source: {
|
|
15343
|
+
shieldName: "bash-safe",
|
|
15344
|
+
shieldLabel: "bash-safe (AST)",
|
|
15345
|
+
sourceType: "shield",
|
|
15346
|
+
rule: astRule
|
|
15347
|
+
},
|
|
15348
|
+
toolName,
|
|
15349
|
+
input,
|
|
15350
|
+
timestamp: entry.timestamp ?? "",
|
|
15351
|
+
project: projLabel,
|
|
15352
|
+
sessionId,
|
|
15353
|
+
agent: "claude"
|
|
15354
|
+
});
|
|
15355
|
+
}
|
|
15356
|
+
}
|
|
15357
|
+
}
|
|
14392
15358
|
}
|
|
14393
15359
|
}
|
|
14394
15360
|
}
|
|
14395
15361
|
}
|
|
14396
15362
|
return result;
|
|
14397
15363
|
}
|
|
14398
|
-
function
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
15364
|
+
function scanGeminiHistory(startDate) {
|
|
15365
|
+
const tmpDir = path36.join(os29.homedir(), ".gemini", "tmp");
|
|
15366
|
+
const result = {
|
|
15367
|
+
filesScanned: 0,
|
|
15368
|
+
sessions: 0,
|
|
15369
|
+
totalToolCalls: 0,
|
|
15370
|
+
bashCalls: 0,
|
|
15371
|
+
findings: [],
|
|
15372
|
+
dlpFindings: [],
|
|
15373
|
+
totalCostUSD: 0,
|
|
15374
|
+
firstDate: null,
|
|
15375
|
+
lastDate: null
|
|
15376
|
+
};
|
|
15377
|
+
if (!fs33.existsSync(tmpDir)) return result;
|
|
15378
|
+
let slugDirs;
|
|
15379
|
+
try {
|
|
15380
|
+
slugDirs = fs33.readdirSync(tmpDir);
|
|
15381
|
+
} catch {
|
|
15382
|
+
return result;
|
|
15383
|
+
}
|
|
15384
|
+
const ruleSources = buildRuleSources();
|
|
15385
|
+
for (const slug of slugDirs) {
|
|
15386
|
+
const slugPath = path36.join(tmpDir, slug);
|
|
15387
|
+
try {
|
|
15388
|
+
if (!fs33.statSync(slugPath).isDirectory()) continue;
|
|
15389
|
+
} catch {
|
|
15390
|
+
continue;
|
|
14415
15391
|
}
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
console.log(chalk21.yellow(" No JSONL session files found.\n"));
|
|
14421
|
-
return;
|
|
15392
|
+
let projLabel = slug;
|
|
15393
|
+
try {
|
|
15394
|
+
projLabel = fs33.readFileSync(path36.join(slugPath, ".project_root"), "utf-8").trim().replace(os29.homedir(), "~").slice(0, 40);
|
|
15395
|
+
} catch {
|
|
14422
15396
|
}
|
|
14423
|
-
const
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
for (const f of scan.findings) {
|
|
14431
|
-
const key = f.source.shieldName;
|
|
14432
|
-
const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
|
|
14433
|
-
entry.findings.push(f);
|
|
14434
|
-
byShield.set(key, entry);
|
|
15397
|
+
const chatsDir = path36.join(slugPath, "chats");
|
|
15398
|
+
if (!fs33.existsSync(chatsDir)) continue;
|
|
15399
|
+
let chatFiles;
|
|
15400
|
+
try {
|
|
15401
|
+
chatFiles = fs33.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
15402
|
+
} catch {
|
|
15403
|
+
continue;
|
|
14435
15404
|
}
|
|
14436
|
-
const
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
);
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
15405
|
+
for (const chatFile of chatFiles) {
|
|
15406
|
+
result.filesScanned++;
|
|
15407
|
+
const sessionId = chatFile.replace(/\.json$/, "");
|
|
15408
|
+
let raw;
|
|
15409
|
+
try {
|
|
15410
|
+
raw = fs33.readFileSync(path36.join(chatsDir, chatFile), "utf-8");
|
|
15411
|
+
} catch {
|
|
15412
|
+
continue;
|
|
15413
|
+
}
|
|
15414
|
+
let session;
|
|
15415
|
+
try {
|
|
15416
|
+
session = JSON.parse(raw);
|
|
15417
|
+
} catch {
|
|
15418
|
+
continue;
|
|
15419
|
+
}
|
|
15420
|
+
result.sessions++;
|
|
15421
|
+
for (const msg of session.messages ?? []) {
|
|
15422
|
+
if (msg.type !== "gemini") continue;
|
|
15423
|
+
if (startDate && msg.timestamp && new Date(msg.timestamp) < startDate) continue;
|
|
15424
|
+
if (msg.timestamp) {
|
|
15425
|
+
if (!result.firstDate || msg.timestamp < result.firstDate)
|
|
15426
|
+
result.firstDate = msg.timestamp;
|
|
15427
|
+
if (!result.lastDate || msg.timestamp > result.lastDate) result.lastDate = msg.timestamp;
|
|
15428
|
+
}
|
|
15429
|
+
const tokens = msg.tokens;
|
|
15430
|
+
const model = msg.model;
|
|
15431
|
+
if (tokens && model) {
|
|
15432
|
+
const p = geminiModelPrice(model);
|
|
15433
|
+
if (p) {
|
|
15434
|
+
const nonCached = Math.max(0, tokens.input - tokens.cached);
|
|
15435
|
+
result.totalCostUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
|
|
14465
15436
|
}
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
|
|
14472
|
-
|
|
15437
|
+
}
|
|
15438
|
+
for (const tc of msg.toolCalls ?? []) {
|
|
15439
|
+
result.totalToolCalls++;
|
|
15440
|
+
const toolName = tc.name ?? "";
|
|
15441
|
+
const toolNameLower = toolName.toLowerCase();
|
|
15442
|
+
const input = tc.args ?? {};
|
|
15443
|
+
if (toolNameLower === "run_shell_command" || toolNameLower === "shell") {
|
|
15444
|
+
result.bashCalls++;
|
|
15445
|
+
}
|
|
15446
|
+
const rawCmd = String(input.command ?? "").trimStart();
|
|
15447
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
|
|
15448
|
+
continue;
|
|
15449
|
+
const dlpMatch = scanArgs(input);
|
|
15450
|
+
if (dlpMatch) {
|
|
15451
|
+
const isDupe = result.dlpFindings.some(
|
|
15452
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
14473
15453
|
);
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
|
|
14485
|
-
)
|
|
14486
|
-
);
|
|
15454
|
+
if (!isDupe) {
|
|
15455
|
+
result.dlpFindings.push({
|
|
15456
|
+
patternName: dlpMatch.patternName,
|
|
15457
|
+
redactedSample: dlpMatch.redactedSample,
|
|
15458
|
+
toolName,
|
|
15459
|
+
timestamp: msg.timestamp ?? "",
|
|
15460
|
+
project: projLabel,
|
|
15461
|
+
sessionId,
|
|
15462
|
+
agent: "gemini"
|
|
15463
|
+
});
|
|
14487
15464
|
}
|
|
14488
15465
|
}
|
|
14489
|
-
|
|
15466
|
+
let ruleMatched = false;
|
|
15467
|
+
for (const source of ruleSources) {
|
|
15468
|
+
const { rule } = source;
|
|
15469
|
+
if (rule.verdict === "allow") continue;
|
|
15470
|
+
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
15471
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
15472
|
+
const inputPreview = preview(input, 120);
|
|
15473
|
+
const isDupe = result.findings.some(
|
|
15474
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15475
|
+
);
|
|
15476
|
+
if (!isDupe) {
|
|
15477
|
+
result.findings.push({
|
|
15478
|
+
source,
|
|
15479
|
+
toolName,
|
|
15480
|
+
input,
|
|
15481
|
+
timestamp: msg.timestamp ?? "",
|
|
15482
|
+
project: projLabel,
|
|
15483
|
+
sessionId,
|
|
15484
|
+
agent: "gemini"
|
|
15485
|
+
});
|
|
15486
|
+
}
|
|
15487
|
+
ruleMatched = true;
|
|
15488
|
+
break;
|
|
15489
|
+
}
|
|
15490
|
+
const isShellTool = ["bash", "execute_bash", "run_shell_command", "shell"].includes(
|
|
15491
|
+
toolNameLower
|
|
15492
|
+
);
|
|
15493
|
+
if (!ruleMatched && isShellTool) {
|
|
15494
|
+
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
15495
|
+
if (shellVerdict) {
|
|
15496
|
+
const astRule = {
|
|
15497
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15498
|
+
tool: "bash",
|
|
15499
|
+
conditions: [],
|
|
15500
|
+
verdict: shellVerdict,
|
|
15501
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15502
|
+
};
|
|
15503
|
+
const inputPreview = preview(input, 120);
|
|
15504
|
+
const isDupe = result.findings.some(
|
|
15505
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15506
|
+
);
|
|
15507
|
+
if (!isDupe) {
|
|
15508
|
+
result.findings.push({
|
|
15509
|
+
source: {
|
|
15510
|
+
shieldName: "bash-safe",
|
|
15511
|
+
shieldLabel: "bash-safe (AST)",
|
|
15512
|
+
sourceType: "shield",
|
|
15513
|
+
rule: astRule
|
|
15514
|
+
},
|
|
15515
|
+
toolName,
|
|
15516
|
+
input,
|
|
15517
|
+
timestamp: msg.timestamp ?? "",
|
|
15518
|
+
project: projLabel,
|
|
15519
|
+
sessionId,
|
|
15520
|
+
agent: "gemini"
|
|
15521
|
+
});
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
}
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
}
|
|
15528
|
+
}
|
|
15529
|
+
return result;
|
|
15530
|
+
}
|
|
15531
|
+
function scanCodexHistory(startDate) {
|
|
15532
|
+
const sessionsBase = path36.join(os29.homedir(), ".codex", "sessions");
|
|
15533
|
+
const result = {
|
|
15534
|
+
filesScanned: 0,
|
|
15535
|
+
sessions: 0,
|
|
15536
|
+
totalToolCalls: 0,
|
|
15537
|
+
bashCalls: 0,
|
|
15538
|
+
findings: [],
|
|
15539
|
+
dlpFindings: [],
|
|
15540
|
+
totalCostUSD: 0,
|
|
15541
|
+
firstDate: null,
|
|
15542
|
+
lastDate: null
|
|
15543
|
+
};
|
|
15544
|
+
if (!fs33.existsSync(sessionsBase)) return result;
|
|
15545
|
+
const jsonlFiles = [];
|
|
15546
|
+
try {
|
|
15547
|
+
for (const year of fs33.readdirSync(sessionsBase)) {
|
|
15548
|
+
const yearPath = path36.join(sessionsBase, year);
|
|
15549
|
+
try {
|
|
15550
|
+
if (!fs33.statSync(yearPath).isDirectory()) continue;
|
|
15551
|
+
} catch {
|
|
15552
|
+
continue;
|
|
15553
|
+
}
|
|
15554
|
+
for (const month of fs33.readdirSync(yearPath)) {
|
|
15555
|
+
const monthPath = path36.join(yearPath, month);
|
|
15556
|
+
try {
|
|
15557
|
+
if (!fs33.statSync(monthPath).isDirectory()) continue;
|
|
15558
|
+
} catch {
|
|
15559
|
+
continue;
|
|
15560
|
+
}
|
|
15561
|
+
for (const day of fs33.readdirSync(monthPath)) {
|
|
15562
|
+
const dayPath = path36.join(monthPath, day);
|
|
15563
|
+
try {
|
|
15564
|
+
if (!fs33.statSync(dayPath).isDirectory()) continue;
|
|
15565
|
+
} catch {
|
|
15566
|
+
continue;
|
|
15567
|
+
}
|
|
15568
|
+
for (const file of fs33.readdirSync(dayPath)) {
|
|
15569
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path36.join(dayPath, file));
|
|
15570
|
+
}
|
|
15571
|
+
}
|
|
15572
|
+
}
|
|
15573
|
+
}
|
|
15574
|
+
} catch {
|
|
15575
|
+
return result;
|
|
15576
|
+
}
|
|
15577
|
+
const ruleSources = buildRuleSources();
|
|
15578
|
+
for (const filePath of jsonlFiles) {
|
|
15579
|
+
result.filesScanned++;
|
|
15580
|
+
let lines;
|
|
15581
|
+
try {
|
|
15582
|
+
lines = fs33.readFileSync(filePath, "utf-8").split("\n");
|
|
15583
|
+
} catch {
|
|
15584
|
+
continue;
|
|
15585
|
+
}
|
|
15586
|
+
let sessionId = "";
|
|
15587
|
+
let startTime = "";
|
|
15588
|
+
let projLabel = "";
|
|
15589
|
+
result.sessions++;
|
|
15590
|
+
let lastTotalInput = 0;
|
|
15591
|
+
let lastTotalCached = 0;
|
|
15592
|
+
let lastTotalOutput = 0;
|
|
15593
|
+
for (const line of lines) {
|
|
15594
|
+
if (!line.trim()) continue;
|
|
15595
|
+
let entry;
|
|
15596
|
+
try {
|
|
15597
|
+
entry = JSON.parse(line);
|
|
15598
|
+
} catch {
|
|
15599
|
+
continue;
|
|
15600
|
+
}
|
|
15601
|
+
const payload = entry.payload ?? {};
|
|
15602
|
+
if (entry.type === "session_meta") {
|
|
15603
|
+
sessionId = String(payload["id"] ?? filePath);
|
|
15604
|
+
startTime = String(payload["timestamp"] ?? "");
|
|
15605
|
+
const cwd = String(payload["cwd"] ?? "");
|
|
15606
|
+
projLabel = cwd.replace(os29.homedir(), "~").slice(0, 40);
|
|
15607
|
+
continue;
|
|
15608
|
+
}
|
|
15609
|
+
if (entry.type === "event_msg" && payload["type"] === "token_count") {
|
|
15610
|
+
const info = payload["info"];
|
|
15611
|
+
const usage = info?.["total_token_usage"] ?? {};
|
|
15612
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
15613
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
15614
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
15615
|
+
continue;
|
|
15616
|
+
}
|
|
15617
|
+
if (entry.type !== "response_item") continue;
|
|
15618
|
+
if (payload["type"] !== "function_call") continue;
|
|
15619
|
+
const ts = startTime;
|
|
15620
|
+
if (startDate && ts && new Date(ts) < startDate) continue;
|
|
15621
|
+
if (ts) {
|
|
15622
|
+
if (!result.firstDate || ts < result.firstDate) result.firstDate = ts;
|
|
15623
|
+
if (!result.lastDate || ts > result.lastDate) result.lastDate = ts;
|
|
15624
|
+
}
|
|
15625
|
+
result.totalToolCalls++;
|
|
15626
|
+
const toolName = String(payload["name"] ?? "");
|
|
15627
|
+
const toolNameLower = toolName.toLowerCase();
|
|
15628
|
+
let input = {};
|
|
15629
|
+
try {
|
|
15630
|
+
input = JSON.parse(String(payload["arguments"] ?? "{}"));
|
|
15631
|
+
} catch {
|
|
15632
|
+
}
|
|
15633
|
+
if ("cmd" in input && !("command" in input)) {
|
|
15634
|
+
input = { ...input, command: input["cmd"] };
|
|
15635
|
+
}
|
|
15636
|
+
if (toolNameLower === "exec_command" || toolNameLower === "shell") {
|
|
15637
|
+
result.bashCalls++;
|
|
15638
|
+
}
|
|
15639
|
+
const rawCmd = String(input["command"] ?? "").trimStart();
|
|
15640
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
|
|
15641
|
+
const dlpMatch = scanArgs(input);
|
|
15642
|
+
if (dlpMatch) {
|
|
15643
|
+
const isDupe = result.dlpFindings.some(
|
|
15644
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
15645
|
+
);
|
|
15646
|
+
if (!isDupe) {
|
|
15647
|
+
result.dlpFindings.push({
|
|
15648
|
+
patternName: dlpMatch.patternName,
|
|
15649
|
+
redactedSample: dlpMatch.redactedSample,
|
|
15650
|
+
toolName,
|
|
15651
|
+
timestamp: ts,
|
|
15652
|
+
project: projLabel,
|
|
15653
|
+
sessionId,
|
|
15654
|
+
agent: "codex"
|
|
15655
|
+
});
|
|
15656
|
+
}
|
|
15657
|
+
}
|
|
15658
|
+
let ruleMatched = false;
|
|
15659
|
+
for (const source of ruleSources) {
|
|
15660
|
+
const { rule } = source;
|
|
15661
|
+
if (rule.verdict === "allow") continue;
|
|
15662
|
+
if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
|
|
15663
|
+
continue;
|
|
15664
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
15665
|
+
const inputPreview = preview(input, 120);
|
|
15666
|
+
const isDupe = result.findings.some(
|
|
15667
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15668
|
+
);
|
|
15669
|
+
if (!isDupe) {
|
|
15670
|
+
result.findings.push({
|
|
15671
|
+
source,
|
|
15672
|
+
toolName,
|
|
15673
|
+
input,
|
|
15674
|
+
timestamp: ts,
|
|
15675
|
+
project: projLabel,
|
|
15676
|
+
sessionId,
|
|
15677
|
+
agent: "codex"
|
|
15678
|
+
});
|
|
15679
|
+
}
|
|
15680
|
+
ruleMatched = true;
|
|
15681
|
+
break;
|
|
15682
|
+
}
|
|
15683
|
+
if (!ruleMatched && (toolNameLower === "exec_command" || toolNameLower === "shell")) {
|
|
15684
|
+
const shellVerdict = detectDangerousShellExec(String(input["command"] ?? ""));
|
|
15685
|
+
if (shellVerdict) {
|
|
15686
|
+
const astRule = {
|
|
15687
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15688
|
+
tool: "bash",
|
|
15689
|
+
conditions: [],
|
|
15690
|
+
verdict: shellVerdict,
|
|
15691
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15692
|
+
};
|
|
15693
|
+
const inputPreview = preview(input, 120);
|
|
15694
|
+
const isDupe = result.findings.some(
|
|
15695
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15696
|
+
);
|
|
15697
|
+
if (!isDupe) {
|
|
15698
|
+
result.findings.push({
|
|
15699
|
+
source: {
|
|
15700
|
+
shieldName: "bash-safe",
|
|
15701
|
+
shieldLabel: "bash-safe (AST)",
|
|
15702
|
+
sourceType: "shield",
|
|
15703
|
+
rule: astRule
|
|
15704
|
+
},
|
|
15705
|
+
toolName,
|
|
15706
|
+
input,
|
|
15707
|
+
timestamp: ts,
|
|
15708
|
+
project: projLabel,
|
|
15709
|
+
sessionId,
|
|
15710
|
+
agent: "codex"
|
|
15711
|
+
});
|
|
15712
|
+
}
|
|
14490
15713
|
}
|
|
14491
15714
|
}
|
|
15715
|
+
}
|
|
15716
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
15717
|
+
result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
15718
|
+
}
|
|
15719
|
+
return result;
|
|
15720
|
+
}
|
|
15721
|
+
function mergeScans(a, b) {
|
|
15722
|
+
const dates = [a.firstDate, b.firstDate].filter(Boolean);
|
|
15723
|
+
const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
|
|
15724
|
+
return {
|
|
15725
|
+
filesScanned: a.filesScanned + b.filesScanned,
|
|
15726
|
+
sessions: a.sessions + b.sessions,
|
|
15727
|
+
totalToolCalls: a.totalToolCalls + b.totalToolCalls,
|
|
15728
|
+
bashCalls: a.bashCalls + b.bashCalls,
|
|
15729
|
+
findings: [...a.findings, ...b.findings],
|
|
15730
|
+
dlpFindings: [...a.dlpFindings, ...b.dlpFindings],
|
|
15731
|
+
totalCostUSD: a.totalCostUSD + b.totalCostUSD,
|
|
15732
|
+
firstDate: dates.length ? dates.sort()[0] : null,
|
|
15733
|
+
lastDate: lastDates.length ? lastDates.sort().at(-1) : null
|
|
15734
|
+
};
|
|
15735
|
+
}
|
|
15736
|
+
function verdictIcon(verdict) {
|
|
15737
|
+
return verdict === "block" ? "\u{1F6D1}" : "\u{1F441} ";
|
|
15738
|
+
}
|
|
15739
|
+
function printFindingRow(f, drillDown, showSessionId, previewWidth) {
|
|
15740
|
+
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
15741
|
+
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
15742
|
+
const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : f.agent === "codex" ? chalk21.magenta("[Codex] ") : chalk21.cyan("[Claude] ");
|
|
15743
|
+
const cmd = drillDown ? chalk21.gray(fullCommand(f.input)) : chalk21.gray(preview(f.input, previewWidth));
|
|
15744
|
+
const sessionSuffix = showSessionId && f.sessionId ? chalk21.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
15745
|
+
console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
|
|
15746
|
+
}
|
|
15747
|
+
function printRuleGroup(ruleFindings, topN, drillDown, previewWidth) {
|
|
15748
|
+
const rule = ruleFindings[0].source.rule;
|
|
15749
|
+
const ruleCount = ruleFindings.length;
|
|
15750
|
+
const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
|
|
15751
|
+
const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
15752
|
+
const icon = verdictIcon(rule.verdict ?? "review");
|
|
15753
|
+
console.log(
|
|
15754
|
+
" " + icon + " " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
|
|
15755
|
+
);
|
|
15756
|
+
const shown = drillDown ? ruleFindings : ruleFindings.slice(0, topN);
|
|
15757
|
+
for (const f of shown) {
|
|
15758
|
+
printFindingRow(f, drillDown, drillDown, previewWidth);
|
|
15759
|
+
}
|
|
15760
|
+
if (!drillDown && ruleFindings.length > topN) {
|
|
15761
|
+
console.log(
|
|
15762
|
+
chalk21.dim(` \u2026 and ${ruleFindings.length - topN} more (--drill-down for full list)`)
|
|
15763
|
+
);
|
|
15764
|
+
}
|
|
15765
|
+
}
|
|
15766
|
+
function registerScanCommand(program2) {
|
|
15767
|
+
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 rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").action((options) => {
|
|
15768
|
+
const drillDown = options.drillDown ?? false;
|
|
15769
|
+
const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
|
|
15770
|
+
const previewWidth = 70;
|
|
15771
|
+
const startDate = options.all ? null : (() => {
|
|
15772
|
+
const d = /* @__PURE__ */ new Date();
|
|
15773
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
15774
|
+
d.setHours(0, 0, 0, 0);
|
|
15775
|
+
return d;
|
|
15776
|
+
})();
|
|
15777
|
+
const isInstalled = fs33.existsSync(path36.join(os29.homedir(), ".node9", "audit.log"));
|
|
15778
|
+
console.log("");
|
|
15779
|
+
if (!isInstalled) {
|
|
15780
|
+
console.log(
|
|
15781
|
+
chalk21.bold("\u{1F6E1} node9") + chalk21.dim(" \u2014 security layer for AI coding agents")
|
|
15782
|
+
);
|
|
15783
|
+
console.log(
|
|
15784
|
+
chalk21.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
|
|
15785
|
+
);
|
|
15786
|
+
console.log("");
|
|
15787
|
+
}
|
|
15788
|
+
console.log(
|
|
15789
|
+
chalk21.cyan.bold("\u{1F50D} Scanning your AI history") + chalk21.dim(" \u2014 what would node9 have caught?")
|
|
15790
|
+
);
|
|
15791
|
+
console.log("");
|
|
15792
|
+
process.stdout.write(chalk21.dim(" Scanning\u2026"));
|
|
15793
|
+
const claudeScan = scanClaudeHistory(startDate);
|
|
15794
|
+
const geminiScan = scanGeminiHistory(startDate);
|
|
15795
|
+
const codexScan = scanCodexHistory(startDate);
|
|
15796
|
+
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
15797
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
15798
|
+
if (scan.filesScanned === 0) {
|
|
15799
|
+
console.log(chalk21.yellow(" No session history found."));
|
|
15800
|
+
console.log(
|
|
15801
|
+
chalk21.gray(
|
|
15802
|
+
" Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
|
|
15803
|
+
)
|
|
15804
|
+
);
|
|
15805
|
+
return;
|
|
15806
|
+
}
|
|
15807
|
+
const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
|
|
15808
|
+
const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
15809
|
+
const breakdownParts = [];
|
|
15810
|
+
if (claudeScan.sessions > 0)
|
|
15811
|
+
breakdownParts.push(chalk21.cyan(String(claudeScan.sessions)) + chalk21.dim(" Claude"));
|
|
15812
|
+
if (geminiScan.sessions > 0)
|
|
15813
|
+
breakdownParts.push(chalk21.blue(String(geminiScan.sessions)) + chalk21.dim(" Gemini"));
|
|
15814
|
+
if (codexScan.sessions > 0)
|
|
15815
|
+
breakdownParts.push(chalk21.magenta(String(codexScan.sessions)) + chalk21.dim(" Codex"));
|
|
15816
|
+
const sessionBreakdown = breakdownParts.length > 1 ? chalk21.dim("(") + breakdownParts.join(chalk21.dim(" \xB7 ")) + chalk21.dim(")") : "";
|
|
15817
|
+
console.log(
|
|
15818
|
+
" " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
|
|
15819
|
+
);
|
|
15820
|
+
console.log("");
|
|
15821
|
+
const totalFindings = scan.findings.length;
|
|
15822
|
+
const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
|
|
15823
|
+
const reviewCount = totalFindings - blockedCount;
|
|
15824
|
+
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
15825
|
+
console.log(chalk21.green(" \u2705 No risky operations found in your history."));
|
|
15826
|
+
console.log(
|
|
15827
|
+
chalk21.dim(" node9 is still worth running \u2014 it monitors every tool call in real time.\n")
|
|
15828
|
+
);
|
|
15829
|
+
} else {
|
|
15830
|
+
const totalRisky = totalFindings + scan.dlpFindings.length;
|
|
15831
|
+
const heroLine = isInstalled ? chalk21.bold(
|
|
15832
|
+
` Found ${chalk21.yellow(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} in your history`
|
|
15833
|
+
) : chalk21.bold(
|
|
15834
|
+
` ${chalk21.red.bold(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} found \u2014 none were blocked`
|
|
15835
|
+
);
|
|
15836
|
+
console.log(heroLine);
|
|
15837
|
+
console.log("");
|
|
15838
|
+
if (blockedCount > 0) {
|
|
15839
|
+
console.log(
|
|
15840
|
+
" " + chalk21.red("\u{1F6D1} Would have blocked") + " " + chalk21.red.bold(String(blockedCount).padStart(5)) + chalk21.dim(" operations stopped before execution")
|
|
15841
|
+
);
|
|
15842
|
+
}
|
|
15843
|
+
if (reviewCount > 0) {
|
|
15844
|
+
console.log(
|
|
15845
|
+
" " + chalk21.yellow("\u{1F441} Would have flagged") + " " + chalk21.yellow.bold(String(reviewCount).padStart(5)) + chalk21.dim(" sent to you for approval")
|
|
15846
|
+
);
|
|
15847
|
+
}
|
|
15848
|
+
if (scan.dlpFindings.length > 0) {
|
|
15849
|
+
console.log(
|
|
15850
|
+
" " + chalk21.red("\u{1F511} Credential leak") + " " + chalk21.red.bold(String(scan.dlpFindings.length).padStart(5)) + chalk21.dim(" secret detected in tool call")
|
|
15851
|
+
);
|
|
15852
|
+
}
|
|
15853
|
+
console.log("");
|
|
15854
|
+
const sections = [];
|
|
15855
|
+
const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
|
|
15856
|
+
if (defaultFindings.length > 0) {
|
|
15857
|
+
sections.push({
|
|
15858
|
+
label: "Default Rules",
|
|
15859
|
+
subtitle: "built-in, always on",
|
|
15860
|
+
findings: defaultFindings
|
|
15861
|
+
});
|
|
15862
|
+
}
|
|
15863
|
+
const byShield = /* @__PURE__ */ new Map();
|
|
15864
|
+
for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
|
|
15865
|
+
const arr = byShield.get(f.source.shieldName) ?? [];
|
|
15866
|
+
arr.push(f);
|
|
15867
|
+
byShield.set(f.source.shieldName, arr);
|
|
15868
|
+
}
|
|
15869
|
+
const shieldsWithFindings = [...byShield.entries()].sort(
|
|
15870
|
+
(a, b) => b[1].length - a[1].length
|
|
15871
|
+
);
|
|
15872
|
+
for (const [shieldName, findings] of shieldsWithFindings) {
|
|
15873
|
+
const description = SHIELDS[shieldName]?.description ?? "";
|
|
15874
|
+
sections.push({
|
|
15875
|
+
label: shieldName,
|
|
15876
|
+
subtitle: description,
|
|
15877
|
+
shieldKey: shieldName,
|
|
15878
|
+
findings
|
|
15879
|
+
});
|
|
15880
|
+
}
|
|
15881
|
+
const userFindings = scan.findings.filter(
|
|
15882
|
+
(f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
|
|
15883
|
+
);
|
|
15884
|
+
if (userFindings.length > 0) {
|
|
15885
|
+
sections.push({
|
|
15886
|
+
label: "Your Rules",
|
|
15887
|
+
subtitle: "added in node9.config.json",
|
|
15888
|
+
findings: userFindings
|
|
15889
|
+
});
|
|
15890
|
+
}
|
|
15891
|
+
for (const section of sections) {
|
|
15892
|
+
const sectionBlocked = section.findings.filter(
|
|
15893
|
+
(f) => f.source.rule.verdict === "block"
|
|
15894
|
+
).length;
|
|
15895
|
+
const sectionReview = section.findings.length - sectionBlocked;
|
|
15896
|
+
const countParts = [];
|
|
15897
|
+
if (sectionBlocked > 0) countParts.push(chalk21.red(`${sectionBlocked} blocked`));
|
|
15898
|
+
if (sectionReview > 0) countParts.push(chalk21.yellow(`${sectionReview} review`));
|
|
15899
|
+
const countStr = countParts.join(chalk21.dim(" \xB7 "));
|
|
15900
|
+
const enableHint = section.shieldKey ? chalk21.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
15901
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
15902
|
+
console.log(
|
|
15903
|
+
" " + chalk21.bold(section.label) + (section.subtitle ? chalk21.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
|
|
15904
|
+
);
|
|
15905
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
15906
|
+
for (const f of section.findings) {
|
|
15907
|
+
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
15908
|
+
const arr = byRule.get(ruleKey) ?? [];
|
|
15909
|
+
arr.push(f);
|
|
15910
|
+
byRule.set(ruleKey, arr);
|
|
15911
|
+
}
|
|
15912
|
+
const sortedRules = [...byRule.entries()].sort((a, b) => {
|
|
15913
|
+
const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
15914
|
+
const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
15915
|
+
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
15916
|
+
return b[1].length - a[1].length;
|
|
15917
|
+
});
|
|
15918
|
+
for (const [, ruleFindings] of sortedRules) {
|
|
15919
|
+
printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
|
|
15920
|
+
}
|
|
15921
|
+
console.log("");
|
|
15922
|
+
}
|
|
15923
|
+
const emptyShields = Object.keys(SHIELDS).filter((n) => !byShield.has(n)).sort();
|
|
15924
|
+
if (emptyShields.length > 0) {
|
|
15925
|
+
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
15926
|
+
console.log(
|
|
15927
|
+
" " + chalk21.bold("Shields") + chalk21.dim(" \xB7 no findings in your history") + " " + chalk21.green("\u2713")
|
|
15928
|
+
);
|
|
15929
|
+
console.log(" " + chalk21.dim(emptyShields.join(" \xB7 ")));
|
|
15930
|
+
console.log(" " + chalk21.dim("\u2192 node9 shield enable <name> to activate any shield"));
|
|
15931
|
+
console.log("");
|
|
15932
|
+
}
|
|
14492
15933
|
if (scan.dlpFindings.length > 0) {
|
|
14493
15934
|
console.log(" " + chalk21.dim("\u2500".repeat(70)));
|
|
14494
15935
|
console.log(
|
|
14495
|
-
" " + chalk21.red.bold("
|
|
15936
|
+
" " + chalk21.red.bold("\u{1F511} Credential Leaks") + chalk21.dim(" \xB7 ") + chalk21.red(
|
|
14496
15937
|
`${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
|
|
14497
15938
|
)
|
|
14498
15939
|
);
|
|
14499
|
-
const shownDlp = scan.dlpFindings.slice(0, topN);
|
|
15940
|
+
const shownDlp = drillDown ? scan.dlpFindings : scan.dlpFindings.slice(0, topN);
|
|
14500
15941
|
for (const f of shownDlp) {
|
|
14501
15942
|
const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
|
|
14502
15943
|
const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
15944
|
+
const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : f.agent === "codex" ? chalk21.magenta("[Codex] ") : chalk21.cyan("[Claude] ");
|
|
15945
|
+
const sessionSuffix = f.sessionId ? chalk21.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
14503
15946
|
console.log(
|
|
14504
|
-
` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
|
|
15947
|
+
` ${ts}${proj}${agentBadge}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample) + sessionSuffix
|
|
14505
15948
|
);
|
|
14506
15949
|
}
|
|
14507
|
-
if (scan.dlpFindings.length > topN) {
|
|
15950
|
+
if (!drillDown && scan.dlpFindings.length > topN) {
|
|
14508
15951
|
console.log(
|
|
14509
15952
|
chalk21.dim(
|
|
14510
|
-
` \u2026 and ${scan.dlpFindings.length - topN} more
|
|
15953
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
|
|
14511
15954
|
)
|
|
14512
15955
|
);
|
|
14513
15956
|
}
|
|
@@ -14516,21 +15959,45 @@ function registerScanCommand(program2) {
|
|
|
14516
15959
|
}
|
|
14517
15960
|
if (scan.totalCostUSD > 0) {
|
|
14518
15961
|
console.log(
|
|
14519
|
-
" " + chalk21.bold("
|
|
15962
|
+
" " + chalk21.bold("Agent spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
|
|
14520
15963
|
);
|
|
14521
15964
|
console.log("");
|
|
14522
15965
|
}
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
|
|
15966
|
+
if (isInstalled) {
|
|
15967
|
+
console.log(chalk21.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
|
|
14526
15968
|
console.log(
|
|
14527
|
-
chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
|
|
15969
|
+
chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live protection stats.")
|
|
14528
15970
|
);
|
|
15971
|
+
if (drillDown) {
|
|
15972
|
+
console.log(
|
|
15973
|
+
chalk21.dim(" Run ") + chalk21.cyan("node9 sessions --detail <session-id>") + chalk21.dim(" to see the full conversation for any session above.")
|
|
15974
|
+
);
|
|
15975
|
+
} else {
|
|
15976
|
+
console.log(
|
|
15977
|
+
chalk21.dim(" Run ") + chalk21.cyan("node9 scan --drill-down") + chalk21.dim(" to see full commands and session IDs.")
|
|
15978
|
+
);
|
|
15979
|
+
}
|
|
14529
15980
|
} else {
|
|
14530
|
-
|
|
15981
|
+
const riskySummary = totalFindings + scan.dlpFindings.length;
|
|
15982
|
+
if (riskySummary > 0) {
|
|
15983
|
+
console.log(
|
|
15984
|
+
chalk21.yellow.bold(
|
|
15985
|
+
` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
|
|
15986
|
+
) + chalk21.dim(" node9 would have caught them.")
|
|
15987
|
+
);
|
|
15988
|
+
}
|
|
15989
|
+
console.log("");
|
|
15990
|
+
console.log(chalk21.bold(" Protect your next session in 30 seconds:"));
|
|
15991
|
+
console.log("");
|
|
15992
|
+
console.log(" " + chalk21.cyan("npm install -g @node9/proxy"));
|
|
15993
|
+
console.log(" " + chalk21.cyan("node9 init"));
|
|
15994
|
+
console.log("");
|
|
15995
|
+
console.log(chalk21.dim(" node9 hooks into Claude Code automatically."));
|
|
14531
15996
|
console.log(
|
|
14532
|
-
|
|
15997
|
+
chalk21.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
|
|
14533
15998
|
);
|
|
15999
|
+
console.log("");
|
|
16000
|
+
console.log(" " + chalk21.dim("\u2192 ") + chalk21.underline("https://node9.ai"));
|
|
14534
16001
|
}
|
|
14535
16002
|
console.log("");
|
|
14536
16003
|
});
|
|
@@ -14560,6 +16027,22 @@ function modelPrice(model) {
|
|
|
14560
16027
|
}
|
|
14561
16028
|
return null;
|
|
14562
16029
|
}
|
|
16030
|
+
var GEMINI_PRICING2 = {
|
|
16031
|
+
"gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
|
|
16032
|
+
"gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
|
|
16033
|
+
"gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
|
|
16034
|
+
"gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
|
|
16035
|
+
"gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
|
|
16036
|
+
"gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
|
|
16037
|
+
};
|
|
16038
|
+
function geminiModelPrice2(model) {
|
|
16039
|
+
const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
16040
|
+
for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
|
|
16041
|
+
if (base === key || base.startsWith(key)) return p;
|
|
16042
|
+
}
|
|
16043
|
+
if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
|
|
16044
|
+
return null;
|
|
16045
|
+
}
|
|
14563
16046
|
function encodeProjectPath(projectPath) {
|
|
14564
16047
|
return projectPath.replace(/\//g, "-");
|
|
14565
16048
|
}
|
|
@@ -14675,6 +16158,246 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
14675
16158
|
}
|
|
14676
16159
|
return result;
|
|
14677
16160
|
}
|
|
16161
|
+
function buildGeminiSessions(days, allAuditEntries) {
|
|
16162
|
+
const tmpDir = path37.join(os30.homedir(), ".gemini", "tmp");
|
|
16163
|
+
if (!fs34.existsSync(tmpDir)) return [];
|
|
16164
|
+
const cutoff = days !== null ? (() => {
|
|
16165
|
+
const d = /* @__PURE__ */ new Date();
|
|
16166
|
+
d.setDate(d.getDate() - days);
|
|
16167
|
+
d.setHours(0, 0, 0, 0);
|
|
16168
|
+
return d;
|
|
16169
|
+
})() : null;
|
|
16170
|
+
let slugDirs;
|
|
16171
|
+
try {
|
|
16172
|
+
slugDirs = fs34.readdirSync(tmpDir);
|
|
16173
|
+
} catch {
|
|
16174
|
+
return [];
|
|
16175
|
+
}
|
|
16176
|
+
const summaries = [];
|
|
16177
|
+
for (const slug of slugDirs) {
|
|
16178
|
+
const slugPath = path37.join(tmpDir, slug);
|
|
16179
|
+
try {
|
|
16180
|
+
if (!fs34.statSync(slugPath).isDirectory()) continue;
|
|
16181
|
+
} catch {
|
|
16182
|
+
continue;
|
|
16183
|
+
}
|
|
16184
|
+
let projectRoot = path37.join(os30.homedir(), slug);
|
|
16185
|
+
try {
|
|
16186
|
+
projectRoot = fs34.readFileSync(path37.join(slugPath, ".project_root"), "utf-8").trim();
|
|
16187
|
+
} catch {
|
|
16188
|
+
}
|
|
16189
|
+
const chatsDir = path37.join(slugPath, "chats");
|
|
16190
|
+
if (!fs34.existsSync(chatsDir)) continue;
|
|
16191
|
+
let chatFiles;
|
|
16192
|
+
try {
|
|
16193
|
+
chatFiles = fs34.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
16194
|
+
} catch {
|
|
16195
|
+
continue;
|
|
16196
|
+
}
|
|
16197
|
+
for (const chatFile of chatFiles) {
|
|
16198
|
+
let raw;
|
|
16199
|
+
try {
|
|
16200
|
+
raw = fs34.readFileSync(path37.join(chatsDir, chatFile), "utf-8");
|
|
16201
|
+
} catch {
|
|
16202
|
+
continue;
|
|
16203
|
+
}
|
|
16204
|
+
let session;
|
|
16205
|
+
try {
|
|
16206
|
+
session = JSON.parse(raw);
|
|
16207
|
+
} catch {
|
|
16208
|
+
continue;
|
|
16209
|
+
}
|
|
16210
|
+
const startTime = session.startTime ?? "";
|
|
16211
|
+
if (!startTime) continue;
|
|
16212
|
+
if (cutoff && new Date(startTime) < cutoff) continue;
|
|
16213
|
+
let firstPrompt = "";
|
|
16214
|
+
for (const msg of session.messages ?? []) {
|
|
16215
|
+
if (msg.type === "user") {
|
|
16216
|
+
const content = msg.content;
|
|
16217
|
+
if (Array.isArray(content) && content[0]?.text) {
|
|
16218
|
+
firstPrompt = content[0].text;
|
|
16219
|
+
} else if (typeof content === "string") {
|
|
16220
|
+
firstPrompt = content;
|
|
16221
|
+
}
|
|
16222
|
+
break;
|
|
16223
|
+
}
|
|
16224
|
+
}
|
|
16225
|
+
const toolCalls = [];
|
|
16226
|
+
let costUSD = 0;
|
|
16227
|
+
const modifiedFiles = [];
|
|
16228
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
16229
|
+
let lastToolTs = "";
|
|
16230
|
+
for (const msg of session.messages ?? []) {
|
|
16231
|
+
if (msg.type !== "gemini") continue;
|
|
16232
|
+
const tokens = msg.tokens;
|
|
16233
|
+
const model = msg.model;
|
|
16234
|
+
if (tokens && model) {
|
|
16235
|
+
const p = geminiModelPrice2(model);
|
|
16236
|
+
if (p) {
|
|
16237
|
+
const nonCached = Math.max(0, tokens.input - tokens.cached);
|
|
16238
|
+
costUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
|
|
16239
|
+
}
|
|
16240
|
+
}
|
|
16241
|
+
for (const tc of msg.toolCalls ?? []) {
|
|
16242
|
+
const tool = tc.name ?? "";
|
|
16243
|
+
const input = tc.args ?? {};
|
|
16244
|
+
const ts = msg.timestamp ?? "";
|
|
16245
|
+
toolCalls.push({ tool, input, timestamp: ts });
|
|
16246
|
+
if (ts > lastToolTs) lastToolTs = ts;
|
|
16247
|
+
const toolLower = tool.toLowerCase();
|
|
16248
|
+
if (toolLower === "write_file" || toolLower === "edit_file" || toolLower === "create_file" || toolLower === "overwrite_file") {
|
|
16249
|
+
const fp = input.file_path ?? input.path ?? input.filename;
|
|
16250
|
+
if (typeof fp === "string" && !seenFiles.has(fp)) {
|
|
16251
|
+
seenFiles.add(fp);
|
|
16252
|
+
modifiedFiles.push(fp);
|
|
16253
|
+
}
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
}
|
|
16257
|
+
const windowEnd = new Date(
|
|
16258
|
+
Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
16259
|
+
).toISOString();
|
|
16260
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
|
|
16261
|
+
summaries.push({
|
|
16262
|
+
sessionId: session.sessionId ?? chatFile.replace(".json", ""),
|
|
16263
|
+
project: projectRoot,
|
|
16264
|
+
projectLabel: projectLabel(projectRoot),
|
|
16265
|
+
firstPrompt,
|
|
16266
|
+
startTime,
|
|
16267
|
+
lastActiveTime: lastToolTs || startTime,
|
|
16268
|
+
toolCalls,
|
|
16269
|
+
blockedCalls,
|
|
16270
|
+
costUSD,
|
|
16271
|
+
hasSnapshot: false,
|
|
16272
|
+
modifiedFiles,
|
|
16273
|
+
agent: "gemini"
|
|
16274
|
+
});
|
|
16275
|
+
}
|
|
16276
|
+
}
|
|
16277
|
+
return summaries;
|
|
16278
|
+
}
|
|
16279
|
+
function buildCodexSessions(days, allAuditEntries) {
|
|
16280
|
+
const sessionsBase = path37.join(os30.homedir(), ".codex", "sessions");
|
|
16281
|
+
if (!fs34.existsSync(sessionsBase)) return [];
|
|
16282
|
+
const cutoff = days !== null ? (() => {
|
|
16283
|
+
const d = /* @__PURE__ */ new Date();
|
|
16284
|
+
d.setDate(d.getDate() - days);
|
|
16285
|
+
d.setHours(0, 0, 0, 0);
|
|
16286
|
+
return d;
|
|
16287
|
+
})() : null;
|
|
16288
|
+
const jsonlFiles = [];
|
|
16289
|
+
try {
|
|
16290
|
+
for (const year of fs34.readdirSync(sessionsBase)) {
|
|
16291
|
+
const yearPath = path37.join(sessionsBase, year);
|
|
16292
|
+
try {
|
|
16293
|
+
if (!fs34.statSync(yearPath).isDirectory()) continue;
|
|
16294
|
+
} catch {
|
|
16295
|
+
continue;
|
|
16296
|
+
}
|
|
16297
|
+
for (const month of fs34.readdirSync(yearPath)) {
|
|
16298
|
+
const monthPath = path37.join(yearPath, month);
|
|
16299
|
+
try {
|
|
16300
|
+
if (!fs34.statSync(monthPath).isDirectory()) continue;
|
|
16301
|
+
} catch {
|
|
16302
|
+
continue;
|
|
16303
|
+
}
|
|
16304
|
+
for (const day of fs34.readdirSync(monthPath)) {
|
|
16305
|
+
const dayPath = path37.join(monthPath, day);
|
|
16306
|
+
try {
|
|
16307
|
+
if (!fs34.statSync(dayPath).isDirectory()) continue;
|
|
16308
|
+
} catch {
|
|
16309
|
+
continue;
|
|
16310
|
+
}
|
|
16311
|
+
for (const file of fs34.readdirSync(dayPath)) {
|
|
16312
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path37.join(dayPath, file));
|
|
16313
|
+
}
|
|
16314
|
+
}
|
|
16315
|
+
}
|
|
16316
|
+
}
|
|
16317
|
+
} catch {
|
|
16318
|
+
return [];
|
|
16319
|
+
}
|
|
16320
|
+
const summaries = [];
|
|
16321
|
+
for (const filePath of jsonlFiles) {
|
|
16322
|
+
let lines;
|
|
16323
|
+
try {
|
|
16324
|
+
lines = fs34.readFileSync(filePath, "utf-8").split("\n");
|
|
16325
|
+
} catch {
|
|
16326
|
+
continue;
|
|
16327
|
+
}
|
|
16328
|
+
let sessionId = "";
|
|
16329
|
+
let startTime = "";
|
|
16330
|
+
let cwd = "";
|
|
16331
|
+
let firstPrompt = "";
|
|
16332
|
+
const toolCalls = [];
|
|
16333
|
+
let lastToolTs = "";
|
|
16334
|
+
let lastTotalInput = 0;
|
|
16335
|
+
let lastTotalCached = 0;
|
|
16336
|
+
let lastTotalOutput = 0;
|
|
16337
|
+
for (const line of lines) {
|
|
16338
|
+
if (!line.trim()) continue;
|
|
16339
|
+
let entry;
|
|
16340
|
+
try {
|
|
16341
|
+
entry = JSON.parse(line);
|
|
16342
|
+
} catch {
|
|
16343
|
+
continue;
|
|
16344
|
+
}
|
|
16345
|
+
const p = entry.payload ?? {};
|
|
16346
|
+
if (entry.type === "session_meta") {
|
|
16347
|
+
sessionId = String(p["id"] ?? "");
|
|
16348
|
+
startTime = String(p["timestamp"] ?? "");
|
|
16349
|
+
cwd = String(p["cwd"] ?? "");
|
|
16350
|
+
continue;
|
|
16351
|
+
}
|
|
16352
|
+
if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
|
|
16353
|
+
firstPrompt = String(p["message"] ?? "");
|
|
16354
|
+
continue;
|
|
16355
|
+
}
|
|
16356
|
+
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
16357
|
+
const info = p["info"] ?? {};
|
|
16358
|
+
const usage = info["total_token_usage"] ?? {};
|
|
16359
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
16360
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
16361
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
16362
|
+
continue;
|
|
16363
|
+
}
|
|
16364
|
+
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
16365
|
+
const tool = String(p["name"] ?? "");
|
|
16366
|
+
let input = {};
|
|
16367
|
+
try {
|
|
16368
|
+
input = JSON.parse(String(p["arguments"] ?? "{}"));
|
|
16369
|
+
} catch {
|
|
16370
|
+
}
|
|
16371
|
+
const ts = entry.timestamp ?? startTime;
|
|
16372
|
+
toolCalls.push({ tool, input, timestamp: ts });
|
|
16373
|
+
if (ts > lastToolTs) lastToolTs = ts;
|
|
16374
|
+
}
|
|
16375
|
+
}
|
|
16376
|
+
if (!sessionId || !startTime) continue;
|
|
16377
|
+
if (cutoff && new Date(startTime) < cutoff) continue;
|
|
16378
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
16379
|
+
const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
16380
|
+
const windowEnd = new Date(
|
|
16381
|
+
Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
16382
|
+
).toISOString();
|
|
16383
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
|
|
16384
|
+
summaries.push({
|
|
16385
|
+
sessionId,
|
|
16386
|
+
project: cwd,
|
|
16387
|
+
projectLabel: projectLabel(cwd),
|
|
16388
|
+
firstPrompt,
|
|
16389
|
+
startTime,
|
|
16390
|
+
lastActiveTime: lastToolTs || startTime,
|
|
16391
|
+
toolCalls,
|
|
16392
|
+
blockedCalls,
|
|
16393
|
+
costUSD,
|
|
16394
|
+
hasSnapshot: false,
|
|
16395
|
+
modifiedFiles: [],
|
|
16396
|
+
agent: "codex"
|
|
16397
|
+
});
|
|
16398
|
+
}
|
|
16399
|
+
return summaries;
|
|
16400
|
+
}
|
|
14678
16401
|
function buildSessions(days, historyPath) {
|
|
14679
16402
|
const hPath = historyPath ?? path37.join(os30.homedir(), ".claude", "history.jsonl");
|
|
14680
16403
|
let historyRaw;
|
|
@@ -14715,20 +16438,27 @@ function buildSessions(days, historyPath) {
|
|
|
14715
16438
|
// 5 min buffer
|
|
14716
16439
|
).toISOString();
|
|
14717
16440
|
const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
|
|
16441
|
+
const lastActiveTime = lastToolTs || entry.timestamp;
|
|
14718
16442
|
summaries.push({
|
|
14719
16443
|
sessionId: entry.sessionId,
|
|
14720
16444
|
project: entry.project,
|
|
14721
16445
|
projectLabel: projectLabel(entry.project),
|
|
14722
16446
|
firstPrompt: entry.display,
|
|
14723
16447
|
startTime: entry.timestamp,
|
|
16448
|
+
lastActiveTime,
|
|
14724
16449
|
toolCalls,
|
|
14725
16450
|
blockedCalls,
|
|
14726
16451
|
costUSD,
|
|
14727
16452
|
hasSnapshot,
|
|
14728
|
-
modifiedFiles
|
|
16453
|
+
modifiedFiles,
|
|
16454
|
+
agent: "claude"
|
|
14729
16455
|
});
|
|
14730
16456
|
}
|
|
14731
|
-
|
|
16457
|
+
if (!historyPath) {
|
|
16458
|
+
summaries.push(...buildGeminiSessions(days, allAuditEntries));
|
|
16459
|
+
summaries.push(...buildCodexSessions(days, allAuditEntries));
|
|
16460
|
+
}
|
|
16461
|
+
summaries.sort((a, b) => a.lastActiveTime > b.lastActiveTime ? -1 : 1);
|
|
14732
16462
|
return summaries;
|
|
14733
16463
|
}
|
|
14734
16464
|
function fmtCost3(usd) {
|
|
@@ -14839,9 +16569,9 @@ function renderSummary(summaries) {
|
|
|
14839
16569
|
const maxGroup = Math.max(...Object.values(groups));
|
|
14840
16570
|
for (const [label, count] of Object.entries(groups)) {
|
|
14841
16571
|
if (count === 0) continue;
|
|
14842
|
-
const
|
|
16572
|
+
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
14843
16573
|
console.log(
|
|
14844
|
-
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(
|
|
16574
|
+
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct)}%)`)
|
|
14845
16575
|
);
|
|
14846
16576
|
}
|
|
14847
16577
|
console.log("");
|
|
@@ -14870,21 +16600,25 @@ function renderList(summaries, totalCost) {
|
|
|
14870
16600
|
console.log("");
|
|
14871
16601
|
let lastGroup = "";
|
|
14872
16602
|
for (const s of summaries) {
|
|
14873
|
-
const
|
|
16603
|
+
const activeDate = fmtDate2(s.lastActiveTime);
|
|
16604
|
+
const group = activeDate + " " + s.projectLabel;
|
|
14874
16605
|
if (group !== lastGroup) {
|
|
14875
|
-
console.log(
|
|
14876
|
-
chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
|
|
14877
|
-
);
|
|
16606
|
+
console.log(chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(activeDate) + chalk22.dim(" " + s.projectLabel));
|
|
14878
16607
|
lastGroup = group;
|
|
14879
16608
|
}
|
|
16609
|
+
const startDate = fmtDate2(s.startTime);
|
|
16610
|
+
const dateRange = startDate !== activeDate ? chalk22.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
14880
16611
|
const timeStr = chalk22.dim(fmtTime(s.startTime));
|
|
14881
16612
|
const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
14882
16613
|
const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
|
|
14883
16614
|
const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
14884
16615
|
const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
14885
16616
|
const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
|
|
16617
|
+
const agentBadge = s.agent === "gemini" ? chalk22.blue(" [Gemini]") : s.agent === "codex" ? chalk22.magenta(" [Codex]") : chalk22.cyan(" [Claude]");
|
|
14886
16618
|
const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
|
|
14887
|
-
console.log(
|
|
16619
|
+
console.log(
|
|
16620
|
+
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
16621
|
+
);
|
|
14888
16622
|
}
|
|
14889
16623
|
console.log("");
|
|
14890
16624
|
console.log(
|
|
@@ -14899,6 +16633,10 @@ function renderDetail(s) {
|
|
|
14899
16633
|
chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
14900
16634
|
);
|
|
14901
16635
|
console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
|
|
16636
|
+
if (s.agent) {
|
|
16637
|
+
const agentLabel2 = s.agent === "gemini" ? chalk22.blue("Gemini CLI") : s.agent === "codex" ? chalk22.magenta("Codex") : chalk22.cyan("Claude Code");
|
|
16638
|
+
console.log(chalk22.bold(" Agent ") + agentLabel2);
|
|
16639
|
+
}
|
|
14902
16640
|
console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
|
|
14903
16641
|
if (s.costUSD > 0)
|
|
14904
16642
|
console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
|
|
@@ -14967,7 +16705,12 @@ function registerSessionsCommand(program2) {
|
|
|
14967
16705
|
console.log("");
|
|
14968
16706
|
process.stdout.write(chalk22.dim(" Loading\u2026"));
|
|
14969
16707
|
const summaries = buildSessions(days);
|
|
14970
|
-
process.stdout.
|
|
16708
|
+
if (process.stdout.isTTY) {
|
|
16709
|
+
process.stdout.clearLine(0);
|
|
16710
|
+
process.stdout.cursorTo(0);
|
|
16711
|
+
} else {
|
|
16712
|
+
process.stdout.write("\n");
|
|
16713
|
+
}
|
|
14971
16714
|
if (options.detail) {
|
|
14972
16715
|
const target = summaries.find(
|
|
14973
16716
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
@@ -15606,10 +17349,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15606
17349
|
program.help();
|
|
15607
17350
|
return;
|
|
15608
17351
|
}
|
|
15609
|
-
const
|
|
17352
|
+
const fullCommand2 = runArgs.join(" ");
|
|
15610
17353
|
let result = await authorizeHeadless(
|
|
15611
17354
|
"shell",
|
|
15612
|
-
{ command:
|
|
17355
|
+
{ command: fullCommand2 },
|
|
15613
17356
|
{
|
|
15614
17357
|
agent: "Terminal"
|
|
15615
17358
|
}
|
|
@@ -15617,11 +17360,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15617
17360
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
15618
17361
|
console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
15619
17362
|
const daemonReady = await autoStartDaemonAndWait();
|
|
15620
|
-
if (daemonReady) result = await authorizeHeadless("shell", { command:
|
|
17363
|
+
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
|
|
15621
17364
|
}
|
|
15622
17365
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
15623
17366
|
const approved = await confirm2({
|
|
15624
|
-
message: `\u{1F6E1}\uFE0F Node9: Allow "${
|
|
17367
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
|
|
15625
17368
|
default: false
|
|
15626
17369
|
});
|
|
15627
17370
|
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
@@ -15634,7 +17377,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15634
17377
|
process.exit(1);
|
|
15635
17378
|
}
|
|
15636
17379
|
console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
15637
|
-
await runProxy(
|
|
17380
|
+
await runProxy(fullCommand2);
|
|
15638
17381
|
} else {
|
|
15639
17382
|
program.help();
|
|
15640
17383
|
}
|