@node9/proxy 1.11.3 → 1.11.5
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 +1602 -230
- package/dist/cli.mjs +1596 -224
- package/dist/index.js +465 -75
- package/dist/index.mjs +465 -75
- package/dist/shields/builtin/bash-safe.json +18 -4
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -786,7 +786,7 @@ var init_config = __esm({
|
|
|
786
786
|
// 120-second auto-deny timeout
|
|
787
787
|
flightRecorder: true,
|
|
788
788
|
auditHashArgs: true,
|
|
789
|
-
approvers: { native: true, browser:
|
|
789
|
+
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
790
790
|
cloudSyncIntervalHours: 5
|
|
791
791
|
},
|
|
792
792
|
policy: {
|
|
@@ -893,7 +893,7 @@ var init_config = __esm({
|
|
|
893
893
|
},
|
|
894
894
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
895
895
|
{
|
|
896
|
-
name: "
|
|
896
|
+
name: "review-force-push",
|
|
897
897
|
tool: "bash",
|
|
898
898
|
conditions: [
|
|
899
899
|
{
|
|
@@ -906,8 +906,8 @@ var init_config = __esm({
|
|
|
906
906
|
}
|
|
907
907
|
],
|
|
908
908
|
conditionMode: "all",
|
|
909
|
-
verdict: "
|
|
910
|
-
reason: "Force push
|
|
909
|
+
verdict: "review",
|
|
910
|
+
reason: "Force push rewrites remote history \u2014 confirm this is intentional",
|
|
911
911
|
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."
|
|
912
912
|
},
|
|
913
913
|
{
|
|
@@ -917,14 +917,16 @@ var init_config = __esm({
|
|
|
917
917
|
{
|
|
918
918
|
field: "command",
|
|
919
919
|
op: "matches",
|
|
920
|
-
|
|
920
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
921
|
+
// "git reset --hard" as a string don't false-positive.
|
|
922
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
921
923
|
flags: "i"
|
|
922
924
|
},
|
|
923
925
|
{
|
|
924
926
|
field: "command",
|
|
925
927
|
op: "notMatches",
|
|
926
|
-
// Exclude recovery ops
|
|
927
|
-
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
|
|
928
|
+
// Exclude recovery ops and routine branch-surgery (--onto) — these are not destructive.
|
|
929
|
+
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
|
|
928
930
|
flags: "i"
|
|
929
931
|
}
|
|
930
932
|
],
|
|
@@ -1150,8 +1152,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1150
1152
|
}
|
|
1151
1153
|
if (typeof args === "string") {
|
|
1152
1154
|
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
1155
|
+
const textLower = text.toLowerCase();
|
|
1153
1156
|
for (const pattern of DLP_PATTERNS) {
|
|
1157
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1154
1160
|
if (pattern.regex.test(text)) {
|
|
1161
|
+
const matchedValue = (text.match(pattern.regex)?.[0] ?? "").toLowerCase();
|
|
1162
|
+
if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
|
|
1155
1163
|
return {
|
|
1156
1164
|
patternName: pattern.name,
|
|
1157
1165
|
fieldPath,
|
|
@@ -1176,8 +1184,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1176
1184
|
}
|
|
1177
1185
|
function scanText(text) {
|
|
1178
1186
|
const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
|
|
1187
|
+
const tLower = t.toLowerCase();
|
|
1179
1188
|
for (const pattern of DLP_PATTERNS) {
|
|
1189
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => tLower.includes(kw.toLowerCase()))) {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1180
1192
|
if (pattern.regex.test(t)) {
|
|
1193
|
+
const matchedValue = (t.match(pattern.regex)?.[0] ?? "").toLowerCase();
|
|
1194
|
+
if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
|
|
1181
1195
|
return {
|
|
1182
1196
|
patternName: pattern.name,
|
|
1183
1197
|
fieldPath: "response-text",
|
|
@@ -1188,38 +1202,315 @@ function scanText(text) {
|
|
|
1188
1202
|
}
|
|
1189
1203
|
return null;
|
|
1190
1204
|
}
|
|
1191
|
-
var import_fs4, import_path4, DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
1205
|
+
var import_fs4, import_path4, DLP_STOPWORDS, DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
1192
1206
|
var init_dlp = __esm({
|
|
1193
1207
|
"src/dlp.ts"() {
|
|
1194
1208
|
"use strict";
|
|
1195
1209
|
import_fs4 = __toESM(require("fs"));
|
|
1196
1210
|
import_path4 = __toESM(require("path"));
|
|
1211
|
+
DLP_STOPWORDS = [
|
|
1212
|
+
"example",
|
|
1213
|
+
"placeholder",
|
|
1214
|
+
"changeme",
|
|
1215
|
+
"your_key",
|
|
1216
|
+
"your_token",
|
|
1217
|
+
"your_secret",
|
|
1218
|
+
"replace_me",
|
|
1219
|
+
"insert_key",
|
|
1220
|
+
"put_your",
|
|
1221
|
+
"fake",
|
|
1222
|
+
"dummy",
|
|
1223
|
+
"sample",
|
|
1224
|
+
"xxxxxxxx",
|
|
1225
|
+
"aaaaaa",
|
|
1226
|
+
"bbbbbb",
|
|
1227
|
+
"00000000",
|
|
1228
|
+
"${",
|
|
1229
|
+
"{{",
|
|
1230
|
+
"%{",
|
|
1231
|
+
"<your",
|
|
1232
|
+
"test_key",
|
|
1233
|
+
"test_token"
|
|
1234
|
+
];
|
|
1197
1235
|
DLP_PATTERNS = [
|
|
1198
|
-
|
|
1199
|
-
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
1200
|
-
// Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
|
|
1201
|
-
// lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
|
|
1202
|
-
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
|
|
1203
|
-
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
1204
|
-
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
1236
|
+
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
1205
1237
|
{
|
|
1206
|
-
name: "
|
|
1207
|
-
regex:
|
|
1208
|
-
severity: "block"
|
|
1238
|
+
name: "AWS Access Key ID",
|
|
1239
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
1240
|
+
severity: "block",
|
|
1241
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"]
|
|
1242
|
+
},
|
|
1243
|
+
// ── GitHub ────────────────────────────────────────────────────────────────
|
|
1244
|
+
{
|
|
1245
|
+
name: "GitHub Token",
|
|
1246
|
+
regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
|
|
1247
|
+
severity: "block",
|
|
1248
|
+
keywords: ["ghp_", "gho_", "ghu_", "ghs_"]
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
name: "GitHub Fine-Grained PAT",
|
|
1252
|
+
regex: /\bgithub_pat_\w{82}\b/,
|
|
1253
|
+
severity: "block",
|
|
1254
|
+
keywords: ["github_pat_"]
|
|
1255
|
+
},
|
|
1256
|
+
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
1257
|
+
{
|
|
1258
|
+
name: "Slack Bot Token",
|
|
1259
|
+
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
1260
|
+
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
1261
|
+
severity: "block",
|
|
1262
|
+
keywords: ["xoxb-"]
|
|
1263
|
+
},
|
|
1264
|
+
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
1265
|
+
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
1266
|
+
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
1267
|
+
{
|
|
1268
|
+
name: "Anthropic API Key",
|
|
1269
|
+
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1270
|
+
severity: "block",
|
|
1271
|
+
keywords: ["sk-ant-api03"]
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
name: "Anthropic Admin Key",
|
|
1275
|
+
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1276
|
+
severity: "block",
|
|
1277
|
+
keywords: ["sk-ant-admin01"]
|
|
1278
|
+
},
|
|
1279
|
+
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
1280
|
+
{
|
|
1281
|
+
name: "OpenAI API Key",
|
|
1282
|
+
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
1283
|
+
severity: "block",
|
|
1284
|
+
keywords: ["sk-"]
|
|
1285
|
+
},
|
|
1286
|
+
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
1287
|
+
{
|
|
1288
|
+
name: "Stripe Secret Key",
|
|
1289
|
+
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
1290
|
+
severity: "block",
|
|
1291
|
+
keywords: ["sk_live_", "sk_test_"]
|
|
1292
|
+
},
|
|
1293
|
+
// ── GCP ───────────────────────────────────────────────────────────────────
|
|
1294
|
+
{
|
|
1295
|
+
name: "GCP API Key",
|
|
1296
|
+
regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
1297
|
+
severity: "block",
|
|
1298
|
+
keywords: ["aiza"]
|
|
1209
1299
|
},
|
|
1210
|
-
// GCP service account JSON (detects the type field that uniquely identifies it)
|
|
1211
1300
|
{
|
|
1212
1301
|
name: "GCP Service Account",
|
|
1213
1302
|
regex: /"type"\s*:\s*"service_account"/,
|
|
1214
|
-
severity: "block"
|
|
1303
|
+
severity: "block",
|
|
1304
|
+
keywords: ["service_account"]
|
|
1305
|
+
},
|
|
1306
|
+
// ── Azure ─────────────────────────────────────────────────────────────────
|
|
1307
|
+
// Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
|
|
1308
|
+
{
|
|
1309
|
+
name: "Azure AD Client Secret",
|
|
1310
|
+
regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
|
|
1311
|
+
severity: "block",
|
|
1312
|
+
keywords: ["q~"]
|
|
1313
|
+
},
|
|
1314
|
+
// ── Databricks ────────────────────────────────────────────────────────────
|
|
1315
|
+
{
|
|
1316
|
+
name: "Databricks API Token",
|
|
1317
|
+
regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
|
|
1318
|
+
severity: "block",
|
|
1319
|
+
keywords: ["dapi"]
|
|
1215
1320
|
},
|
|
1216
|
-
//
|
|
1321
|
+
// ── DigitalOcean ──────────────────────────────────────────────────────────
|
|
1322
|
+
{
|
|
1323
|
+
name: "DigitalOcean PAT",
|
|
1324
|
+
regex: /\bdop_v1_[a-f0-9]{64}\b/,
|
|
1325
|
+
severity: "block",
|
|
1326
|
+
keywords: ["dop_v1_"]
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: "DigitalOcean Access Token",
|
|
1330
|
+
regex: /\bdoo_v1_[a-f0-9]{64}\b/,
|
|
1331
|
+
severity: "block",
|
|
1332
|
+
keywords: ["doo_v1_"]
|
|
1333
|
+
},
|
|
1334
|
+
// ── Doppler ───────────────────────────────────────────────────────────────
|
|
1335
|
+
{
|
|
1336
|
+
name: "Doppler Token",
|
|
1337
|
+
regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
|
|
1338
|
+
severity: "block",
|
|
1339
|
+
keywords: ["dp.pt."]
|
|
1340
|
+
},
|
|
1341
|
+
// ── HashiCorp Vault ───────────────────────────────────────────────────────
|
|
1342
|
+
{
|
|
1343
|
+
name: "HashiCorp Vault Service Token",
|
|
1344
|
+
regex: /\bhvs\.[\w-]{90,120}\b/,
|
|
1345
|
+
severity: "block",
|
|
1346
|
+
keywords: ["hvs."]
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
name: "HashiCorp Vault Batch Token",
|
|
1350
|
+
regex: /\bhvb\.[\w-]{138,300}\b/,
|
|
1351
|
+
severity: "block",
|
|
1352
|
+
keywords: ["hvb."]
|
|
1353
|
+
},
|
|
1354
|
+
// ── Hugging Face ──────────────────────────────────────────────────────────
|
|
1355
|
+
{ name: "HuggingFace Token", regex: /\bhf_[A-Za-z]{34}\b/, severity: "block", keywords: ["hf_"] },
|
|
1356
|
+
// ── Postman ───────────────────────────────────────────────────────────────
|
|
1357
|
+
{
|
|
1358
|
+
name: "Postman API Token",
|
|
1359
|
+
regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
|
|
1360
|
+
severity: "block",
|
|
1361
|
+
keywords: ["pmak-"]
|
|
1362
|
+
},
|
|
1363
|
+
// ── Pulumi ────────────────────────────────────────────────────────────────
|
|
1364
|
+
{
|
|
1365
|
+
name: "Pulumi Access Token",
|
|
1366
|
+
regex: /\bpul-[a-f0-9]{40}\b/,
|
|
1367
|
+
severity: "block",
|
|
1368
|
+
keywords: ["pul-"]
|
|
1369
|
+
},
|
|
1370
|
+
// ── SendGrid ──────────────────────────────────────────────────────────────
|
|
1371
|
+
{
|
|
1372
|
+
name: "SendGrid API Key",
|
|
1373
|
+
regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
|
|
1374
|
+
severity: "block",
|
|
1375
|
+
keywords: ["sg."]
|
|
1376
|
+
},
|
|
1377
|
+
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
1378
|
+
{
|
|
1379
|
+
name: "Private Key (PEM)",
|
|
1380
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
1381
|
+
severity: "block",
|
|
1382
|
+
keywords: ["-----begin"]
|
|
1383
|
+
},
|
|
1384
|
+
// ── NPM ───────────────────────────────────────────────────────────────────
|
|
1217
1385
|
{
|
|
1218
1386
|
name: "NPM Auth Token",
|
|
1219
|
-
regex: /_authToken\s*=\s*[A-Za-z0-9_
|
|
1220
|
-
severity: "block"
|
|
1387
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
|
|
1388
|
+
severity: "block",
|
|
1389
|
+
keywords: ["_authtoken"]
|
|
1390
|
+
},
|
|
1391
|
+
// ── JWT ───────────────────────────────────────────────────────────────────
|
|
1392
|
+
// review (not block): JWTs appear legitimately in API calls; flag for human approval
|
|
1393
|
+
{
|
|
1394
|
+
name: "JWT",
|
|
1395
|
+
regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
|
|
1396
|
+
severity: "review",
|
|
1397
|
+
keywords: ["eyj"]
|
|
1398
|
+
},
|
|
1399
|
+
// ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
|
|
1400
|
+
{
|
|
1401
|
+
name: "Stripe Restricted Key",
|
|
1402
|
+
regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
|
|
1403
|
+
severity: "block",
|
|
1404
|
+
keywords: ["rk_live_", "rk_test_", "rk_prod_"]
|
|
1405
|
+
},
|
|
1406
|
+
// ── Slack (app token) ─────────────────────────────────────────────────────
|
|
1407
|
+
{
|
|
1408
|
+
name: "Slack App Token",
|
|
1409
|
+
regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
|
|
1410
|
+
severity: "block",
|
|
1411
|
+
keywords: ["xapp-"]
|
|
1412
|
+
},
|
|
1413
|
+
// ── GitLab ────────────────────────────────────────────────────────────────
|
|
1414
|
+
{ name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
|
|
1415
|
+
{
|
|
1416
|
+
name: "GitLab Deploy Token",
|
|
1417
|
+
regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
|
|
1418
|
+
severity: "block",
|
|
1419
|
+
keywords: ["gldt-"]
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
name: "GitLab CI Job Token",
|
|
1423
|
+
regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
|
|
1424
|
+
severity: "block",
|
|
1425
|
+
keywords: ["glcbt-"]
|
|
1426
|
+
},
|
|
1427
|
+
// ── npm (publish token) ───────────────────────────────────────────────────
|
|
1428
|
+
{
|
|
1429
|
+
name: "npm Access Token",
|
|
1430
|
+
regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
|
|
1431
|
+
severity: "block",
|
|
1432
|
+
keywords: ["npm_"]
|
|
1433
|
+
},
|
|
1434
|
+
// ── Shopify ───────────────────────────────────────────────────────────────
|
|
1435
|
+
{
|
|
1436
|
+
name: "Shopify Access Token",
|
|
1437
|
+
regex: /\bshpat_[a-fA-F0-9]{32}\b/,
|
|
1438
|
+
severity: "block",
|
|
1439
|
+
keywords: ["shpat_"]
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
name: "Shopify Custom Access Token",
|
|
1443
|
+
regex: /\bshpca_[a-fA-F0-9]{32}\b/,
|
|
1444
|
+
severity: "block",
|
|
1445
|
+
keywords: ["shpca_"]
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
name: "Shopify Private App Token",
|
|
1449
|
+
regex: /\bshppa_[a-fA-F0-9]{32}\b/,
|
|
1450
|
+
severity: "block",
|
|
1451
|
+
keywords: ["shppa_"]
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
name: "Shopify Shared Secret",
|
|
1455
|
+
regex: /\bshpss_[a-fA-F0-9]{32}\b/,
|
|
1456
|
+
severity: "block",
|
|
1457
|
+
keywords: ["shpss_"]
|
|
1458
|
+
},
|
|
1459
|
+
// ── Linear ────────────────────────────────────────────────────────────────
|
|
1460
|
+
{
|
|
1461
|
+
name: "Linear API Key",
|
|
1462
|
+
regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
|
|
1463
|
+
severity: "block",
|
|
1464
|
+
keywords: ["lin_api_"]
|
|
1465
|
+
},
|
|
1466
|
+
// ── PlanetScale ───────────────────────────────────────────────────────────
|
|
1467
|
+
{
|
|
1468
|
+
name: "PlanetScale API Token",
|
|
1469
|
+
regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
|
|
1470
|
+
severity: "block",
|
|
1471
|
+
keywords: ["pscale_tkn_"]
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "PlanetScale Password",
|
|
1475
|
+
regex: /\bpscale_pw_[\w.-]{32,64}\b/,
|
|
1476
|
+
severity: "block",
|
|
1477
|
+
keywords: ["pscale_pw_"]
|
|
1478
|
+
},
|
|
1479
|
+
// ── Sentry ────────────────────────────────────────────────────────────────
|
|
1480
|
+
{
|
|
1481
|
+
name: "Sentry User Token",
|
|
1482
|
+
regex: /\bsntryu_[a-f0-9]{64}\b/,
|
|
1483
|
+
severity: "block",
|
|
1484
|
+
keywords: ["sntryu_"]
|
|
1485
|
+
},
|
|
1486
|
+
// ── Grafana ───────────────────────────────────────────────────────────────
|
|
1487
|
+
{
|
|
1488
|
+
name: "Grafana Service Account Token",
|
|
1489
|
+
regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
|
|
1490
|
+
severity: "block",
|
|
1491
|
+
keywords: ["glsa_"]
|
|
1492
|
+
},
|
|
1493
|
+
// ── Heroku ────────────────────────────────────────────────────────────────
|
|
1494
|
+
{
|
|
1495
|
+
name: "Heroku API Key",
|
|
1496
|
+
regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
|
|
1497
|
+
severity: "block",
|
|
1498
|
+
keywords: ["hrku-aa"]
|
|
1499
|
+
},
|
|
1500
|
+
// ── PyPI ──────────────────────────────────────────────────────────────────
|
|
1501
|
+
{
|
|
1502
|
+
name: "PyPI Upload Token",
|
|
1503
|
+
regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
|
|
1504
|
+
severity: "block",
|
|
1505
|
+
keywords: ["pypi-"]
|
|
1221
1506
|
},
|
|
1222
|
-
|
|
1507
|
+
// ── Bearer Token ─────────────────────────────────────────────────────────
|
|
1508
|
+
{
|
|
1509
|
+
name: "Bearer Token",
|
|
1510
|
+
regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
|
|
1511
|
+
severity: "review",
|
|
1512
|
+
keywords: ["bearer"]
|
|
1513
|
+
}
|
|
1223
1514
|
];
|
|
1224
1515
|
SENSITIVE_PATH_PATTERNS = [
|
|
1225
1516
|
/[/\\]\.ssh[/\\]/i,
|
|
@@ -1786,17 +2077,97 @@ function getNestedValue(obj, path43) {
|
|
|
1786
2077
|
if (!obj || typeof obj !== "object") return null;
|
|
1787
2078
|
return path43.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1788
2079
|
}
|
|
1789
|
-
function
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2080
|
+
function normalizeCommandForPolicy(command) {
|
|
2081
|
+
try {
|
|
2082
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2083
|
+
const strips = [];
|
|
2084
|
+
syntax.Walk(f, (node) => {
|
|
2085
|
+
if (!node) return false;
|
|
2086
|
+
const n = node;
|
|
2087
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2088
|
+
const args = n.Args || [];
|
|
2089
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
2090
|
+
const argParts = args[i].Parts || [];
|
|
2091
|
+
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
2092
|
+
const flagVal = argParts[0].Value || "";
|
|
2093
|
+
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
2094
|
+
const next = args[i + 1];
|
|
2095
|
+
const nextParts = next.Parts || [];
|
|
2096
|
+
if (nextParts.length !== 1) continue;
|
|
2097
|
+
const quotedNode = nextParts[0];
|
|
2098
|
+
const nt = syntax.NodeType(quotedNode);
|
|
2099
|
+
if (nt === "SglQuoted") {
|
|
2100
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2101
|
+
} else if (nt === "DblQuoted") {
|
|
2102
|
+
const innerParts = quotedNode.Parts || [];
|
|
2103
|
+
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
2104
|
+
if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return true;
|
|
2108
|
+
});
|
|
2109
|
+
if (strips.length === 0) return command;
|
|
2110
|
+
strips.sort((a, b) => b[0] - a[0]);
|
|
2111
|
+
let result = command;
|
|
2112
|
+
for (const [start, end] of strips) {
|
|
2113
|
+
result = result.slice(0, start) + '""' + result.slice(end);
|
|
2114
|
+
}
|
|
2115
|
+
return result;
|
|
2116
|
+
} catch {
|
|
2117
|
+
return command;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function scanArgsForDynamicExec(args, startIdx) {
|
|
2121
|
+
let hasCmdSubst = false;
|
|
2122
|
+
let hasParamExp = false;
|
|
2123
|
+
let hasCurl = false;
|
|
2124
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
2125
|
+
syntax.Walk(args[i], (inner) => {
|
|
2126
|
+
if (!inner) return false;
|
|
2127
|
+
const inn = inner;
|
|
2128
|
+
const it = syntax.NodeType(inn);
|
|
2129
|
+
if (it === "CmdSubst") hasCmdSubst = true;
|
|
2130
|
+
if (it === "ParamExp") hasParamExp = true;
|
|
2131
|
+
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
2132
|
+
return true;
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
if (hasCmdSubst && hasCurl) return "block";
|
|
2136
|
+
if (hasCmdSubst || hasParamExp) return "review";
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
function detectDangerousShellExec(command) {
|
|
2140
|
+
try {
|
|
2141
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2142
|
+
let result = null;
|
|
2143
|
+
syntax.Walk(f, (node) => {
|
|
2144
|
+
if (!node || result === "block") return false;
|
|
2145
|
+
const n = node;
|
|
2146
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2147
|
+
const args = n.Args || [];
|
|
2148
|
+
if (args.length === 0) return true;
|
|
2149
|
+
const firstParts = args[0].Parts || [];
|
|
2150
|
+
if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
|
|
2151
|
+
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
2152
|
+
if (cmdName === "eval") {
|
|
2153
|
+
const v = scanArgsForDynamicExec(args, 1);
|
|
2154
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
2155
|
+
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
2156
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
2157
|
+
const flagParts = args[i].Parts || [];
|
|
2158
|
+
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
2159
|
+
continue;
|
|
2160
|
+
const v = scanArgsForDynamicExec(args, i + 1);
|
|
2161
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return true;
|
|
2166
|
+
});
|
|
2167
|
+
return result;
|
|
2168
|
+
} catch {
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
1800
2171
|
}
|
|
1801
2172
|
function shouldSnapshot(toolName, args, config) {
|
|
1802
2173
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1816,7 +2187,7 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1816
2187
|
const results = rule.conditions.map((cond) => {
|
|
1817
2188
|
const rawVal = getNestedValue(args, cond.field);
|
|
1818
2189
|
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
1819
|
-
const val = cond.field === "command" && normalized !== null ?
|
|
2190
|
+
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1820
2191
|
switch (cond.op) {
|
|
1821
2192
|
case "exists":
|
|
1822
2193
|
return val !== null && val !== "";
|
|
@@ -1864,52 +2235,35 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
1864
2235
|
const fieldName = toolInspection[matchingPattern];
|
|
1865
2236
|
return fieldName === "sql" || fieldName === "query";
|
|
1866
2237
|
}
|
|
1867
|
-
|
|
2238
|
+
function analyzeShellCommand(command) {
|
|
1868
2239
|
const actions = [];
|
|
1869
2240
|
const paths = [];
|
|
1870
2241
|
const allTokens = [];
|
|
1871
2242
|
const addToken = (token) => {
|
|
1872
2243
|
const lower = token.toLowerCase();
|
|
1873
2244
|
allTokens.push(lower);
|
|
1874
|
-
if (lower.includes("/"))
|
|
1875
|
-
|
|
1876
|
-
allTokens.push(...segments);
|
|
1877
|
-
}
|
|
1878
|
-
if (lower.startsWith("-")) {
|
|
1879
|
-
allTokens.push(lower.replace(/^-+/, ""));
|
|
1880
|
-
}
|
|
2245
|
+
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
2246
|
+
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
1881
2247
|
};
|
|
1882
2248
|
try {
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
if (!node) return;
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
for (const key in node) {
|
|
1899
|
-
if (key === "Parent") continue;
|
|
1900
|
-
const val = node[key];
|
|
1901
|
-
if (Array.isArray(val)) {
|
|
1902
|
-
val.forEach((child) => {
|
|
1903
|
-
if (child && typeof child === "object" && "type" in child) {
|
|
1904
|
-
walk(child);
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
} else if (val && typeof val === "object" && "type" in val) {
|
|
1908
|
-
walk(val);
|
|
1909
|
-
}
|
|
2249
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
2250
|
+
syntax.Walk(f, (node) => {
|
|
2251
|
+
if (!node) return false;
|
|
2252
|
+
const n = node;
|
|
2253
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2254
|
+
const wordValues = (n.Args || []).map((arg) => {
|
|
2255
|
+
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
2256
|
+
}).filter((s) => s.length > 0);
|
|
2257
|
+
if (wordValues.length > 0) {
|
|
2258
|
+
const cmd = wordValues[0].toLowerCase();
|
|
2259
|
+
if (!actions.includes(cmd)) actions.push(cmd);
|
|
2260
|
+
wordValues.forEach((w) => addToken(w));
|
|
2261
|
+
wordValues.slice(1).forEach((w) => {
|
|
2262
|
+
if (!w.startsWith("-")) paths.push(w);
|
|
2263
|
+
});
|
|
1910
2264
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
2265
|
+
return true;
|
|
2266
|
+
});
|
|
1913
2267
|
} catch {
|
|
1914
2268
|
}
|
|
1915
2269
|
if (allTokens.length === 0) {
|
|
@@ -1934,7 +2288,18 @@ async function analyzeShellCommand(command) {
|
|
|
1934
2288
|
}
|
|
1935
2289
|
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1936
2290
|
const config = getConfig();
|
|
1937
|
-
|
|
2291
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
2292
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
2293
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2294
|
+
if (dlpMatch) {
|
|
2295
|
+
return {
|
|
2296
|
+
decision: dlpMatch.severity,
|
|
2297
|
+
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2298
|
+
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (wouldBeIgnored) return { decision: "allow" };
|
|
1938
2303
|
if (config.policy.smartRules.length > 0) {
|
|
1939
2304
|
const matchedRule = config.policy.smartRules.find(
|
|
1940
2305
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
@@ -1964,13 +2329,30 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1964
2329
|
let pathTokens = [];
|
|
1965
2330
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1966
2331
|
if (shellCommand) {
|
|
1967
|
-
const analyzed =
|
|
2332
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
1968
2333
|
allTokens = analyzed.allTokens;
|
|
1969
2334
|
pathTokens = analyzed.paths;
|
|
1970
2335
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
1971
2336
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1972
2337
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1973
2338
|
}
|
|
2339
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2340
|
+
if (evalVerdict === "block") {
|
|
2341
|
+
return {
|
|
2342
|
+
decision: "block",
|
|
2343
|
+
blockedByLabel: "Node9: Eval Remote Execution",
|
|
2344
|
+
reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
|
|
2345
|
+
tier: 3
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
if (evalVerdict === "review") {
|
|
2349
|
+
return {
|
|
2350
|
+
decision: "review",
|
|
2351
|
+
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2352
|
+
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2353
|
+
tier: 3
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
1974
2356
|
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1975
2357
|
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1976
2358
|
const sinks = pipeAnalysis.sinkTargets;
|
|
@@ -2224,7 +2606,7 @@ async function explainPolicy(toolName, args) {
|
|
|
2224
2606
|
let pathTokens = [];
|
|
2225
2607
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
2226
2608
|
if (shellCommand) {
|
|
2227
|
-
const analyzed =
|
|
2609
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
2228
2610
|
allTokens = analyzed.allTokens;
|
|
2229
2611
|
pathTokens = analyzed.paths;
|
|
2230
2612
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
@@ -2257,6 +2639,25 @@ async function explainPolicy(toolName, args) {
|
|
|
2257
2639
|
outcome: "checked",
|
|
2258
2640
|
detail: "No inline execution pattern detected"
|
|
2259
2641
|
});
|
|
2642
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2643
|
+
if (evalVerdict) {
|
|
2644
|
+
const label = evalVerdict === "block" ? "Node9: Eval Remote Execution" : "Node9: Eval Dynamic Content";
|
|
2645
|
+
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";
|
|
2646
|
+
steps.push({ name: "AST eval detection", outcome: evalVerdict, detail, isFinal: true });
|
|
2647
|
+
return {
|
|
2648
|
+
tool: toolName,
|
|
2649
|
+
args,
|
|
2650
|
+
waterfall,
|
|
2651
|
+
steps,
|
|
2652
|
+
decision: evalVerdict,
|
|
2653
|
+
blockedByLabel: label
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
steps.push({
|
|
2657
|
+
name: "AST eval detection",
|
|
2658
|
+
outcome: "checked",
|
|
2659
|
+
detail: "No dangerous eval detected"
|
|
2660
|
+
});
|
|
2260
2661
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
2261
2662
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
2262
2663
|
steps.push({
|
|
@@ -2371,7 +2772,7 @@ function isIgnoredTool(toolName) {
|
|
|
2371
2772
|
const config = getConfig();
|
|
2372
2773
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
2373
2774
|
}
|
|
2374
|
-
var import_fs7, import_path8, import_os6, import_picomatch,
|
|
2775
|
+
var import_fs7, import_path8, import_os6, import_picomatch, import_mvdan_sh, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, SQL_DML_KEYWORDS;
|
|
2375
2776
|
var init_policy = __esm({
|
|
2376
2777
|
"src/policy/index.ts"() {
|
|
2377
2778
|
"use strict";
|
|
@@ -2379,7 +2780,7 @@ var init_policy = __esm({
|
|
|
2379
2780
|
import_path8 = __toESM(require("path"));
|
|
2380
2781
|
import_os6 = __toESM(require("os"));
|
|
2381
2782
|
import_picomatch = __toESM(require("picomatch"));
|
|
2382
|
-
|
|
2783
|
+
import_mvdan_sh = __toESM(require("mvdan-sh"));
|
|
2383
2784
|
init_dlp();
|
|
2384
2785
|
init_config();
|
|
2385
2786
|
init_regex();
|
|
@@ -2387,6 +2788,20 @@ var init_policy = __esm({
|
|
|
2387
2788
|
init_pipe_chain();
|
|
2388
2789
|
init_ssh_parser();
|
|
2389
2790
|
init_trusted_hosts();
|
|
2791
|
+
({ syntax } = import_mvdan_sh.default);
|
|
2792
|
+
sharedParser = syntax.NewParser();
|
|
2793
|
+
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
2794
|
+
"-m",
|
|
2795
|
+
"--message",
|
|
2796
|
+
"--body",
|
|
2797
|
+
"--title",
|
|
2798
|
+
"--description",
|
|
2799
|
+
"--comment",
|
|
2800
|
+
"--subject",
|
|
2801
|
+
"--summary"
|
|
2802
|
+
]);
|
|
2803
|
+
SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
2804
|
+
DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
2390
2805
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2391
2806
|
}
|
|
2392
2807
|
});
|
|
@@ -7925,7 +8340,7 @@ async function ensureDaemon() {
|
|
|
7925
8340
|
} catch {
|
|
7926
8341
|
}
|
|
7927
8342
|
console.log(import_chalk25.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
7928
|
-
const child = (0,
|
|
8343
|
+
const child = (0, import_child_process16.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
7929
8344
|
detached: true,
|
|
7930
8345
|
stdio: "ignore",
|
|
7931
8346
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -8328,10 +8743,10 @@ async function startTail(options = {}) {
|
|
|
8328
8743
|
try {
|
|
8329
8744
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
8330
8745
|
if (browserEnabled) {
|
|
8331
|
-
if (process.platform === "darwin") (0,
|
|
8746
|
+
if (process.platform === "darwin") (0, import_child_process16.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
8332
8747
|
else if (process.platform === "win32")
|
|
8333
|
-
(0,
|
|
8334
|
-
else (0,
|
|
8748
|
+
(0, import_child_process16.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
8749
|
+
else (0, import_child_process16.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
8335
8750
|
const intToken = getInternalToken();
|
|
8336
8751
|
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
8337
8752
|
method: "POST",
|
|
@@ -8528,7 +8943,7 @@ async function startTail(options = {}) {
|
|
|
8528
8943
|
process.exit(1);
|
|
8529
8944
|
});
|
|
8530
8945
|
}
|
|
8531
|
-
var import_http2, import_chalk25, import_fs37, import_os33, import_path40, import_readline5,
|
|
8946
|
+
var import_http2, import_chalk25, import_fs37, import_os33, import_path40, import_readline5, import_child_process16, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
8532
8947
|
var init_tail = __esm({
|
|
8533
8948
|
"src/tui/tail.ts"() {
|
|
8534
8949
|
"use strict";
|
|
@@ -8538,7 +8953,7 @@ var init_tail = __esm({
|
|
|
8538
8953
|
import_os33 = __toESM(require("os"));
|
|
8539
8954
|
import_path40 = __toESM(require("path"));
|
|
8540
8955
|
import_readline5 = __toESM(require("readline"));
|
|
8541
|
-
|
|
8956
|
+
import_child_process16 = require("child_process");
|
|
8542
8957
|
init_daemon2();
|
|
8543
8958
|
init_daemon();
|
|
8544
8959
|
init_core();
|
|
@@ -9335,6 +9750,25 @@ async function setupGemini() {
|
|
|
9335
9750
|
printDaemonTip();
|
|
9336
9751
|
}
|
|
9337
9752
|
}
|
|
9753
|
+
function claudeDesktopConfigPath(homeDir2 = import_os11.default.homedir()) {
|
|
9754
|
+
if (process.platform === "darwin") {
|
|
9755
|
+
return import_path15.default.join(
|
|
9756
|
+
homeDir2,
|
|
9757
|
+
"Library",
|
|
9758
|
+
"Application Support",
|
|
9759
|
+
"Claude",
|
|
9760
|
+
"claude_desktop_config.json"
|
|
9761
|
+
);
|
|
9762
|
+
}
|
|
9763
|
+
if (process.platform === "linux") {
|
|
9764
|
+
return import_path15.default.join(homeDir2, ".config", "Claude", "claude_desktop_config.json");
|
|
9765
|
+
}
|
|
9766
|
+
if (process.platform === "win32") {
|
|
9767
|
+
const appData = process.env.APPDATA ?? import_path15.default.join(homeDir2, "AppData", "Roaming");
|
|
9768
|
+
return import_path15.default.join(appData, "Claude", "claude_desktop_config.json");
|
|
9769
|
+
}
|
|
9770
|
+
return null;
|
|
9771
|
+
}
|
|
9338
9772
|
function detectAgents(homeDir2 = import_os11.default.homedir()) {
|
|
9339
9773
|
const exists = (p) => {
|
|
9340
9774
|
try {
|
|
@@ -9348,13 +9782,15 @@ function detectAgents(homeDir2 = import_os11.default.homedir()) {
|
|
|
9348
9782
|
return false;
|
|
9349
9783
|
}
|
|
9350
9784
|
};
|
|
9785
|
+
const desktopPath = claudeDesktopConfigPath(homeDir2);
|
|
9351
9786
|
return {
|
|
9352
9787
|
claude: exists(import_path15.default.join(homeDir2, ".claude")) || exists(import_path15.default.join(homeDir2, ".claude.json")),
|
|
9353
9788
|
gemini: exists(import_path15.default.join(homeDir2, ".gemini")),
|
|
9354
9789
|
cursor: exists(import_path15.default.join(homeDir2, ".cursor")),
|
|
9355
9790
|
codex: exists(import_path15.default.join(homeDir2, ".codex")),
|
|
9356
9791
|
windsurf: exists(import_path15.default.join(homeDir2, ".codeium", "windsurf")),
|
|
9357
|
-
vscode: exists(import_path15.default.join(homeDir2, ".vscode"))
|
|
9792
|
+
vscode: exists(import_path15.default.join(homeDir2, ".vscode")),
|
|
9793
|
+
claudeDesktop: desktopPath !== null && exists(import_path15.default.dirname(desktopPath))
|
|
9358
9794
|
};
|
|
9359
9795
|
}
|
|
9360
9796
|
async function setupCursor() {
|
|
@@ -9780,6 +10216,105 @@ function teardownVSCode() {
|
|
|
9780
10216
|
console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
|
|
9781
10217
|
}
|
|
9782
10218
|
}
|
|
10219
|
+
async function setupClaudeDesktop() {
|
|
10220
|
+
const configPath = claudeDesktopConfigPath();
|
|
10221
|
+
if (!configPath) {
|
|
10222
|
+
console.log(import_chalk.default.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
10223
|
+
return;
|
|
10224
|
+
}
|
|
10225
|
+
const config = readJson(configPath) ?? {};
|
|
10226
|
+
const servers = config.mcpServers ?? {};
|
|
10227
|
+
let anythingChanged = false;
|
|
10228
|
+
if (!hasNode9McpServer(servers)) {
|
|
10229
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
10230
|
+
config.mcpServers = servers;
|
|
10231
|
+
writeJson(configPath, config);
|
|
10232
|
+
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
10233
|
+
anythingChanged = true;
|
|
10234
|
+
}
|
|
10235
|
+
const serversToWrap = [];
|
|
10236
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
10237
|
+
if (!server.command || server.command === "node9") continue;
|
|
10238
|
+
serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
|
|
10239
|
+
}
|
|
10240
|
+
if (serversToWrap.length > 0) {
|
|
10241
|
+
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
10242
|
+
console.log(import_chalk.default.white(` ${configPath}`));
|
|
10243
|
+
for (const { name, upstream } of serversToWrap) {
|
|
10244
|
+
console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
10245
|
+
}
|
|
10246
|
+
console.log("");
|
|
10247
|
+
const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
10248
|
+
if (proceed) {
|
|
10249
|
+
for (const { name, upstream } of serversToWrap) {
|
|
10250
|
+
servers[name] = {
|
|
10251
|
+
...servers[name],
|
|
10252
|
+
command: "node9",
|
|
10253
|
+
args: ["mcp", "--upstream", upstream]
|
|
10254
|
+
};
|
|
10255
|
+
}
|
|
10256
|
+
config.mcpServers = servers;
|
|
10257
|
+
writeJson(configPath, config);
|
|
10258
|
+
console.log(import_chalk.default.green(`
|
|
10259
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
10260
|
+
anythingChanged = true;
|
|
10261
|
+
} else {
|
|
10262
|
+
console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
|
|
10263
|
+
}
|
|
10264
|
+
console.log("");
|
|
10265
|
+
}
|
|
10266
|
+
console.log(
|
|
10267
|
+
import_chalk.default.yellow(
|
|
10268
|
+
" \u26A0\uFE0F Note: Claude Desktop does not support pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode."
|
|
10269
|
+
)
|
|
10270
|
+
);
|
|
10271
|
+
console.log("");
|
|
10272
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
10273
|
+
console.log(import_chalk.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Desktop."));
|
|
10274
|
+
printDaemonTip();
|
|
10275
|
+
return;
|
|
10276
|
+
}
|
|
10277
|
+
if (anythingChanged) {
|
|
10278
|
+
console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Desktop via MCP proxy!"));
|
|
10279
|
+
console.log(import_chalk.default.gray(" Restart Claude Desktop for changes to take effect."));
|
|
10280
|
+
printDaemonTip();
|
|
10281
|
+
}
|
|
10282
|
+
}
|
|
10283
|
+
function teardownClaudeDesktop() {
|
|
10284
|
+
const configPath = claudeDesktopConfigPath();
|
|
10285
|
+
if (!configPath) {
|
|
10286
|
+
console.log(import_chalk.default.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
const config = readJson(configPath);
|
|
10290
|
+
if (!config?.mcpServers) {
|
|
10291
|
+
console.log(import_chalk.default.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
|
|
10292
|
+
return;
|
|
10293
|
+
}
|
|
10294
|
+
let changed = false;
|
|
10295
|
+
if (removeNode9McpServer(config.mcpServers)) {
|
|
10296
|
+
changed = true;
|
|
10297
|
+
console.log(import_chalk.default.green(` \u2705 Removed node9 MCP server entry from ${configPath}`));
|
|
10298
|
+
}
|
|
10299
|
+
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
10300
|
+
const args = server.args;
|
|
10301
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
10302
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
10303
|
+
config.mcpServers[name] = {
|
|
10304
|
+
...server,
|
|
10305
|
+
command: originalCmd,
|
|
10306
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
10307
|
+
};
|
|
10308
|
+
changed = true;
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
if (changed) {
|
|
10312
|
+
writeJson(configPath, config);
|
|
10313
|
+
console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
|
|
10314
|
+
} else {
|
|
10315
|
+
console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
|
|
10316
|
+
}
|
|
10317
|
+
}
|
|
9783
10318
|
function getAgentsStatus(homeDir2 = import_os11.default.homedir()) {
|
|
9784
10319
|
const detected = detectAgents(homeDir2);
|
|
9785
10320
|
const claudeWired = (() => {
|
|
@@ -9850,6 +10385,18 @@ function getAgentsStatus(homeDir2 = import_os11.default.homedir()) {
|
|
|
9850
10385
|
installed: detected.codex,
|
|
9851
10386
|
wired: codexWired,
|
|
9852
10387
|
mode: detected.codex ? "mcp" : null
|
|
10388
|
+
},
|
|
10389
|
+
{
|
|
10390
|
+
name: "claudeDesktop",
|
|
10391
|
+
label: "Claude Desktop",
|
|
10392
|
+
installed: detected.claudeDesktop,
|
|
10393
|
+
wired: (() => {
|
|
10394
|
+
const cfgPath = claudeDesktopConfigPath(homeDir2);
|
|
10395
|
+
if (!cfgPath) return false;
|
|
10396
|
+
const cfg = readJson(cfgPath);
|
|
10397
|
+
return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
|
|
10398
|
+
})(),
|
|
10399
|
+
mode: detected.claudeDesktop ? "mcp" : null
|
|
9853
10400
|
}
|
|
9854
10401
|
];
|
|
9855
10402
|
}
|
|
@@ -10966,7 +11513,6 @@ var import_path27 = __toESM(require("path"));
|
|
|
10966
11513
|
var import_os21 = __toESM(require("os"));
|
|
10967
11514
|
init_audit();
|
|
10968
11515
|
init_config();
|
|
10969
|
-
init_policy();
|
|
10970
11516
|
init_daemon();
|
|
10971
11517
|
|
|
10972
11518
|
// src/utils/cp-mv-parser.ts
|
|
@@ -11077,8 +11623,20 @@ function registerLogCommand(program2) {
|
|
|
11077
11623
|
}
|
|
11078
11624
|
const safeCwd = typeof payload.cwd === "string" && import_path27.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
11079
11625
|
const config = getConfig(safeCwd);
|
|
11080
|
-
if (
|
|
11081
|
-
|
|
11626
|
+
if ((tool === "Bash" || tool === "bash") && config.settings.enableUndo !== false) {
|
|
11627
|
+
const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
11628
|
+
if (bashCommand) {
|
|
11629
|
+
const effectiveCwd = safeCwd ?? process.cwd();
|
|
11630
|
+
const history = getSnapshotHistory();
|
|
11631
|
+
const hasPrior = history.some((e) => e.cwd === effectiveCwd);
|
|
11632
|
+
if (hasPrior) {
|
|
11633
|
+
await createShadowSnapshot(
|
|
11634
|
+
"Bash",
|
|
11635
|
+
{ command: bashCommand },
|
|
11636
|
+
config.policy.snapshot.ignorePaths
|
|
11637
|
+
);
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11082
11640
|
}
|
|
11083
11641
|
} catch (err2) {
|
|
11084
11642
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
@@ -11796,6 +12354,18 @@ function isAllow(decision) {
|
|
|
11796
12354
|
function isDlp(checkedBy) {
|
|
11797
12355
|
return !!checkedBy?.includes("dlp");
|
|
11798
12356
|
}
|
|
12357
|
+
var BLOCK_REASON_LABELS = {
|
|
12358
|
+
timeout: "Popup timeout",
|
|
12359
|
+
"smart-rule-block": "Smart rule",
|
|
12360
|
+
"observe-mode-dlp-would-block": "DLP (observe)",
|
|
12361
|
+
"persistent-deny": "Persistent deny",
|
|
12362
|
+
"local-decision": "User denied",
|
|
12363
|
+
"dlp-block": "DLP block",
|
|
12364
|
+
"loop-detected": "Loop detected"
|
|
12365
|
+
};
|
|
12366
|
+
function humanBlockReason(reason) {
|
|
12367
|
+
return BLOCK_REASON_LABELS[reason] ?? reason;
|
|
12368
|
+
}
|
|
11799
12369
|
function barStr(value, max, width) {
|
|
11800
12370
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
11801
12371
|
const filled = Math.max(1, Math.round(value / max * width));
|
|
@@ -11914,20 +12484,106 @@ function loadClaudeCost(start, end) {
|
|
|
11914
12484
|
}
|
|
11915
12485
|
return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
|
|
11916
12486
|
}
|
|
11917
|
-
function
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
12487
|
+
function loadCodexCost(start, end) {
|
|
12488
|
+
const sessionsBase = import_path30.default.join(import_os24.default.homedir(), ".codex", "sessions");
|
|
12489
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
12490
|
+
let total = 0;
|
|
12491
|
+
let toolCalls = 0;
|
|
12492
|
+
if (!import_fs28.default.existsSync(sessionsBase)) return { total, byDay, toolCalls };
|
|
12493
|
+
const jsonlFiles = [];
|
|
12494
|
+
try {
|
|
12495
|
+
for (const year of import_fs28.default.readdirSync(sessionsBase)) {
|
|
12496
|
+
const yearPath = import_path30.default.join(sessionsBase, year);
|
|
12497
|
+
try {
|
|
12498
|
+
if (!import_fs28.default.statSync(yearPath).isDirectory()) continue;
|
|
12499
|
+
} catch {
|
|
12500
|
+
continue;
|
|
12501
|
+
}
|
|
12502
|
+
for (const month of import_fs28.default.readdirSync(yearPath)) {
|
|
12503
|
+
const monthPath = import_path30.default.join(yearPath, month);
|
|
12504
|
+
try {
|
|
12505
|
+
if (!import_fs28.default.statSync(monthPath).isDirectory()) continue;
|
|
12506
|
+
} catch {
|
|
12507
|
+
continue;
|
|
12508
|
+
}
|
|
12509
|
+
for (const day of import_fs28.default.readdirSync(monthPath)) {
|
|
12510
|
+
const dayPath = import_path30.default.join(monthPath, day);
|
|
12511
|
+
try {
|
|
12512
|
+
if (!import_fs28.default.statSync(dayPath).isDirectory()) continue;
|
|
12513
|
+
} catch {
|
|
12514
|
+
continue;
|
|
12515
|
+
}
|
|
12516
|
+
for (const file of import_fs28.default.readdirSync(dayPath)) {
|
|
12517
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(import_path30.default.join(dayPath, file));
|
|
12518
|
+
}
|
|
12519
|
+
}
|
|
12520
|
+
}
|
|
12521
|
+
}
|
|
12522
|
+
} catch {
|
|
12523
|
+
return { total, byDay, toolCalls };
|
|
12524
|
+
}
|
|
12525
|
+
for (const filePath of jsonlFiles) {
|
|
12526
|
+
let lines;
|
|
12527
|
+
try {
|
|
12528
|
+
lines = import_fs28.default.readFileSync(filePath, "utf-8").split("\n");
|
|
12529
|
+
} catch {
|
|
12530
|
+
continue;
|
|
12531
|
+
}
|
|
12532
|
+
let sessionStart2 = "";
|
|
12533
|
+
let lastTotalInput = 0;
|
|
12534
|
+
let lastTotalCached = 0;
|
|
12535
|
+
let lastTotalOutput = 0;
|
|
12536
|
+
let sessionToolCalls = 0;
|
|
12537
|
+
for (const line of lines) {
|
|
12538
|
+
if (!line.trim()) continue;
|
|
12539
|
+
let entry;
|
|
12540
|
+
try {
|
|
12541
|
+
entry = JSON.parse(line);
|
|
12542
|
+
} catch {
|
|
12543
|
+
continue;
|
|
12544
|
+
}
|
|
12545
|
+
const p = entry.payload ?? {};
|
|
12546
|
+
if (entry.type === "session_meta") {
|
|
12547
|
+
sessionStart2 = String(p["timestamp"] ?? "");
|
|
12548
|
+
continue;
|
|
12549
|
+
}
|
|
12550
|
+
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
12551
|
+
const info = p["info"] ?? {};
|
|
12552
|
+
const usage = info["total_token_usage"] ?? {};
|
|
12553
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
12554
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
12555
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
12556
|
+
}
|
|
12557
|
+
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
12558
|
+
sessionToolCalls++;
|
|
12559
|
+
}
|
|
12560
|
+
}
|
|
12561
|
+
if (!sessionStart2) continue;
|
|
12562
|
+
const ts = new Date(sessionStart2);
|
|
12563
|
+
if (ts < start || ts > end) continue;
|
|
12564
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
12565
|
+
const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
12566
|
+
total += cost;
|
|
12567
|
+
toolCalls += sessionToolCalls;
|
|
12568
|
+
const dateKey = sessionStart2.slice(0, 10);
|
|
12569
|
+
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
12570
|
+
}
|
|
12571
|
+
return { total, byDay, toolCalls };
|
|
12572
|
+
}
|
|
12573
|
+
function registerReportCommand(program2) {
|
|
12574
|
+
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) => {
|
|
12575
|
+
const period = ["today", "7d", "30d", "month"].includes(
|
|
12576
|
+
options.period
|
|
12577
|
+
) ? options.period : "7d";
|
|
12578
|
+
const logPath = import_path30.default.join(import_os24.default.homedir(), ".node9", "audit.log");
|
|
12579
|
+
const allEntries = parseAuditLog(logPath);
|
|
12580
|
+
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
12581
|
+
if (unackedDlp.length > 0) {
|
|
12582
|
+
console.log("");
|
|
12583
|
+
console.log(
|
|
12584
|
+
import_chalk9.default.bgRed.white.bold(
|
|
12585
|
+
` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
|
|
12586
|
+
) + " " + import_chalk9.default.yellow("\u2192 run: node9 dlp")
|
|
11931
12587
|
);
|
|
11932
12588
|
}
|
|
11933
12589
|
if (allEntries.length === 0) {
|
|
@@ -11938,7 +12594,7 @@ function registerReportCommand(program2) {
|
|
|
11938
12594
|
}
|
|
11939
12595
|
const { start, end } = getDateRange(period);
|
|
11940
12596
|
const {
|
|
11941
|
-
total:
|
|
12597
|
+
total: claudeCostUSD,
|
|
11942
12598
|
byDay: costByDay,
|
|
11943
12599
|
byModel: costByModel,
|
|
11944
12600
|
inputTokens: costInputTokens,
|
|
@@ -11946,6 +12602,15 @@ function registerReportCommand(program2) {
|
|
|
11946
12602
|
cacheWriteTokens: costCacheWrite,
|
|
11947
12603
|
cacheReadTokens: costCacheRead
|
|
11948
12604
|
} = loadClaudeCost(start, end);
|
|
12605
|
+
const {
|
|
12606
|
+
total: codexCostUSD,
|
|
12607
|
+
byDay: codexCostByDay,
|
|
12608
|
+
toolCalls: codexToolCalls
|
|
12609
|
+
} = loadCodexCost(start, end);
|
|
12610
|
+
const costUSD = claudeCostUSD + codexCostUSD;
|
|
12611
|
+
for (const [day, c] of codexCostByDay) {
|
|
12612
|
+
costByDay.set(day, (costByDay.get(day) ?? 0) + c);
|
|
12613
|
+
}
|
|
11949
12614
|
const periodMs = end.getTime() - start.getTime();
|
|
11950
12615
|
const priorEnd = new Date(start.getTime() - 1);
|
|
11951
12616
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
@@ -12028,6 +12693,7 @@ function registerReportCommand(program2) {
|
|
|
12028
12693
|
if (e.testResult === "pass") testPasses++;
|
|
12029
12694
|
else if (e.testResult === "fail") testFails++;
|
|
12030
12695
|
}
|
|
12696
|
+
if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
|
|
12031
12697
|
const total = entries.length;
|
|
12032
12698
|
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
12033
12699
|
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
@@ -12154,7 +12820,8 @@ function registerReportCommand(program2) {
|
|
|
12154
12820
|
let rightStyled = "";
|
|
12155
12821
|
if (i < topBlocks.length) {
|
|
12156
12822
|
const [reason, count] = topBlocks[i];
|
|
12157
|
-
const
|
|
12823
|
+
const readable = humanBlockReason(reason);
|
|
12824
|
+
const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
12158
12825
|
const countStr = num(count).padStart(BLOCK_COUNT_W);
|
|
12159
12826
|
const b = colorBar(count, maxBlock, BAR);
|
|
12160
12827
|
rightStyled = import_chalk9.default.white(label.padEnd(LABEL)) + b + " " + import_chalk9.default.red(countStr);
|
|
@@ -12220,31 +12887,24 @@ function registerReportCommand(program2) {
|
|
|
12220
12887
|
console.log("");
|
|
12221
12888
|
console.log(" " + import_chalk9.default.bold("Tokens") + " " + import_chalk9.default.dim(`${num(totalTokens)} total`));
|
|
12222
12889
|
console.log(" " + import_chalk9.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12223
|
-
const
|
|
12890
|
+
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12891
|
+
const TOK_LABEL = 14;
|
|
12892
|
+
const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
|
|
12893
|
+
const nonCacheRows = [
|
|
12224
12894
|
["Input", costInputTokens, import_chalk9.default.cyan(num(costInputTokens))],
|
|
12225
12895
|
["Output", costOutputTokens, import_chalk9.default.white(num(costOutputTokens))],
|
|
12226
|
-
["Cache write", costCacheWrite, import_chalk9.default.yellow(num(costCacheWrite))]
|
|
12227
|
-
["Cache read", costCacheRead, import_chalk9.default.green(num(costCacheRead))]
|
|
12896
|
+
["Cache write", costCacheWrite, import_chalk9.default.yellow(num(costCacheWrite))]
|
|
12228
12897
|
];
|
|
12229
|
-
const
|
|
12230
|
-
costInputTokens,
|
|
12231
|
-
costOutputTokens,
|
|
12232
|
-
costCacheWrite,
|
|
12233
|
-
costCacheRead,
|
|
12234
|
-
1
|
|
12235
|
-
);
|
|
12236
|
-
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12237
|
-
const TOK_LABEL = 14;
|
|
12238
|
-
for (const [label, count, colored] of tokenRows) {
|
|
12898
|
+
for (const [label, count, colored] of nonCacheRows) {
|
|
12239
12899
|
if (count === 0) continue;
|
|
12240
|
-
const b = colorBar(count,
|
|
12900
|
+
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
12241
12901
|
console.log(" " + import_chalk9.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
12242
12902
|
}
|
|
12243
|
-
if (
|
|
12903
|
+
if (costCacheRead > 0) {
|
|
12904
|
+
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
12905
|
+
const pct = cacheHitPct > 0 ? import_chalk9.default.dim(` ${cacheHitPct}% hit rate`) : "";
|
|
12244
12906
|
console.log(
|
|
12245
|
-
" " + import_chalk9.default.
|
|
12246
|
-
`Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
|
|
12247
|
-
)
|
|
12907
|
+
" " + import_chalk9.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk9.default.green(num(costCacheRead)) + pct
|
|
12248
12908
|
);
|
|
12249
12909
|
}
|
|
12250
12910
|
}
|
|
@@ -12260,6 +12920,11 @@ function registerReportCommand(program2) {
|
|
|
12260
12920
|
console.log("");
|
|
12261
12921
|
console.log(" " + import_chalk9.default.bold("Cost") + " " + costHeaderRight);
|
|
12262
12922
|
console.log(" " + import_chalk9.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12923
|
+
if (codexCostUSD > 0)
|
|
12924
|
+
costByModel.set(
|
|
12925
|
+
"codex (openai)",
|
|
12926
|
+
(costByModel.get("codex (openai)") ?? 0) + codexCostUSD
|
|
12927
|
+
);
|
|
12263
12928
|
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
12264
12929
|
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
12265
12930
|
const MODEL_LABEL = 22;
|
|
@@ -12686,6 +13351,7 @@ function registerInitCommand(program2) {
|
|
|
12686
13351
|
else if (agent === "codex") await setupCodex();
|
|
12687
13352
|
else if (agent === "windsurf") await setupWindsurf();
|
|
12688
13353
|
else if (agent === "vscode") await setupVSCode();
|
|
13354
|
+
else if (agent === "claudeDesktop") await setupClaudeDesktop();
|
|
12689
13355
|
console.log("");
|
|
12690
13356
|
}
|
|
12691
13357
|
if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
|
|
@@ -13515,6 +14181,7 @@ var import_readline4 = __toESM(require("readline"));
|
|
|
13515
14181
|
var import_fs32 = __toESM(require("fs"));
|
|
13516
14182
|
var import_os28 = __toESM(require("os"));
|
|
13517
14183
|
var import_path35 = __toESM(require("path"));
|
|
14184
|
+
var import_child_process15 = require("child_process");
|
|
13518
14185
|
init_core();
|
|
13519
14186
|
init_daemon();
|
|
13520
14187
|
init_shields();
|
|
@@ -13594,8 +14261,31 @@ var TOOLS = [
|
|
|
13594
14261
|
},
|
|
13595
14262
|
{
|
|
13596
14263
|
name: "node9_undo_list",
|
|
13597
|
-
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.",
|
|
13598
|
-
inputSchema: {
|
|
14264
|
+
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.",
|
|
14265
|
+
inputSchema: {
|
|
14266
|
+
type: "object",
|
|
14267
|
+
properties: {
|
|
14268
|
+
cwd: {
|
|
14269
|
+
type: "string",
|
|
14270
|
+
description: "Filter to snapshots for a specific project directory. Omit to show all projects."
|
|
14271
|
+
}
|
|
14272
|
+
},
|
|
14273
|
+
required: []
|
|
14274
|
+
}
|
|
14275
|
+
},
|
|
14276
|
+
{
|
|
14277
|
+
name: "node9_undo_detail",
|
|
14278
|
+
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.",
|
|
14279
|
+
inputSchema: {
|
|
14280
|
+
type: "object",
|
|
14281
|
+
properties: {
|
|
14282
|
+
hash: {
|
|
14283
|
+
type: "string",
|
|
14284
|
+
description: "The git commit hash (full or 7-char prefix) from node9_undo_list."
|
|
14285
|
+
}
|
|
14286
|
+
},
|
|
14287
|
+
required: ["hash"]
|
|
14288
|
+
}
|
|
13599
14289
|
},
|
|
13600
14290
|
{
|
|
13601
14291
|
name: "node9_undo_revert",
|
|
@@ -13617,13 +14307,18 @@ var TOOLS = [
|
|
|
13617
14307
|
},
|
|
13618
14308
|
{
|
|
13619
14309
|
name: "node9_audit_get",
|
|
13620
|
-
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.",
|
|
14310
|
+
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.",
|
|
13621
14311
|
inputSchema: {
|
|
13622
14312
|
type: "object",
|
|
13623
14313
|
properties: {
|
|
13624
14314
|
limit: {
|
|
13625
14315
|
type: "number",
|
|
13626
14316
|
description: "Number of recent entries to return (default: 20, max: 100)."
|
|
14317
|
+
},
|
|
14318
|
+
filter: {
|
|
14319
|
+
type: "string",
|
|
14320
|
+
enum: ["all", "block", "review"],
|
|
14321
|
+
description: 'Filter by decision. Omit or use "all" to show every entry.'
|
|
13627
14322
|
}
|
|
13628
14323
|
},
|
|
13629
14324
|
required: []
|
|
@@ -13634,6 +14329,53 @@ var TOOLS = [
|
|
|
13634
14329
|
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.",
|
|
13635
14330
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
13636
14331
|
},
|
|
14332
|
+
{
|
|
14333
|
+
name: "node9_scan",
|
|
14334
|
+
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.",
|
|
14335
|
+
inputSchema: {
|
|
14336
|
+
type: "object",
|
|
14337
|
+
properties: {
|
|
14338
|
+
drill_down: {
|
|
14339
|
+
type: "boolean",
|
|
14340
|
+
description: "Show full commands and session IDs for every finding (default: false for a clean summary)."
|
|
14341
|
+
}
|
|
14342
|
+
},
|
|
14343
|
+
required: []
|
|
14344
|
+
}
|
|
14345
|
+
},
|
|
14346
|
+
{
|
|
14347
|
+
name: "node9_report",
|
|
14348
|
+
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.).",
|
|
14349
|
+
inputSchema: {
|
|
14350
|
+
type: "object",
|
|
14351
|
+
properties: {
|
|
14352
|
+
period: {
|
|
14353
|
+
type: "string",
|
|
14354
|
+
enum: ["today", "7d", "30d", "month"],
|
|
14355
|
+
description: "Time period for the report (default: 7d)."
|
|
14356
|
+
},
|
|
14357
|
+
no_tests: {
|
|
14358
|
+
type: "boolean",
|
|
14359
|
+
description: "Exclude test runner calls (npm test, vitest, pytest\u2026) from stats."
|
|
14360
|
+
}
|
|
14361
|
+
},
|
|
14362
|
+
required: []
|
|
14363
|
+
}
|
|
14364
|
+
},
|
|
14365
|
+
{
|
|
14366
|
+
name: "node9_session",
|
|
14367
|
+
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.",
|
|
14368
|
+
inputSchema: {
|
|
14369
|
+
type: "object",
|
|
14370
|
+
properties: {
|
|
14371
|
+
detail: {
|
|
14372
|
+
type: "string",
|
|
14373
|
+
description: "Session ID to show the full tool trace for. Omit to list all recent sessions."
|
|
14374
|
+
}
|
|
14375
|
+
},
|
|
14376
|
+
required: []
|
|
14377
|
+
}
|
|
14378
|
+
},
|
|
13637
14379
|
{
|
|
13638
14380
|
name: "node9_rule_add",
|
|
13639
14381
|
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.',
|
|
@@ -13826,21 +14568,40 @@ function handleApproverSet(args) {
|
|
|
13826
14568
|
}
|
|
13827
14569
|
function handleAuditGet(args) {
|
|
13828
14570
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
14571
|
+
const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
|
|
13829
14572
|
const auditPath = import_path35.default.join(import_os28.default.homedir(), ".node9", "audit.log");
|
|
13830
14573
|
if (!import_fs32.default.existsSync(auditPath)) return "No audit log found.";
|
|
13831
|
-
const
|
|
13832
|
-
const
|
|
13833
|
-
const
|
|
14574
|
+
const rawLines = import_fs32.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
14575
|
+
const parsed = [];
|
|
14576
|
+
for (const line of rawLines) {
|
|
13834
14577
|
try {
|
|
13835
14578
|
const e = JSON.parse(line);
|
|
13836
|
-
|
|
14579
|
+
const decision = String(e.decision ?? "allow");
|
|
14580
|
+
if (filter && decision !== filter) continue;
|
|
14581
|
+
const argsObj = e.args;
|
|
14582
|
+
let detail = "";
|
|
14583
|
+
if (argsObj) {
|
|
14584
|
+
const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
|
|
14585
|
+
if (typeof cmd === "string" && cmd) {
|
|
14586
|
+
detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
|
|
14587
|
+
}
|
|
14588
|
+
}
|
|
14589
|
+
const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
|
|
14590
|
+
const toolPad = String(e.tool ?? "").padEnd(20);
|
|
14591
|
+
const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
|
|
14592
|
+
parsed.push({ raw: line, decision, formatted: line2 });
|
|
13837
14593
|
} catch {
|
|
13838
|
-
|
|
14594
|
+
parsed.push({ raw: line, decision: "allow", formatted: line });
|
|
13839
14595
|
}
|
|
13840
|
-
}
|
|
13841
|
-
|
|
14596
|
+
}
|
|
14597
|
+
const recent = parsed.slice(-limit);
|
|
14598
|
+
if (recent.length === 0) {
|
|
14599
|
+
return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
|
|
14600
|
+
}
|
|
14601
|
+
const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
|
|
14602
|
+
return `${header}
|
|
13842
14603
|
|
|
13843
|
-
${
|
|
14604
|
+
${recent.map((e) => e.formatted).join("\n")}`;
|
|
13844
14605
|
}
|
|
13845
14606
|
function handlePolicyGet() {
|
|
13846
14607
|
const config = getConfig();
|
|
@@ -13893,10 +14654,43 @@ function handleRuleAdd(args) {
|
|
|
13893
14654
|
writeGlobalConfigRaw(raw);
|
|
13894
14655
|
return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
|
|
13895
14656
|
}
|
|
13896
|
-
function
|
|
13897
|
-
const
|
|
14657
|
+
function runCliCommand(subArgs) {
|
|
14658
|
+
const result = (0, import_child_process15.spawnSync)(process.execPath, [process.argv[1], ...subArgs], {
|
|
14659
|
+
encoding: "utf-8",
|
|
14660
|
+
timeout: 6e4,
|
|
14661
|
+
// Disable colors — stdout is piped (not a TTY), chalk auto-detects, but be explicit
|
|
14662
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" }
|
|
14663
|
+
});
|
|
14664
|
+
if (result.error) throw result.error;
|
|
14665
|
+
const out = (result.stdout ?? "").trimEnd();
|
|
14666
|
+
if (!out && result.stderr) throw new Error(result.stderr.trimEnd());
|
|
14667
|
+
return out || "(no output)";
|
|
14668
|
+
}
|
|
14669
|
+
function handleScanMcp(args) {
|
|
14670
|
+
const cliArgs = ["scan"];
|
|
14671
|
+
if (args.drill_down === true) cliArgs.push("--drill-down");
|
|
14672
|
+
return runCliCommand(cliArgs);
|
|
14673
|
+
}
|
|
14674
|
+
function handleReportMcp(args) {
|
|
14675
|
+
const cliArgs = ["report"];
|
|
14676
|
+
if (typeof args.period === "string") cliArgs.push("--period", args.period);
|
|
14677
|
+
if (args.no_tests === true) cliArgs.push("--no-tests");
|
|
14678
|
+
return runCliCommand(cliArgs);
|
|
14679
|
+
}
|
|
14680
|
+
function handleSessionMcp(args) {
|
|
14681
|
+
const cliArgs = ["sessions"];
|
|
14682
|
+
if (typeof args.detail === "string" && args.detail) cliArgs.push("--detail", args.detail);
|
|
14683
|
+
return runCliCommand(cliArgs);
|
|
14684
|
+
}
|
|
14685
|
+
function handleUndoList(args) {
|
|
14686
|
+
const cwdFilter = typeof args.cwd === "string" && args.cwd ? args.cwd : null;
|
|
14687
|
+
let history = getSnapshotHistory();
|
|
14688
|
+
if (cwdFilter) {
|
|
14689
|
+
history = history.filter((e) => e.cwd === cwdFilter);
|
|
14690
|
+
}
|
|
13898
14691
|
if (history.length === 0) {
|
|
13899
|
-
|
|
14692
|
+
const hint = cwdFilter ? ` for cwd: ${cwdFilter}` : "";
|
|
14693
|
+
return `No snapshots found${hint}. Node9 captures snapshots automatically before file edits.`;
|
|
13900
14694
|
}
|
|
13901
14695
|
const lines = history.slice().reverse().map((entry, i) => {
|
|
13902
14696
|
const date = new Date(entry.timestamp).toLocaleString();
|
|
@@ -13905,7 +14699,39 @@ function handleUndoList() {
|
|
|
13905
14699
|
return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
|
|
13906
14700
|
full hash: ${entry.hash}`;
|
|
13907
14701
|
});
|
|
13908
|
-
|
|
14702
|
+
const header = cwdFilter ? `${lines.length} snapshot(s) for ${cwdFilter}:` : `${lines.length} snapshot(s) across all projects:`;
|
|
14703
|
+
return `${header}
|
|
14704
|
+
|
|
14705
|
+
${lines.join("\n\n")}`;
|
|
14706
|
+
}
|
|
14707
|
+
function handleUndoDetail(args) {
|
|
14708
|
+
const hash = args.hash;
|
|
14709
|
+
if (typeof hash !== "string" || !hash) {
|
|
14710
|
+
throw new Error("hash is required");
|
|
14711
|
+
}
|
|
14712
|
+
const history = getSnapshotHistory();
|
|
14713
|
+
const entry = history.find((e) => e.hash === hash || e.hash.startsWith(hash));
|
|
14714
|
+
if (!entry) {
|
|
14715
|
+
throw new Error(`Snapshot ${hash} not found. Run node9_undo_list to see available snapshots.`);
|
|
14716
|
+
}
|
|
14717
|
+
const lines = [];
|
|
14718
|
+
lines.push(`Hash: ${entry.hash}`);
|
|
14719
|
+
lines.push(`Tool: ${entry.tool}`);
|
|
14720
|
+
lines.push(`Summary: ${entry.argsSummary || "(none)"}`);
|
|
14721
|
+
lines.push(`CWD: ${entry.cwd}`);
|
|
14722
|
+
lines.push(`Time: ${new Date(entry.timestamp).toLocaleString()}`);
|
|
14723
|
+
lines.push(`Files: ${entry.files?.length ? entry.files.join(", ") : "(none recorded)"}`);
|
|
14724
|
+
if (entry.diff) {
|
|
14725
|
+
lines.push("");
|
|
14726
|
+
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");
|
|
14727
|
+
lines.push(entry.diff);
|
|
14728
|
+
} else {
|
|
14729
|
+
lines.push("");
|
|
14730
|
+
lines.push(
|
|
14731
|
+
"No diff available (first snapshot for this project, or snapshot predates diff capture)."
|
|
14732
|
+
);
|
|
14733
|
+
}
|
|
14734
|
+
return lines.join("\n");
|
|
13909
14735
|
}
|
|
13910
14736
|
function handleUndoRevert(args) {
|
|
13911
14737
|
const hash = args.hash;
|
|
@@ -13973,7 +14799,9 @@ function runMcpServer() {
|
|
|
13973
14799
|
} else if (toolName === "node9_approver_set") {
|
|
13974
14800
|
text = handleApproverSet(toolArgs);
|
|
13975
14801
|
} else if (toolName === "node9_undo_list") {
|
|
13976
|
-
text = handleUndoList();
|
|
14802
|
+
text = handleUndoList(toolArgs);
|
|
14803
|
+
} else if (toolName === "node9_undo_detail") {
|
|
14804
|
+
text = handleUndoDetail(toolArgs);
|
|
13977
14805
|
} else if (toolName === "node9_undo_revert") {
|
|
13978
14806
|
text = handleUndoRevert(toolArgs);
|
|
13979
14807
|
} else if (toolName === "node9_audit_get") {
|
|
@@ -13982,6 +14810,12 @@ function runMcpServer() {
|
|
|
13982
14810
|
text = handlePolicyGet();
|
|
13983
14811
|
} else if (toolName === "node9_rule_add") {
|
|
13984
14812
|
text = handleRuleAdd(toolArgs);
|
|
14813
|
+
} else if (toolName === "node9_scan") {
|
|
14814
|
+
text = handleScanMcp(toolArgs);
|
|
14815
|
+
} else if (toolName === "node9_report") {
|
|
14816
|
+
text = handleReportMcp(toolArgs);
|
|
14817
|
+
} else if (toolName === "node9_session") {
|
|
14818
|
+
text = handleSessionMcp(toolArgs);
|
|
13985
14819
|
} else {
|
|
13986
14820
|
process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
|
|
13987
14821
|
return;
|
|
@@ -14220,7 +15054,8 @@ var SETUP_FN = {
|
|
|
14220
15054
|
cursor: setupCursor,
|
|
14221
15055
|
codex: setupCodex,
|
|
14222
15056
|
windsurf: setupWindsurf,
|
|
14223
|
-
vscode: setupVSCode
|
|
15057
|
+
vscode: setupVSCode,
|
|
15058
|
+
claudeDesktop: setupClaudeDesktop
|
|
14224
15059
|
};
|
|
14225
15060
|
var TEARDOWN_FN = {
|
|
14226
15061
|
claude: teardownClaude,
|
|
@@ -14228,7 +15063,8 @@ var TEARDOWN_FN = {
|
|
|
14228
15063
|
cursor: teardownCursor,
|
|
14229
15064
|
codex: teardownCodex,
|
|
14230
15065
|
windsurf: teardownWindsurf,
|
|
14231
|
-
vscode: teardownVSCode
|
|
15066
|
+
vscode: teardownVSCode,
|
|
15067
|
+
claudeDesktop: teardownClaudeDesktop
|
|
14232
15068
|
};
|
|
14233
15069
|
var AGENT_NAMES = Object.keys(SETUP_FN);
|
|
14234
15070
|
function registerAgentsCommand(program2) {
|
|
@@ -14352,11 +15188,18 @@ function preview(input, max) {
|
|
|
14352
15188
|
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
14353
15189
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
14354
15190
|
}
|
|
15191
|
+
function fullCommand(input) {
|
|
15192
|
+
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
15193
|
+
return String(cmd).replace(/\s+/g, " ").trim();
|
|
15194
|
+
}
|
|
15195
|
+
var DEFAULT_RULE_NAMES = new Set(
|
|
15196
|
+
DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
|
|
15197
|
+
);
|
|
14355
15198
|
function buildRuleSources() {
|
|
14356
15199
|
const sources = [];
|
|
14357
15200
|
for (const [shieldName, shield] of Object.entries(SHIELDS)) {
|
|
14358
15201
|
for (const rule of shield.smartRules) {
|
|
14359
|
-
sources.push({ shieldName, shieldLabel: shieldName, rule });
|
|
15202
|
+
sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
|
|
14360
15203
|
}
|
|
14361
15204
|
}
|
|
14362
15205
|
try {
|
|
@@ -14365,9 +15208,12 @@ function buildRuleSources() {
|
|
|
14365
15208
|
if (!rule.name) continue;
|
|
14366
15209
|
if (rule.name.startsWith("shield:")) continue;
|
|
14367
15210
|
const isCloud = rule.name.startsWith("cloud:");
|
|
15211
|
+
const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
|
|
15212
|
+
const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
|
|
14368
15213
|
sources.push({
|
|
14369
|
-
shieldName: isCloud ? "cloud" : "custom",
|
|
14370
|
-
shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
|
|
15214
|
+
shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
|
|
15215
|
+
shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
|
|
15216
|
+
sourceType,
|
|
14371
15217
|
rule
|
|
14372
15218
|
});
|
|
14373
15219
|
}
|
|
@@ -14413,6 +15259,7 @@ function scanClaudeHistory(startDate) {
|
|
|
14413
15259
|
for (const file of files) {
|
|
14414
15260
|
result.filesScanned++;
|
|
14415
15261
|
result.sessions++;
|
|
15262
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
14416
15263
|
let raw;
|
|
14417
15264
|
try {
|
|
14418
15265
|
raw = import_fs33.default.readFileSync(import_path36.default.join(projPath, file), "utf-8");
|
|
@@ -14471,10 +15318,12 @@ function scanClaudeHistory(startDate) {
|
|
|
14471
15318
|
toolName,
|
|
14472
15319
|
timestamp: entry.timestamp ?? "",
|
|
14473
15320
|
project: projLabel,
|
|
15321
|
+
sessionId,
|
|
14474
15322
|
agent: "claude"
|
|
14475
15323
|
});
|
|
14476
15324
|
}
|
|
14477
15325
|
}
|
|
15326
|
+
let ruleMatched = false;
|
|
14478
15327
|
for (const source of ruleSources) {
|
|
14479
15328
|
const { rule } = source;
|
|
14480
15329
|
if (rule.verdict === "allow") continue;
|
|
@@ -14491,11 +15340,45 @@ function scanClaudeHistory(startDate) {
|
|
|
14491
15340
|
input,
|
|
14492
15341
|
timestamp: entry.timestamp ?? "",
|
|
14493
15342
|
project: projLabel,
|
|
15343
|
+
sessionId,
|
|
14494
15344
|
agent: "claude"
|
|
14495
15345
|
});
|
|
14496
15346
|
}
|
|
15347
|
+
ruleMatched = true;
|
|
14497
15348
|
break;
|
|
14498
15349
|
}
|
|
15350
|
+
if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
|
|
15351
|
+
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
15352
|
+
if (shellVerdict) {
|
|
15353
|
+
const astRule = {
|
|
15354
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15355
|
+
tool: "bash",
|
|
15356
|
+
conditions: [],
|
|
15357
|
+
verdict: shellVerdict,
|
|
15358
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15359
|
+
};
|
|
15360
|
+
const inputPreview = preview(input, 120);
|
|
15361
|
+
const isDupe = result.findings.some(
|
|
15362
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15363
|
+
);
|
|
15364
|
+
if (!isDupe) {
|
|
15365
|
+
result.findings.push({
|
|
15366
|
+
source: {
|
|
15367
|
+
shieldName: "bash-safe",
|
|
15368
|
+
shieldLabel: "bash-safe (AST)",
|
|
15369
|
+
sourceType: "shield",
|
|
15370
|
+
rule: astRule
|
|
15371
|
+
},
|
|
15372
|
+
toolName,
|
|
15373
|
+
input,
|
|
15374
|
+
timestamp: entry.timestamp ?? "",
|
|
15375
|
+
project: projLabel,
|
|
15376
|
+
sessionId,
|
|
15377
|
+
agent: "claude"
|
|
15378
|
+
});
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
}
|
|
14499
15382
|
}
|
|
14500
15383
|
}
|
|
14501
15384
|
}
|
|
@@ -14545,6 +15428,7 @@ function scanGeminiHistory(startDate) {
|
|
|
14545
15428
|
}
|
|
14546
15429
|
for (const chatFile of chatFiles) {
|
|
14547
15430
|
result.filesScanned++;
|
|
15431
|
+
const sessionId = chatFile.replace(/\.json$/, "");
|
|
14548
15432
|
let raw;
|
|
14549
15433
|
try {
|
|
14550
15434
|
raw = import_fs33.default.readFileSync(import_path36.default.join(chatsDir, chatFile), "utf-8");
|
|
@@ -14598,10 +15482,12 @@ function scanGeminiHistory(startDate) {
|
|
|
14598
15482
|
toolName,
|
|
14599
15483
|
timestamp: msg.timestamp ?? "",
|
|
14600
15484
|
project: projLabel,
|
|
15485
|
+
sessionId,
|
|
14601
15486
|
agent: "gemini"
|
|
14602
15487
|
});
|
|
14603
15488
|
}
|
|
14604
15489
|
}
|
|
15490
|
+
let ruleMatched = false;
|
|
14605
15491
|
for (const source of ruleSources) {
|
|
14606
15492
|
const { rule } = source;
|
|
14607
15493
|
if (rule.verdict === "allow") continue;
|
|
@@ -14618,17 +15504,244 @@ function scanGeminiHistory(startDate) {
|
|
|
14618
15504
|
input,
|
|
14619
15505
|
timestamp: msg.timestamp ?? "",
|
|
14620
15506
|
project: projLabel,
|
|
15507
|
+
sessionId,
|
|
14621
15508
|
agent: "gemini"
|
|
14622
15509
|
});
|
|
14623
15510
|
}
|
|
15511
|
+
ruleMatched = true;
|
|
14624
15512
|
break;
|
|
14625
15513
|
}
|
|
15514
|
+
const isShellTool = ["bash", "execute_bash", "run_shell_command", "shell"].includes(
|
|
15515
|
+
toolNameLower
|
|
15516
|
+
);
|
|
15517
|
+
if (!ruleMatched && isShellTool) {
|
|
15518
|
+
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
15519
|
+
if (shellVerdict) {
|
|
15520
|
+
const astRule = {
|
|
15521
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15522
|
+
tool: "bash",
|
|
15523
|
+
conditions: [],
|
|
15524
|
+
verdict: shellVerdict,
|
|
15525
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15526
|
+
};
|
|
15527
|
+
const inputPreview = preview(input, 120);
|
|
15528
|
+
const isDupe = result.findings.some(
|
|
15529
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15530
|
+
);
|
|
15531
|
+
if (!isDupe) {
|
|
15532
|
+
result.findings.push({
|
|
15533
|
+
source: {
|
|
15534
|
+
shieldName: "bash-safe",
|
|
15535
|
+
shieldLabel: "bash-safe (AST)",
|
|
15536
|
+
sourceType: "shield",
|
|
15537
|
+
rule: astRule
|
|
15538
|
+
},
|
|
15539
|
+
toolName,
|
|
15540
|
+
input,
|
|
15541
|
+
timestamp: msg.timestamp ?? "",
|
|
15542
|
+
project: projLabel,
|
|
15543
|
+
sessionId,
|
|
15544
|
+
agent: "gemini"
|
|
15545
|
+
});
|
|
15546
|
+
}
|
|
15547
|
+
}
|
|
15548
|
+
}
|
|
14626
15549
|
}
|
|
14627
15550
|
}
|
|
14628
15551
|
}
|
|
14629
15552
|
}
|
|
14630
15553
|
return result;
|
|
14631
15554
|
}
|
|
15555
|
+
function scanCodexHistory(startDate) {
|
|
15556
|
+
const sessionsBase = import_path36.default.join(import_os29.default.homedir(), ".codex", "sessions");
|
|
15557
|
+
const result = {
|
|
15558
|
+
filesScanned: 0,
|
|
15559
|
+
sessions: 0,
|
|
15560
|
+
totalToolCalls: 0,
|
|
15561
|
+
bashCalls: 0,
|
|
15562
|
+
findings: [],
|
|
15563
|
+
dlpFindings: [],
|
|
15564
|
+
totalCostUSD: 0,
|
|
15565
|
+
firstDate: null,
|
|
15566
|
+
lastDate: null
|
|
15567
|
+
};
|
|
15568
|
+
if (!import_fs33.default.existsSync(sessionsBase)) return result;
|
|
15569
|
+
const jsonlFiles = [];
|
|
15570
|
+
try {
|
|
15571
|
+
for (const year of import_fs33.default.readdirSync(sessionsBase)) {
|
|
15572
|
+
const yearPath = import_path36.default.join(sessionsBase, year);
|
|
15573
|
+
try {
|
|
15574
|
+
if (!import_fs33.default.statSync(yearPath).isDirectory()) continue;
|
|
15575
|
+
} catch {
|
|
15576
|
+
continue;
|
|
15577
|
+
}
|
|
15578
|
+
for (const month of import_fs33.default.readdirSync(yearPath)) {
|
|
15579
|
+
const monthPath = import_path36.default.join(yearPath, month);
|
|
15580
|
+
try {
|
|
15581
|
+
if (!import_fs33.default.statSync(monthPath).isDirectory()) continue;
|
|
15582
|
+
} catch {
|
|
15583
|
+
continue;
|
|
15584
|
+
}
|
|
15585
|
+
for (const day of import_fs33.default.readdirSync(monthPath)) {
|
|
15586
|
+
const dayPath = import_path36.default.join(monthPath, day);
|
|
15587
|
+
try {
|
|
15588
|
+
if (!import_fs33.default.statSync(dayPath).isDirectory()) continue;
|
|
15589
|
+
} catch {
|
|
15590
|
+
continue;
|
|
15591
|
+
}
|
|
15592
|
+
for (const file of import_fs33.default.readdirSync(dayPath)) {
|
|
15593
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(import_path36.default.join(dayPath, file));
|
|
15594
|
+
}
|
|
15595
|
+
}
|
|
15596
|
+
}
|
|
15597
|
+
}
|
|
15598
|
+
} catch {
|
|
15599
|
+
return result;
|
|
15600
|
+
}
|
|
15601
|
+
const ruleSources = buildRuleSources();
|
|
15602
|
+
for (const filePath of jsonlFiles) {
|
|
15603
|
+
result.filesScanned++;
|
|
15604
|
+
let lines;
|
|
15605
|
+
try {
|
|
15606
|
+
lines = import_fs33.default.readFileSync(filePath, "utf-8").split("\n");
|
|
15607
|
+
} catch {
|
|
15608
|
+
continue;
|
|
15609
|
+
}
|
|
15610
|
+
let sessionId = "";
|
|
15611
|
+
let startTime = "";
|
|
15612
|
+
let projLabel = "";
|
|
15613
|
+
result.sessions++;
|
|
15614
|
+
let lastTotalInput = 0;
|
|
15615
|
+
let lastTotalCached = 0;
|
|
15616
|
+
let lastTotalOutput = 0;
|
|
15617
|
+
for (const line of lines) {
|
|
15618
|
+
if (!line.trim()) continue;
|
|
15619
|
+
let entry;
|
|
15620
|
+
try {
|
|
15621
|
+
entry = JSON.parse(line);
|
|
15622
|
+
} catch {
|
|
15623
|
+
continue;
|
|
15624
|
+
}
|
|
15625
|
+
const payload = entry.payload ?? {};
|
|
15626
|
+
if (entry.type === "session_meta") {
|
|
15627
|
+
sessionId = String(payload["id"] ?? filePath);
|
|
15628
|
+
startTime = String(payload["timestamp"] ?? "");
|
|
15629
|
+
const cwd = String(payload["cwd"] ?? "");
|
|
15630
|
+
projLabel = cwd.replace(import_os29.default.homedir(), "~").slice(0, 40);
|
|
15631
|
+
continue;
|
|
15632
|
+
}
|
|
15633
|
+
if (entry.type === "event_msg" && payload["type"] === "token_count") {
|
|
15634
|
+
const info = payload["info"];
|
|
15635
|
+
const usage = info?.["total_token_usage"] ?? {};
|
|
15636
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
15637
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
15638
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
15639
|
+
continue;
|
|
15640
|
+
}
|
|
15641
|
+
if (entry.type !== "response_item") continue;
|
|
15642
|
+
if (payload["type"] !== "function_call") continue;
|
|
15643
|
+
const ts = startTime;
|
|
15644
|
+
if (startDate && ts && new Date(ts) < startDate) continue;
|
|
15645
|
+
if (ts) {
|
|
15646
|
+
if (!result.firstDate || ts < result.firstDate) result.firstDate = ts;
|
|
15647
|
+
if (!result.lastDate || ts > result.lastDate) result.lastDate = ts;
|
|
15648
|
+
}
|
|
15649
|
+
result.totalToolCalls++;
|
|
15650
|
+
const toolName = String(payload["name"] ?? "");
|
|
15651
|
+
const toolNameLower = toolName.toLowerCase();
|
|
15652
|
+
let input = {};
|
|
15653
|
+
try {
|
|
15654
|
+
input = JSON.parse(String(payload["arguments"] ?? "{}"));
|
|
15655
|
+
} catch {
|
|
15656
|
+
}
|
|
15657
|
+
if ("cmd" in input && !("command" in input)) {
|
|
15658
|
+
input = { ...input, command: input["cmd"] };
|
|
15659
|
+
}
|
|
15660
|
+
if (toolNameLower === "exec_command" || toolNameLower === "shell") {
|
|
15661
|
+
result.bashCalls++;
|
|
15662
|
+
}
|
|
15663
|
+
const rawCmd = String(input["command"] ?? "").trimStart();
|
|
15664
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
|
|
15665
|
+
const dlpMatch = scanArgs(input);
|
|
15666
|
+
if (dlpMatch) {
|
|
15667
|
+
const isDupe = result.dlpFindings.some(
|
|
15668
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
15669
|
+
);
|
|
15670
|
+
if (!isDupe) {
|
|
15671
|
+
result.dlpFindings.push({
|
|
15672
|
+
patternName: dlpMatch.patternName,
|
|
15673
|
+
redactedSample: dlpMatch.redactedSample,
|
|
15674
|
+
toolName,
|
|
15675
|
+
timestamp: ts,
|
|
15676
|
+
project: projLabel,
|
|
15677
|
+
sessionId,
|
|
15678
|
+
agent: "codex"
|
|
15679
|
+
});
|
|
15680
|
+
}
|
|
15681
|
+
}
|
|
15682
|
+
let ruleMatched = false;
|
|
15683
|
+
for (const source of ruleSources) {
|
|
15684
|
+
const { rule } = source;
|
|
15685
|
+
if (rule.verdict === "allow") continue;
|
|
15686
|
+
if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
|
|
15687
|
+
continue;
|
|
15688
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
15689
|
+
const inputPreview = preview(input, 120);
|
|
15690
|
+
const isDupe = result.findings.some(
|
|
15691
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15692
|
+
);
|
|
15693
|
+
if (!isDupe) {
|
|
15694
|
+
result.findings.push({
|
|
15695
|
+
source,
|
|
15696
|
+
toolName,
|
|
15697
|
+
input,
|
|
15698
|
+
timestamp: ts,
|
|
15699
|
+
project: projLabel,
|
|
15700
|
+
sessionId,
|
|
15701
|
+
agent: "codex"
|
|
15702
|
+
});
|
|
15703
|
+
}
|
|
15704
|
+
ruleMatched = true;
|
|
15705
|
+
break;
|
|
15706
|
+
}
|
|
15707
|
+
if (!ruleMatched && (toolNameLower === "exec_command" || toolNameLower === "shell")) {
|
|
15708
|
+
const shellVerdict = detectDangerousShellExec(String(input["command"] ?? ""));
|
|
15709
|
+
if (shellVerdict) {
|
|
15710
|
+
const astRule = {
|
|
15711
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
15712
|
+
tool: "bash",
|
|
15713
|
+
conditions: [],
|
|
15714
|
+
verdict: shellVerdict,
|
|
15715
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
15716
|
+
};
|
|
15717
|
+
const inputPreview = preview(input, 120);
|
|
15718
|
+
const isDupe = result.findings.some(
|
|
15719
|
+
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15720
|
+
);
|
|
15721
|
+
if (!isDupe) {
|
|
15722
|
+
result.findings.push({
|
|
15723
|
+
source: {
|
|
15724
|
+
shieldName: "bash-safe",
|
|
15725
|
+
shieldLabel: "bash-safe (AST)",
|
|
15726
|
+
sourceType: "shield",
|
|
15727
|
+
rule: astRule
|
|
15728
|
+
},
|
|
15729
|
+
toolName,
|
|
15730
|
+
input,
|
|
15731
|
+
timestamp: ts,
|
|
15732
|
+
project: projLabel,
|
|
15733
|
+
sessionId,
|
|
15734
|
+
agent: "codex"
|
|
15735
|
+
});
|
|
15736
|
+
}
|
|
15737
|
+
}
|
|
15738
|
+
}
|
|
15739
|
+
}
|
|
15740
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
15741
|
+
result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
15742
|
+
}
|
|
15743
|
+
return result;
|
|
15744
|
+
}
|
|
14632
15745
|
function mergeScans(a, b) {
|
|
14633
15746
|
const dates = [a.firstDate, b.firstDate].filter(Boolean);
|
|
14634
15747
|
const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
|
|
@@ -14644,22 +15757,67 @@ function mergeScans(a, b) {
|
|
|
14644
15757
|
lastDate: lastDates.length ? lastDates.sort().at(-1) : null
|
|
14645
15758
|
};
|
|
14646
15759
|
}
|
|
15760
|
+
function verdictIcon(verdict) {
|
|
15761
|
+
return verdict === "block" ? "\u{1F6D1}" : "\u{1F441} ";
|
|
15762
|
+
}
|
|
15763
|
+
function printFindingRow(f, drillDown, showSessionId, previewWidth) {
|
|
15764
|
+
const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
|
|
15765
|
+
const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
15766
|
+
const agentBadge = f.agent === "gemini" ? import_chalk21.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk21.default.magenta("[Codex] ") : import_chalk21.default.cyan("[Claude] ");
|
|
15767
|
+
const cmd = drillDown ? import_chalk21.default.gray(fullCommand(f.input)) : import_chalk21.default.gray(preview(f.input, previewWidth));
|
|
15768
|
+
const sessionSuffix = showSessionId && f.sessionId ? import_chalk21.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
15769
|
+
console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
|
|
15770
|
+
}
|
|
15771
|
+
function printRuleGroup(ruleFindings, topN, drillDown, previewWidth) {
|
|
15772
|
+
const rule = ruleFindings[0].source.rule;
|
|
15773
|
+
const ruleCount = ruleFindings.length;
|
|
15774
|
+
const countBadge = ruleCount > 1 ? import_chalk21.default.white(` \xD7${ruleCount}`) : "";
|
|
15775
|
+
const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
15776
|
+
const icon = verdictIcon(rule.verdict ?? "review");
|
|
15777
|
+
console.log(
|
|
15778
|
+
" " + icon + " " + import_chalk21.default.white(shortName) + countBadge + (rule.reason ? import_chalk21.default.dim(` \u2014 ${rule.reason}`) : "")
|
|
15779
|
+
);
|
|
15780
|
+
const shown = drillDown ? ruleFindings : ruleFindings.slice(0, topN);
|
|
15781
|
+
for (const f of shown) {
|
|
15782
|
+
printFindingRow(f, drillDown, drillDown, previewWidth);
|
|
15783
|
+
}
|
|
15784
|
+
if (!drillDown && ruleFindings.length > topN) {
|
|
15785
|
+
console.log(
|
|
15786
|
+
import_chalk21.default.dim(` \u2026 and ${ruleFindings.length - topN} more (--drill-down for full list)`)
|
|
15787
|
+
);
|
|
15788
|
+
}
|
|
15789
|
+
}
|
|
14647
15790
|
function registerScanCommand(program2) {
|
|
14648
|
-
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
|
|
14649
|
-
const
|
|
15791
|
+
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) => {
|
|
15792
|
+
const drillDown = options.drillDown ?? false;
|
|
15793
|
+
const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
|
|
15794
|
+
const previewWidth = 70;
|
|
14650
15795
|
const startDate = options.all ? null : (() => {
|
|
14651
15796
|
const d = /* @__PURE__ */ new Date();
|
|
14652
15797
|
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
14653
15798
|
d.setHours(0, 0, 0, 0);
|
|
14654
15799
|
return d;
|
|
14655
15800
|
})();
|
|
15801
|
+
const isInstalled = import_fs33.default.existsSync(import_path36.default.join(import_os29.default.homedir(), ".node9", "audit.log"));
|
|
14656
15802
|
console.log("");
|
|
14657
|
-
|
|
15803
|
+
if (!isInstalled) {
|
|
15804
|
+
console.log(
|
|
15805
|
+
import_chalk21.default.bold("\u{1F6E1} node9") + import_chalk21.default.dim(" \u2014 security layer for AI coding agents")
|
|
15806
|
+
);
|
|
15807
|
+
console.log(
|
|
15808
|
+
import_chalk21.default.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
|
|
15809
|
+
);
|
|
15810
|
+
console.log("");
|
|
15811
|
+
}
|
|
15812
|
+
console.log(
|
|
15813
|
+
import_chalk21.default.cyan.bold("\u{1F50D} Scanning your AI history") + import_chalk21.default.dim(" \u2014 what would node9 have caught?")
|
|
15814
|
+
);
|
|
14658
15815
|
console.log("");
|
|
14659
15816
|
process.stdout.write(import_chalk21.default.dim(" Scanning\u2026"));
|
|
14660
15817
|
const claudeScan = scanClaudeHistory(startDate);
|
|
14661
15818
|
const geminiScan = scanGeminiHistory(startDate);
|
|
14662
|
-
const
|
|
15819
|
+
const codexScan = scanCodexHistory(startDate);
|
|
15820
|
+
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
14663
15821
|
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
14664
15822
|
if (scan.filesScanned === 0) {
|
|
14665
15823
|
console.log(import_chalk21.default.yellow(" No session history found."));
|
|
@@ -14672,95 +15830,151 @@ function registerScanCommand(program2) {
|
|
|
14672
15830
|
}
|
|
14673
15831
|
const rangeLabel = options.all ? import_chalk21.default.dim("all time") : import_chalk21.default.dim(`last ${options.days ?? 90} days`);
|
|
14674
15832
|
const dateRange = scan.firstDate && scan.lastDate ? import_chalk21.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
14675
|
-
const
|
|
15833
|
+
const breakdownParts = [];
|
|
15834
|
+
if (claudeScan.sessions > 0)
|
|
15835
|
+
breakdownParts.push(import_chalk21.default.cyan(String(claudeScan.sessions)) + import_chalk21.default.dim(" Claude"));
|
|
15836
|
+
if (geminiScan.sessions > 0)
|
|
15837
|
+
breakdownParts.push(import_chalk21.default.blue(String(geminiScan.sessions)) + import_chalk21.default.dim(" Gemini"));
|
|
15838
|
+
if (codexScan.sessions > 0)
|
|
15839
|
+
breakdownParts.push(import_chalk21.default.magenta(String(codexScan.sessions)) + import_chalk21.default.dim(" Codex"));
|
|
15840
|
+
const sessionBreakdown = breakdownParts.length > 1 ? import_chalk21.default.dim("(") + breakdownParts.join(import_chalk21.default.dim(" \xB7 ")) + import_chalk21.default.dim(")") : "";
|
|
14676
15841
|
console.log(
|
|
14677
15842
|
" " + import_chalk21.default.white(num2(scan.sessions)) + import_chalk21.default.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + import_chalk21.default.white(num2(scan.totalToolCalls)) + import_chalk21.default.dim(" tool calls ") + import_chalk21.default.white(num2(scan.bashCalls)) + import_chalk21.default.dim(" bash commands ") + rangeLabel + dateRange
|
|
14678
15843
|
);
|
|
14679
15844
|
console.log("");
|
|
14680
|
-
const byShield = /* @__PURE__ */ new Map();
|
|
14681
|
-
for (const f of scan.findings) {
|
|
14682
|
-
const key = f.source.shieldName;
|
|
14683
|
-
const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
|
|
14684
|
-
entry.findings.push(f);
|
|
14685
|
-
byShield.set(key, entry);
|
|
14686
|
-
}
|
|
14687
15845
|
const totalFindings = scan.findings.length;
|
|
15846
|
+
const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
|
|
15847
|
+
const reviewCount = totalFindings - blockedCount;
|
|
14688
15848
|
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
14689
|
-
console.log(import_chalk21.default.green(" \u2705 No
|
|
14690
|
-
console.log(
|
|
15849
|
+
console.log(import_chalk21.default.green(" \u2705 No risky operations found in your history."));
|
|
15850
|
+
console.log(
|
|
15851
|
+
import_chalk21.default.dim(" node9 is still worth running \u2014 it monitors every tool call in real time.\n")
|
|
15852
|
+
);
|
|
14691
15853
|
} else {
|
|
14692
|
-
|
|
15854
|
+
const totalRisky = totalFindings + scan.dlpFindings.length;
|
|
15855
|
+
const heroLine = isInstalled ? import_chalk21.default.bold(
|
|
15856
|
+
` Found ${import_chalk21.default.yellow(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} in your history`
|
|
15857
|
+
) : import_chalk21.default.bold(
|
|
15858
|
+
` ${import_chalk21.default.red.bold(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} found \u2014 none were blocked`
|
|
15859
|
+
);
|
|
15860
|
+
console.log(heroLine);
|
|
15861
|
+
console.log("");
|
|
15862
|
+
if (blockedCount > 0) {
|
|
14693
15863
|
console.log(
|
|
14694
|
-
"
|
|
14695
|
-
`${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
|
|
14696
|
-
)
|
|
15864
|
+
" " + import_chalk21.default.red("\u{1F6D1} Would have blocked") + " " + import_chalk21.default.red.bold(String(blockedCount).padStart(5)) + import_chalk21.default.dim(" operations stopped before execution")
|
|
14697
15865
|
);
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
15866
|
+
}
|
|
15867
|
+
if (reviewCount > 0) {
|
|
15868
|
+
console.log(
|
|
15869
|
+
" " + import_chalk21.default.yellow("\u{1F441} Would have flagged") + " " + import_chalk21.default.yellow.bold(String(reviewCount).padStart(5)) + import_chalk21.default.dim(" sent to you for approval")
|
|
14701
15870
|
);
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
15871
|
+
}
|
|
15872
|
+
if (scan.dlpFindings.length > 0) {
|
|
15873
|
+
console.log(
|
|
15874
|
+
" " + import_chalk21.default.red("\u{1F511} Credential leak") + " " + import_chalk21.default.red.bold(String(scan.dlpFindings.length).padStart(5)) + import_chalk21.default.dim(" secret detected in tool call")
|
|
15875
|
+
);
|
|
15876
|
+
}
|
|
15877
|
+
console.log("");
|
|
15878
|
+
const sections = [];
|
|
15879
|
+
const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
|
|
15880
|
+
if (defaultFindings.length > 0) {
|
|
15881
|
+
sections.push({
|
|
15882
|
+
label: "Default Rules",
|
|
15883
|
+
subtitle: "built-in, always on",
|
|
15884
|
+
findings: defaultFindings
|
|
15885
|
+
});
|
|
15886
|
+
}
|
|
15887
|
+
const byShield = /* @__PURE__ */ new Map();
|
|
15888
|
+
for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
|
|
15889
|
+
const arr = byShield.get(f.source.shieldName) ?? [];
|
|
15890
|
+
arr.push(f);
|
|
15891
|
+
byShield.set(f.source.shieldName, arr);
|
|
15892
|
+
}
|
|
15893
|
+
const shieldsWithFindings = [...byShield.entries()].sort(
|
|
15894
|
+
(a, b) => b[1].length - a[1].length
|
|
15895
|
+
);
|
|
15896
|
+
for (const [shieldName, findings] of shieldsWithFindings) {
|
|
15897
|
+
const description = SHIELDS[shieldName]?.description ?? "";
|
|
15898
|
+
sections.push({
|
|
15899
|
+
label: shieldName,
|
|
15900
|
+
subtitle: description,
|
|
15901
|
+
shieldKey: shieldName,
|
|
15902
|
+
findings
|
|
15903
|
+
});
|
|
15904
|
+
}
|
|
15905
|
+
const userFindings = scan.findings.filter(
|
|
15906
|
+
(f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
|
|
15907
|
+
);
|
|
15908
|
+
if (userFindings.length > 0) {
|
|
15909
|
+
sections.push({
|
|
15910
|
+
label: "Your Rules",
|
|
15911
|
+
subtitle: "added in node9.config.json",
|
|
15912
|
+
findings: userFindings
|
|
15913
|
+
});
|
|
15914
|
+
}
|
|
15915
|
+
for (const section of sections) {
|
|
15916
|
+
const sectionBlocked = section.findings.filter(
|
|
15917
|
+
(f) => f.source.rule.verdict === "block"
|
|
15918
|
+
).length;
|
|
15919
|
+
const sectionReview = section.findings.length - sectionBlocked;
|
|
15920
|
+
const countParts = [];
|
|
15921
|
+
if (sectionBlocked > 0) countParts.push(import_chalk21.default.red(`${sectionBlocked} blocked`));
|
|
15922
|
+
if (sectionReview > 0) countParts.push(import_chalk21.default.yellow(`${sectionReview} review`));
|
|
15923
|
+
const countStr = countParts.join(import_chalk21.default.dim(" \xB7 "));
|
|
15924
|
+
const enableHint = section.shieldKey ? import_chalk21.default.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
15925
|
+
console.log(" " + import_chalk21.default.dim("\u2500".repeat(70)));
|
|
15926
|
+
console.log(
|
|
15927
|
+
" " + import_chalk21.default.bold(section.label) + (section.subtitle ? import_chalk21.default.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
|
|
15928
|
+
);
|
|
15929
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
15930
|
+
for (const f of section.findings) {
|
|
15931
|
+
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
15932
|
+
const arr = byRule.get(ruleKey) ?? [];
|
|
15933
|
+
arr.push(f);
|
|
15934
|
+
byRule.set(ruleKey, arr);
|
|
15935
|
+
}
|
|
15936
|
+
const sortedRules = [...byRule.entries()].sort((a, b) => {
|
|
15937
|
+
const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
15938
|
+
const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
15939
|
+
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
15940
|
+
return b[1].length - a[1].length;
|
|
15941
|
+
});
|
|
15942
|
+
for (const [, ruleFindings] of sortedRules) {
|
|
15943
|
+
printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
|
|
14742
15944
|
}
|
|
15945
|
+
console.log("");
|
|
15946
|
+
}
|
|
15947
|
+
const emptyShields = Object.keys(SHIELDS).filter((n) => !byShield.has(n)).sort();
|
|
15948
|
+
if (emptyShields.length > 0) {
|
|
15949
|
+
console.log(" " + import_chalk21.default.dim("\u2500".repeat(70)));
|
|
15950
|
+
console.log(
|
|
15951
|
+
" " + import_chalk21.default.bold("Shields") + import_chalk21.default.dim(" \xB7 no findings in your history") + " " + import_chalk21.default.green("\u2713")
|
|
15952
|
+
);
|
|
15953
|
+
console.log(" " + import_chalk21.default.dim(emptyShields.join(" \xB7 ")));
|
|
15954
|
+
console.log(" " + import_chalk21.default.dim("\u2192 node9 shield enable <name> to activate any shield"));
|
|
15955
|
+
console.log("");
|
|
14743
15956
|
}
|
|
14744
15957
|
if (scan.dlpFindings.length > 0) {
|
|
14745
15958
|
console.log(" " + import_chalk21.default.dim("\u2500".repeat(70)));
|
|
14746
15959
|
console.log(
|
|
14747
|
-
" " + import_chalk21.default.red.bold("
|
|
15960
|
+
" " + import_chalk21.default.red.bold("\u{1F511} Credential Leaks") + import_chalk21.default.dim(" \xB7 ") + import_chalk21.default.red(
|
|
14748
15961
|
`${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
|
|
14749
15962
|
)
|
|
14750
15963
|
);
|
|
14751
|
-
const shownDlp = scan.dlpFindings.slice(0, topN);
|
|
15964
|
+
const shownDlp = drillDown ? scan.dlpFindings : scan.dlpFindings.slice(0, topN);
|
|
14752
15965
|
for (const f of shownDlp) {
|
|
14753
15966
|
const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
|
|
14754
15967
|
const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
14755
|
-
const agentBadge = f.agent === "gemini" ? import_chalk21.default.blue("[Gemini] ") : import_chalk21.default.cyan("[Claude] ");
|
|
15968
|
+
const agentBadge = f.agent === "gemini" ? import_chalk21.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk21.default.magenta("[Codex] ") : import_chalk21.default.cyan("[Claude] ");
|
|
15969
|
+
const sessionSuffix = f.sessionId ? import_chalk21.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
14756
15970
|
console.log(
|
|
14757
|
-
` ${ts}${proj}${agentBadge}` + import_chalk21.default.yellow(f.patternName) + import_chalk21.default.dim(" ") + import_chalk21.default.gray(f.redactedSample)
|
|
15971
|
+
` ${ts}${proj}${agentBadge}` + import_chalk21.default.yellow(f.patternName) + import_chalk21.default.dim(" ") + import_chalk21.default.gray(f.redactedSample) + sessionSuffix
|
|
14758
15972
|
);
|
|
14759
15973
|
}
|
|
14760
|
-
if (scan.dlpFindings.length > topN) {
|
|
15974
|
+
if (!drillDown && scan.dlpFindings.length > topN) {
|
|
14761
15975
|
console.log(
|
|
14762
15976
|
import_chalk21.default.dim(
|
|
14763
|
-
` \u2026 and ${scan.dlpFindings.length - topN} more
|
|
15977
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
|
|
14764
15978
|
)
|
|
14765
15979
|
);
|
|
14766
15980
|
}
|
|
@@ -14773,17 +15987,41 @@ function registerScanCommand(program2) {
|
|
|
14773
15987
|
);
|
|
14774
15988
|
console.log("");
|
|
14775
15989
|
}
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
console.log(import_chalk21.default.green(" \u2705 node9 is active \u2014 future sessions are protected."));
|
|
15990
|
+
if (isInstalled) {
|
|
15991
|
+
console.log(import_chalk21.default.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
|
|
14779
15992
|
console.log(
|
|
14780
|
-
import_chalk21.default.dim(" Run ") + import_chalk21.default.cyan("node9 report") + import_chalk21.default.dim(" to see live stats.")
|
|
15993
|
+
import_chalk21.default.dim(" Run ") + import_chalk21.default.cyan("node9 report") + import_chalk21.default.dim(" to see live protection stats.")
|
|
14781
15994
|
);
|
|
15995
|
+
if (drillDown) {
|
|
15996
|
+
console.log(
|
|
15997
|
+
import_chalk21.default.dim(" Run ") + import_chalk21.default.cyan("node9 sessions --detail <session-id>") + import_chalk21.default.dim(" to see the full conversation for any session above.")
|
|
15998
|
+
);
|
|
15999
|
+
} else {
|
|
16000
|
+
console.log(
|
|
16001
|
+
import_chalk21.default.dim(" Run ") + import_chalk21.default.cyan("node9 scan --drill-down") + import_chalk21.default.dim(" to see full commands and session IDs.")
|
|
16002
|
+
);
|
|
16003
|
+
}
|
|
14782
16004
|
} else {
|
|
14783
|
-
|
|
16005
|
+
const riskySummary = totalFindings + scan.dlpFindings.length;
|
|
16006
|
+
if (riskySummary > 0) {
|
|
16007
|
+
console.log(
|
|
16008
|
+
import_chalk21.default.yellow.bold(
|
|
16009
|
+
` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
|
|
16010
|
+
) + import_chalk21.default.dim(" node9 would have caught them.")
|
|
16011
|
+
);
|
|
16012
|
+
}
|
|
16013
|
+
console.log("");
|
|
16014
|
+
console.log(import_chalk21.default.bold(" Protect your next session in 30 seconds:"));
|
|
16015
|
+
console.log("");
|
|
16016
|
+
console.log(" " + import_chalk21.default.cyan("npm install -g @node9/proxy"));
|
|
16017
|
+
console.log(" " + import_chalk21.default.cyan("node9 init"));
|
|
16018
|
+
console.log("");
|
|
16019
|
+
console.log(import_chalk21.default.dim(" node9 hooks into Claude Code automatically."));
|
|
14784
16020
|
console.log(
|
|
14785
|
-
|
|
16021
|
+
import_chalk21.default.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
|
|
14786
16022
|
);
|
|
16023
|
+
console.log("");
|
|
16024
|
+
console.log(" " + import_chalk21.default.dim("\u2192 ") + import_chalk21.default.underline("https://node9.ai"));
|
|
14787
16025
|
}
|
|
14788
16026
|
console.log("");
|
|
14789
16027
|
});
|
|
@@ -15050,6 +16288,7 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
15050
16288
|
projectLabel: projectLabel(projectRoot),
|
|
15051
16289
|
firstPrompt,
|
|
15052
16290
|
startTime,
|
|
16291
|
+
lastActiveTime: lastToolTs || startTime,
|
|
15053
16292
|
toolCalls,
|
|
15054
16293
|
blockedCalls,
|
|
15055
16294
|
costUSD,
|
|
@@ -15061,6 +16300,128 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
15061
16300
|
}
|
|
15062
16301
|
return summaries;
|
|
15063
16302
|
}
|
|
16303
|
+
function buildCodexSessions(days, allAuditEntries) {
|
|
16304
|
+
const sessionsBase = import_path37.default.join(import_os30.default.homedir(), ".codex", "sessions");
|
|
16305
|
+
if (!import_fs34.default.existsSync(sessionsBase)) return [];
|
|
16306
|
+
const cutoff = days !== null ? (() => {
|
|
16307
|
+
const d = /* @__PURE__ */ new Date();
|
|
16308
|
+
d.setDate(d.getDate() - days);
|
|
16309
|
+
d.setHours(0, 0, 0, 0);
|
|
16310
|
+
return d;
|
|
16311
|
+
})() : null;
|
|
16312
|
+
const jsonlFiles = [];
|
|
16313
|
+
try {
|
|
16314
|
+
for (const year of import_fs34.default.readdirSync(sessionsBase)) {
|
|
16315
|
+
const yearPath = import_path37.default.join(sessionsBase, year);
|
|
16316
|
+
try {
|
|
16317
|
+
if (!import_fs34.default.statSync(yearPath).isDirectory()) continue;
|
|
16318
|
+
} catch {
|
|
16319
|
+
continue;
|
|
16320
|
+
}
|
|
16321
|
+
for (const month of import_fs34.default.readdirSync(yearPath)) {
|
|
16322
|
+
const monthPath = import_path37.default.join(yearPath, month);
|
|
16323
|
+
try {
|
|
16324
|
+
if (!import_fs34.default.statSync(monthPath).isDirectory()) continue;
|
|
16325
|
+
} catch {
|
|
16326
|
+
continue;
|
|
16327
|
+
}
|
|
16328
|
+
for (const day of import_fs34.default.readdirSync(monthPath)) {
|
|
16329
|
+
const dayPath = import_path37.default.join(monthPath, day);
|
|
16330
|
+
try {
|
|
16331
|
+
if (!import_fs34.default.statSync(dayPath).isDirectory()) continue;
|
|
16332
|
+
} catch {
|
|
16333
|
+
continue;
|
|
16334
|
+
}
|
|
16335
|
+
for (const file of import_fs34.default.readdirSync(dayPath)) {
|
|
16336
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(import_path37.default.join(dayPath, file));
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
} catch {
|
|
16342
|
+
return [];
|
|
16343
|
+
}
|
|
16344
|
+
const summaries = [];
|
|
16345
|
+
for (const filePath of jsonlFiles) {
|
|
16346
|
+
let lines;
|
|
16347
|
+
try {
|
|
16348
|
+
lines = import_fs34.default.readFileSync(filePath, "utf-8").split("\n");
|
|
16349
|
+
} catch {
|
|
16350
|
+
continue;
|
|
16351
|
+
}
|
|
16352
|
+
let sessionId = "";
|
|
16353
|
+
let startTime = "";
|
|
16354
|
+
let cwd = "";
|
|
16355
|
+
let firstPrompt = "";
|
|
16356
|
+
const toolCalls = [];
|
|
16357
|
+
let lastToolTs = "";
|
|
16358
|
+
let lastTotalInput = 0;
|
|
16359
|
+
let lastTotalCached = 0;
|
|
16360
|
+
let lastTotalOutput = 0;
|
|
16361
|
+
for (const line of lines) {
|
|
16362
|
+
if (!line.trim()) continue;
|
|
16363
|
+
let entry;
|
|
16364
|
+
try {
|
|
16365
|
+
entry = JSON.parse(line);
|
|
16366
|
+
} catch {
|
|
16367
|
+
continue;
|
|
16368
|
+
}
|
|
16369
|
+
const p = entry.payload ?? {};
|
|
16370
|
+
if (entry.type === "session_meta") {
|
|
16371
|
+
sessionId = String(p["id"] ?? "");
|
|
16372
|
+
startTime = String(p["timestamp"] ?? "");
|
|
16373
|
+
cwd = String(p["cwd"] ?? "");
|
|
16374
|
+
continue;
|
|
16375
|
+
}
|
|
16376
|
+
if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
|
|
16377
|
+
firstPrompt = String(p["message"] ?? "");
|
|
16378
|
+
continue;
|
|
16379
|
+
}
|
|
16380
|
+
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
16381
|
+
const info = p["info"] ?? {};
|
|
16382
|
+
const usage = info["total_token_usage"] ?? {};
|
|
16383
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
16384
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
16385
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
16386
|
+
continue;
|
|
16387
|
+
}
|
|
16388
|
+
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
16389
|
+
const tool = String(p["name"] ?? "");
|
|
16390
|
+
let input = {};
|
|
16391
|
+
try {
|
|
16392
|
+
input = JSON.parse(String(p["arguments"] ?? "{}"));
|
|
16393
|
+
} catch {
|
|
16394
|
+
}
|
|
16395
|
+
const ts = entry.timestamp ?? startTime;
|
|
16396
|
+
toolCalls.push({ tool, input, timestamp: ts });
|
|
16397
|
+
if (ts > lastToolTs) lastToolTs = ts;
|
|
16398
|
+
}
|
|
16399
|
+
}
|
|
16400
|
+
if (!sessionId || !startTime) continue;
|
|
16401
|
+
if (cutoff && new Date(startTime) < cutoff) continue;
|
|
16402
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
16403
|
+
const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
16404
|
+
const windowEnd = new Date(
|
|
16405
|
+
Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
16406
|
+
).toISOString();
|
|
16407
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
|
|
16408
|
+
summaries.push({
|
|
16409
|
+
sessionId,
|
|
16410
|
+
project: cwd,
|
|
16411
|
+
projectLabel: projectLabel(cwd),
|
|
16412
|
+
firstPrompt,
|
|
16413
|
+
startTime,
|
|
16414
|
+
lastActiveTime: lastToolTs || startTime,
|
|
16415
|
+
toolCalls,
|
|
16416
|
+
blockedCalls,
|
|
16417
|
+
costUSD,
|
|
16418
|
+
hasSnapshot: false,
|
|
16419
|
+
modifiedFiles: [],
|
|
16420
|
+
agent: "codex"
|
|
16421
|
+
});
|
|
16422
|
+
}
|
|
16423
|
+
return summaries;
|
|
16424
|
+
}
|
|
15064
16425
|
function buildSessions(days, historyPath) {
|
|
15065
16426
|
const hPath = historyPath ?? import_path37.default.join(import_os30.default.homedir(), ".claude", "history.jsonl");
|
|
15066
16427
|
let historyRaw;
|
|
@@ -15101,12 +16462,14 @@ function buildSessions(days, historyPath) {
|
|
|
15101
16462
|
// 5 min buffer
|
|
15102
16463
|
).toISOString();
|
|
15103
16464
|
const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
|
|
16465
|
+
const lastActiveTime = lastToolTs || entry.timestamp;
|
|
15104
16466
|
summaries.push({
|
|
15105
16467
|
sessionId: entry.sessionId,
|
|
15106
16468
|
project: entry.project,
|
|
15107
16469
|
projectLabel: projectLabel(entry.project),
|
|
15108
16470
|
firstPrompt: entry.display,
|
|
15109
16471
|
startTime: entry.timestamp,
|
|
16472
|
+
lastActiveTime,
|
|
15110
16473
|
toolCalls,
|
|
15111
16474
|
blockedCalls,
|
|
15112
16475
|
costUSD,
|
|
@@ -15117,8 +16480,9 @@ function buildSessions(days, historyPath) {
|
|
|
15117
16480
|
}
|
|
15118
16481
|
if (!historyPath) {
|
|
15119
16482
|
summaries.push(...buildGeminiSessions(days, allAuditEntries));
|
|
16483
|
+
summaries.push(...buildCodexSessions(days, allAuditEntries));
|
|
15120
16484
|
}
|
|
15121
|
-
summaries.sort((a, b) => a.
|
|
16485
|
+
summaries.sort((a, b) => a.lastActiveTime > b.lastActiveTime ? -1 : 1);
|
|
15122
16486
|
return summaries;
|
|
15123
16487
|
}
|
|
15124
16488
|
function fmtCost3(usd) {
|
|
@@ -15260,22 +16624,25 @@ function renderList(summaries, totalCost) {
|
|
|
15260
16624
|
console.log("");
|
|
15261
16625
|
let lastGroup = "";
|
|
15262
16626
|
for (const s of summaries) {
|
|
15263
|
-
const
|
|
16627
|
+
const activeDate = fmtDate2(s.lastActiveTime);
|
|
16628
|
+
const group = activeDate + " " + s.projectLabel;
|
|
15264
16629
|
if (group !== lastGroup) {
|
|
15265
|
-
console.log(
|
|
15266
|
-
import_chalk22.default.dim(" \u2500\u2500\u2500 ") + import_chalk22.default.bold(fmtDate2(s.startTime)) + import_chalk22.default.dim(" " + s.projectLabel)
|
|
15267
|
-
);
|
|
16630
|
+
console.log(import_chalk22.default.dim(" \u2500\u2500\u2500 ") + import_chalk22.default.bold(activeDate) + import_chalk22.default.dim(" " + s.projectLabel));
|
|
15268
16631
|
lastGroup = group;
|
|
15269
16632
|
}
|
|
16633
|
+
const startDate = fmtDate2(s.startTime);
|
|
16634
|
+
const dateRange = startDate !== activeDate ? import_chalk22.default.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
15270
16635
|
const timeStr = import_chalk22.default.dim(fmtTime(s.startTime));
|
|
15271
16636
|
const prompt = import_chalk22.default.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
15272
16637
|
const tools = s.toolCalls.length > 0 ? import_chalk22.default.dim(String(s.toolCalls.length).padStart(3) + " tools") : import_chalk22.default.dim(" 0 tools");
|
|
15273
16638
|
const cost = s.costUSD > 0 ? import_chalk22.default.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
15274
16639
|
const blocked = s.blockedCalls.length > 0 ? import_chalk22.default.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
15275
16640
|
const snap = s.hasSnapshot ? import_chalk22.default.green(" \u{1F4F8}") : "";
|
|
15276
|
-
const agentBadge = s.agent === "gemini" ? import_chalk22.default.blue(" [Gemini]") : import_chalk22.default.cyan(" [Claude]");
|
|
16641
|
+
const agentBadge = s.agent === "gemini" ? import_chalk22.default.blue(" [Gemini]") : s.agent === "codex" ? import_chalk22.default.magenta(" [Codex]") : import_chalk22.default.cyan(" [Claude]");
|
|
15277
16642
|
const sid = import_chalk22.default.dim(" " + s.sessionId.slice(0, 8));
|
|
15278
|
-
console.log(
|
|
16643
|
+
console.log(
|
|
16644
|
+
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
16645
|
+
);
|
|
15279
16646
|
}
|
|
15280
16647
|
console.log("");
|
|
15281
16648
|
console.log(
|
|
@@ -15291,7 +16658,7 @@ function renderDetail(s) {
|
|
|
15291
16658
|
);
|
|
15292
16659
|
console.log(import_chalk22.default.bold(" Project ") + import_chalk22.default.white(s.projectLabel));
|
|
15293
16660
|
if (s.agent) {
|
|
15294
|
-
const agentLabel2 = s.agent === "gemini" ? import_chalk22.default.blue("Gemini CLI") : import_chalk22.default.cyan("Claude Code");
|
|
16661
|
+
const agentLabel2 = s.agent === "gemini" ? import_chalk22.default.blue("Gemini CLI") : s.agent === "codex" ? import_chalk22.default.magenta("Codex") : import_chalk22.default.cyan("Claude Code");
|
|
15295
16662
|
console.log(import_chalk22.default.bold(" Agent ") + agentLabel2);
|
|
15296
16663
|
}
|
|
15297
16664
|
console.log(import_chalk22.default.bold(" When ") + import_chalk22.default.white(fmtDateTime(s.startTime)));
|
|
@@ -15362,7 +16729,12 @@ function registerSessionsCommand(program2) {
|
|
|
15362
16729
|
console.log("");
|
|
15363
16730
|
process.stdout.write(import_chalk22.default.dim(" Loading\u2026"));
|
|
15364
16731
|
const summaries = buildSessions(days);
|
|
15365
|
-
process.stdout.
|
|
16732
|
+
if (process.stdout.isTTY) {
|
|
16733
|
+
process.stdout.clearLine(0);
|
|
16734
|
+
process.stdout.cursorTo(0);
|
|
16735
|
+
} else {
|
|
16736
|
+
process.stdout.write("\n");
|
|
16737
|
+
}
|
|
15366
16738
|
if (options.detail) {
|
|
15367
16739
|
const target = summaries.find(
|
|
15368
16740
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
@@ -16001,10 +17373,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
16001
17373
|
program.help();
|
|
16002
17374
|
return;
|
|
16003
17375
|
}
|
|
16004
|
-
const
|
|
17376
|
+
const fullCommand2 = runArgs.join(" ");
|
|
16005
17377
|
let result = await authorizeHeadless(
|
|
16006
17378
|
"shell",
|
|
16007
|
-
{ command:
|
|
17379
|
+
{ command: fullCommand2 },
|
|
16008
17380
|
{
|
|
16009
17381
|
agent: "Terminal"
|
|
16010
17382
|
}
|
|
@@ -16012,11 +17384,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
16012
17384
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
16013
17385
|
console.error(import_chalk26.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
16014
17386
|
const daemonReady = await autoStartDaemonAndWait();
|
|
16015
|
-
if (daemonReady) result = await authorizeHeadless("shell", { command:
|
|
17387
|
+
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
|
|
16016
17388
|
}
|
|
16017
17389
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
16018
17390
|
const approved = await (0, import_prompts2.confirm)({
|
|
16019
|
-
message: `\u{1F6E1}\uFE0F Node9: Allow "${
|
|
17391
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
|
|
16020
17392
|
default: false
|
|
16021
17393
|
});
|
|
16022
17394
|
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
@@ -16029,7 +17401,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
16029
17401
|
process.exit(1);
|
|
16030
17402
|
}
|
|
16031
17403
|
console.error(import_chalk26.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
16032
|
-
await runProxy(
|
|
17404
|
+
await runProxy(fullCommand2);
|
|
16033
17405
|
} else {
|
|
16034
17406
|
program.help();
|
|
16035
17407
|
}
|