@node9/proxy 1.11.2 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -31
- package/dist/cli.js +2029 -286
- package/dist/cli.mjs +2023 -280
- package/dist/index.js +466 -76
- package/dist/index.mjs +466 -76
- package/dist/shields/builtin/bash-safe.json +18 -4
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -116,7 +116,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
116
116
|
}
|
|
117
117
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
118
118
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
119
|
-
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
119
|
+
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
120
120
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
121
121
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
122
|
tool: toolName,
|
|
@@ -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();
|
|
@@ -8647,10 +9062,10 @@ function bold(s) {
|
|
|
8647
9062
|
function color(c, s) {
|
|
8648
9063
|
return `${c}${s}${RESET3}`;
|
|
8649
9064
|
}
|
|
8650
|
-
function progressBar(
|
|
8651
|
-
const filled = Math.round(Math.min(
|
|
9065
|
+
function progressBar(pct, warnAt = 70, critAt = 85) {
|
|
9066
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
|
|
8652
9067
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
8653
|
-
const c =
|
|
9068
|
+
const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
|
|
8654
9069
|
return `${c}${bar}${RESET3}`;
|
|
8655
9070
|
}
|
|
8656
9071
|
function formatTimeLeft(resetsAt) {
|
|
@@ -8866,15 +9281,15 @@ function renderContextLine(stdin) {
|
|
|
8866
9281
|
}
|
|
8867
9282
|
const rl = stdin.rate_limits;
|
|
8868
9283
|
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
8869
|
-
const
|
|
8870
|
-
const bar = progressBar(
|
|
9284
|
+
const pct = Math.round(rl.five_hour.used_percentage);
|
|
9285
|
+
const bar = progressBar(pct, 60, 80);
|
|
8871
9286
|
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
8872
|
-
parts.push(`${dim("\u2502")} 5h ${bar} ${
|
|
9287
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
|
|
8873
9288
|
}
|
|
8874
9289
|
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
8875
|
-
const
|
|
8876
|
-
const bar = progressBar(
|
|
8877
|
-
parts.push(`${dim("\u2502")} 7d ${bar} ${
|
|
9290
|
+
const pct = Math.round(rl.seven_day.used_percentage);
|
|
9291
|
+
const bar = progressBar(pct, 60, 80);
|
|
9292
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
|
|
8878
9293
|
}
|
|
8879
9294
|
if (parts.length === 0) return null;
|
|
8880
9295
|
return parts.join(" ");
|
|
@@ -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);
|
|
@@ -11747,8 +12305,8 @@ function buildTestTimestamps(allEntries) {
|
|
|
11747
12305
|
return testTs;
|
|
11748
12306
|
}
|
|
11749
12307
|
function isTestEntry(entry, testTs) {
|
|
11750
|
-
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
11751
12308
|
if (entry.testRun === true) return true;
|
|
12309
|
+
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
11752
12310
|
const cmd = entry.args?.command;
|
|
11753
12311
|
if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
|
|
11754
12312
|
const t = new Date(entry.ts).getTime();
|
|
@@ -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));
|
|
@@ -11806,10 +12376,6 @@ function colorBar(value, max, width) {
|
|
|
11806
12376
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
11807
12377
|
return import_chalk9.default.cyan(s.slice(0, filled)) + import_chalk9.default.dim(s.slice(filled));
|
|
11808
12378
|
}
|
|
11809
|
-
function pct(num3, total) {
|
|
11810
|
-
if (total === 0) return "\u2013";
|
|
11811
|
-
return Math.round(num3 / total * 100) + "%";
|
|
11812
|
-
}
|
|
11813
12379
|
function fmtDate(d) {
|
|
11814
12380
|
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
11815
12381
|
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
@@ -11918,18 +12484,104 @@ function loadClaudeCost(start, end) {
|
|
|
11918
12484
|
}
|
|
11919
12485
|
return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
|
|
11920
12486
|
}
|
|
11921
|
-
function
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
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(
|
|
11933
12585
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
|
|
11934
12586
|
) + " " + import_chalk9.default.yellow("\u2192 run: node9 dlp")
|
|
11935
12587
|
);
|
|
@@ -11942,7 +12594,7 @@ function registerReportCommand(program2) {
|
|
|
11942
12594
|
}
|
|
11943
12595
|
const { start, end } = getDateRange(period);
|
|
11944
12596
|
const {
|
|
11945
|
-
total:
|
|
12597
|
+
total: claudeCostUSD,
|
|
11946
12598
|
byDay: costByDay,
|
|
11947
12599
|
byModel: costByModel,
|
|
11948
12600
|
inputTokens: costInputTokens,
|
|
@@ -11950,6 +12602,15 @@ function registerReportCommand(program2) {
|
|
|
11950
12602
|
cacheWriteTokens: costCacheWrite,
|
|
11951
12603
|
cacheReadTokens: costCacheRead
|
|
11952
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
|
+
}
|
|
11953
12614
|
const periodMs = end.getTime() - start.getTime();
|
|
11954
12615
|
const priorEnd = new Date(start.getTime() - 1);
|
|
11955
12616
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
@@ -11980,9 +12641,12 @@ function registerReportCommand(program2) {
|
|
|
11980
12641
|
`));
|
|
11981
12642
|
return;
|
|
11982
12643
|
}
|
|
11983
|
-
let
|
|
11984
|
-
let
|
|
11985
|
-
let
|
|
12644
|
+
let userApproved = 0;
|
|
12645
|
+
let userDenied = 0;
|
|
12646
|
+
let timedOut = 0;
|
|
12647
|
+
let hardBlocked = 0;
|
|
12648
|
+
let dlpBlocked = 0;
|
|
12649
|
+
let observeDlp = 0;
|
|
11986
12650
|
let loopHits = 0;
|
|
11987
12651
|
let testPasses = 0;
|
|
11988
12652
|
let testFails = 0;
|
|
@@ -11995,9 +12659,16 @@ function registerReportCommand(program2) {
|
|
|
11995
12659
|
for (const e of entries) {
|
|
11996
12660
|
const allow = isAllow(e.decision);
|
|
11997
12661
|
const dateKey = e.ts.slice(0, 10);
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12662
|
+
const userInteracted = e.source === "daemon";
|
|
12663
|
+
if (userInteracted) {
|
|
12664
|
+
if (allow) userApproved++;
|
|
12665
|
+
else userDenied++;
|
|
12666
|
+
} else if (!allow) {
|
|
12667
|
+
if (e.checkedBy === "timeout") timedOut++;
|
|
12668
|
+
else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
12669
|
+
else if (isDlp(e.checkedBy)) dlpBlocked++;
|
|
12670
|
+
else if (e.checkedBy !== "loop-detected") hardBlocked++;
|
|
12671
|
+
}
|
|
12001
12672
|
if (e.checkedBy === "loop-detected") loopHits++;
|
|
12002
12673
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
12003
12674
|
t.calls++;
|
|
@@ -12022,6 +12693,7 @@ function registerReportCommand(program2) {
|
|
|
12022
12693
|
if (e.testResult === "pass") testPasses++;
|
|
12023
12694
|
else if (e.testResult === "fail") testFails++;
|
|
12024
12695
|
}
|
|
12696
|
+
if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
|
|
12025
12697
|
const total = entries.length;
|
|
12026
12698
|
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
12027
12699
|
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
@@ -12048,25 +12720,84 @@ function registerReportCommand(program2) {
|
|
|
12048
12720
|
" " + import_chalk9.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk9.default.dim(" \xB7 ") + import_chalk9.default.white(periodLabel[period]) + import_chalk9.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk9.default.dim(` ${num(total)} events`) + (excludeTests ? import_chalk9.default.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
|
|
12049
12721
|
);
|
|
12050
12722
|
console.log(" " + line);
|
|
12051
|
-
|
|
12052
|
-
const
|
|
12053
|
-
const dlpLabel = dlpHits > 0 ? import_chalk9.default.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : import_chalk9.default.dim("\u{1F6A8} 0 DLP hits");
|
|
12054
|
-
const loopLabel = loopHits > 0 ? import_chalk9.default.yellow(`\u{1F504} ${loopHits} loops`) : import_chalk9.default.dim("\u{1F504} 0 loops");
|
|
12055
|
-
const currentRate = total > 0 ? blocked / total : 0;
|
|
12723
|
+
const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
|
|
12724
|
+
const currentRate = total > 0 ? totalBlocked / total : 0;
|
|
12056
12725
|
const trendLabel = (() => {
|
|
12057
|
-
if (priorBlockRate === null) return
|
|
12726
|
+
if (priorBlockRate === null) return "";
|
|
12058
12727
|
const delta = Math.round((currentRate - priorBlockRate) * 100);
|
|
12059
|
-
|
|
12060
|
-
return import_chalk9.default.
|
|
12728
|
+
if (delta === 0) return "";
|
|
12729
|
+
return " " + (delta > 0 ? import_chalk9.default.red(`\u25B2${delta}%`) : import_chalk9.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk9.default.dim(" vs prior");
|
|
12061
12730
|
})();
|
|
12062
12731
|
const reads = toolMap.get("Read")?.calls ?? 0;
|
|
12063
12732
|
const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
|
|
12064
12733
|
const ratioLabel = reads > 0 ? import_chalk9.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk9.default.dim("edit/read \u2013");
|
|
12065
12734
|
const testLabel = testPasses + testFails > 0 ? import_chalk9.default.dim("tests ") + import_chalk9.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk9.default.red(`${testFails}\u2717`) : "") : import_chalk9.default.dim("tests \u2013");
|
|
12735
|
+
console.log("");
|
|
12736
|
+
console.log(" " + import_chalk9.default.bold("Protection Summary"));
|
|
12737
|
+
console.log(" " + import_chalk9.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12066
12738
|
console.log(
|
|
12067
|
-
" " + import_chalk9.default.
|
|
12739
|
+
" " + import_chalk9.default.dim("Intercepted") + " " + import_chalk9.default.white(num(total)) + import_chalk9.default.dim(" tool calls")
|
|
12740
|
+
);
|
|
12741
|
+
console.log("");
|
|
12742
|
+
const COL1 = 18;
|
|
12743
|
+
const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
|
|
12744
|
+
const countStr = colorFn(num(count));
|
|
12745
|
+
const noteStr = note ? import_chalk9.default.dim(" " + note) : "";
|
|
12746
|
+
console.log(" " + icon + " " + import_chalk9.default.white(label.padEnd(COL1)) + countStr + noteStr);
|
|
12747
|
+
};
|
|
12748
|
+
summaryRow(
|
|
12749
|
+
userApproved > 0 ? import_chalk9.default.green("\u2705") : import_chalk9.default.dim("\u2705"),
|
|
12750
|
+
"User approved",
|
|
12751
|
+
userApproved,
|
|
12752
|
+
userApproved === 0 ? "no popups this period" : void 0,
|
|
12753
|
+
userApproved > 0 ? (s) => import_chalk9.default.green(s) : (s) => import_chalk9.default.dim(s)
|
|
12754
|
+
);
|
|
12755
|
+
summaryRow(
|
|
12756
|
+
userDenied > 0 ? import_chalk9.default.red("\u{1F6AB}") : import_chalk9.default.dim("\u{1F6AB}"),
|
|
12757
|
+
"User denied",
|
|
12758
|
+
userDenied,
|
|
12759
|
+
void 0,
|
|
12760
|
+
userDenied > 0 ? (s) => import_chalk9.default.red(s) : (s) => import_chalk9.default.dim(s)
|
|
12761
|
+
);
|
|
12762
|
+
summaryRow(
|
|
12763
|
+
timedOut > 0 ? import_chalk9.default.yellow("\u23F1") : import_chalk9.default.dim("\u23F1"),
|
|
12764
|
+
"Timed out",
|
|
12765
|
+
timedOut,
|
|
12766
|
+
timedOut > 0 ? "no approval response" : void 0,
|
|
12767
|
+
timedOut > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
|
|
12768
|
+
);
|
|
12769
|
+
summaryRow(
|
|
12770
|
+
hardBlocked > 0 ? import_chalk9.default.red("\u{1F6D1}") : import_chalk9.default.dim("\u{1F6D1}"),
|
|
12771
|
+
"Auto-blocked",
|
|
12772
|
+
hardBlocked,
|
|
12773
|
+
void 0,
|
|
12774
|
+
hardBlocked > 0 ? (s) => import_chalk9.default.red(s) : (s) => import_chalk9.default.dim(s)
|
|
12775
|
+
);
|
|
12776
|
+
summaryRow(
|
|
12777
|
+
dlpBlocked > 0 ? import_chalk9.default.yellow("\u{1F6A8}") : import_chalk9.default.dim("\u{1F6A8}"),
|
|
12778
|
+
"DLP blocked",
|
|
12779
|
+
dlpBlocked,
|
|
12780
|
+
void 0,
|
|
12781
|
+
dlpBlocked > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
|
|
12782
|
+
);
|
|
12783
|
+
summaryRow(
|
|
12784
|
+
observeDlp > 0 ? import_chalk9.default.blue("\u{1F441}") : import_chalk9.default.dim("\u{1F441}"),
|
|
12785
|
+
"DLP (observe)",
|
|
12786
|
+
observeDlp,
|
|
12787
|
+
observeDlp > 0 ? "would-block in strict mode" : void 0,
|
|
12788
|
+
observeDlp > 0 ? (s) => import_chalk9.default.blue(s) : (s) => import_chalk9.default.dim(s)
|
|
12789
|
+
);
|
|
12790
|
+
summaryRow(
|
|
12791
|
+
loopHits > 0 ? import_chalk9.default.yellow("\u{1F504}") : import_chalk9.default.dim("\u{1F504}"),
|
|
12792
|
+
"Loops detected",
|
|
12793
|
+
loopHits,
|
|
12794
|
+
void 0,
|
|
12795
|
+
loopHits > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
|
|
12068
12796
|
);
|
|
12069
|
-
|
|
12797
|
+
if (trendLabel || ratioLabel || testPasses + testFails > 0) {
|
|
12798
|
+
console.log("");
|
|
12799
|
+
console.log(" " + ratioLabel + " " + testLabel + trendLabel);
|
|
12800
|
+
}
|
|
12070
12801
|
console.log("");
|
|
12071
12802
|
const toolHeaderRaw = "Top Tools";
|
|
12072
12803
|
const blockHeaderRaw = "Top Blocks";
|
|
@@ -12089,7 +12820,8 @@ function registerReportCommand(program2) {
|
|
|
12089
12820
|
let rightStyled = "";
|
|
12090
12821
|
if (i < topBlocks.length) {
|
|
12091
12822
|
const [reason, count] = topBlocks[i];
|
|
12092
|
-
const
|
|
12823
|
+
const readable = humanBlockReason(reason);
|
|
12824
|
+
const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
12093
12825
|
const countStr = num(count).padStart(BLOCK_COUNT_W);
|
|
12094
12826
|
const b = colorBar(count, maxBlock, BAR);
|
|
12095
12827
|
rightStyled = import_chalk9.default.white(label.padEnd(LABEL)) + b + " " + import_chalk9.default.red(countStr);
|
|
@@ -12155,31 +12887,24 @@ function registerReportCommand(program2) {
|
|
|
12155
12887
|
console.log("");
|
|
12156
12888
|
console.log(" " + import_chalk9.default.bold("Tokens") + " " + import_chalk9.default.dim(`${num(totalTokens)} total`));
|
|
12157
12889
|
console.log(" " + import_chalk9.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12158
|
-
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 = [
|
|
12159
12894
|
["Input", costInputTokens, import_chalk9.default.cyan(num(costInputTokens))],
|
|
12160
12895
|
["Output", costOutputTokens, import_chalk9.default.white(num(costOutputTokens))],
|
|
12161
|
-
["Cache write", costCacheWrite, import_chalk9.default.yellow(num(costCacheWrite))]
|
|
12162
|
-
["Cache read", costCacheRead, import_chalk9.default.green(num(costCacheRead))]
|
|
12896
|
+
["Cache write", costCacheWrite, import_chalk9.default.yellow(num(costCacheWrite))]
|
|
12163
12897
|
];
|
|
12164
|
-
const
|
|
12165
|
-
costInputTokens,
|
|
12166
|
-
costOutputTokens,
|
|
12167
|
-
costCacheWrite,
|
|
12168
|
-
costCacheRead,
|
|
12169
|
-
1
|
|
12170
|
-
);
|
|
12171
|
-
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12172
|
-
const TOK_LABEL = 14;
|
|
12173
|
-
for (const [label, count, colored] of tokenRows) {
|
|
12898
|
+
for (const [label, count, colored] of nonCacheRows) {
|
|
12174
12899
|
if (count === 0) continue;
|
|
12175
|
-
const b = colorBar(count,
|
|
12900
|
+
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
12176
12901
|
console.log(" " + import_chalk9.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
12177
12902
|
}
|
|
12178
|
-
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`) : "";
|
|
12179
12906
|
console.log(
|
|
12180
|
-
" " + import_chalk9.default.
|
|
12181
|
-
`Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
|
|
12182
|
-
)
|
|
12907
|
+
" " + import_chalk9.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk9.default.green(num(costCacheRead)) + pct
|
|
12183
12908
|
);
|
|
12184
12909
|
}
|
|
12185
12910
|
}
|
|
@@ -12195,6 +12920,11 @@ function registerReportCommand(program2) {
|
|
|
12195
12920
|
console.log("");
|
|
12196
12921
|
console.log(" " + import_chalk9.default.bold("Cost") + " " + costHeaderRight);
|
|
12197
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
|
+
);
|
|
12198
12928
|
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
12199
12929
|
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
12200
12930
|
const MODEL_LABEL = 22;
|
|
@@ -12621,6 +13351,7 @@ function registerInitCommand(program2) {
|
|
|
12621
13351
|
else if (agent === "codex") await setupCodex();
|
|
12622
13352
|
else if (agent === "windsurf") await setupWindsurf();
|
|
12623
13353
|
else if (agent === "vscode") await setupVSCode();
|
|
13354
|
+
else if (agent === "claudeDesktop") await setupClaudeDesktop();
|
|
12624
13355
|
console.log("");
|
|
12625
13356
|
}
|
|
12626
13357
|
if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
|
|
@@ -13450,6 +14181,7 @@ var import_readline4 = __toESM(require("readline"));
|
|
|
13450
14181
|
var import_fs32 = __toESM(require("fs"));
|
|
13451
14182
|
var import_os28 = __toESM(require("os"));
|
|
13452
14183
|
var import_path35 = __toESM(require("path"));
|
|
14184
|
+
var import_child_process15 = require("child_process");
|
|
13453
14185
|
init_core();
|
|
13454
14186
|
init_daemon();
|
|
13455
14187
|
init_shields();
|
|
@@ -13529,8 +14261,31 @@ var TOOLS = [
|
|
|
13529
14261
|
},
|
|
13530
14262
|
{
|
|
13531
14263
|
name: "node9_undo_list",
|
|
13532
|
-
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.",
|
|
13533
|
-
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
|
+
}
|
|
13534
14289
|
},
|
|
13535
14290
|
{
|
|
13536
14291
|
name: "node9_undo_revert",
|
|
@@ -13552,13 +14307,18 @@ var TOOLS = [
|
|
|
13552
14307
|
},
|
|
13553
14308
|
{
|
|
13554
14309
|
name: "node9_audit_get",
|
|
13555
|
-
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.",
|
|
13556
14311
|
inputSchema: {
|
|
13557
14312
|
type: "object",
|
|
13558
14313
|
properties: {
|
|
13559
14314
|
limit: {
|
|
13560
14315
|
type: "number",
|
|
13561
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.'
|
|
13562
14322
|
}
|
|
13563
14323
|
},
|
|
13564
14324
|
required: []
|
|
@@ -13569,6 +14329,53 @@ var TOOLS = [
|
|
|
13569
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.",
|
|
13570
14330
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
13571
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
|
+
},
|
|
13572
14379
|
{
|
|
13573
14380
|
name: "node9_rule_add",
|
|
13574
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.',
|
|
@@ -13761,21 +14568,40 @@ function handleApproverSet(args) {
|
|
|
13761
14568
|
}
|
|
13762
14569
|
function handleAuditGet(args) {
|
|
13763
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;
|
|
13764
14572
|
const auditPath = import_path35.default.join(import_os28.default.homedir(), ".node9", "audit.log");
|
|
13765
14573
|
if (!import_fs32.default.existsSync(auditPath)) return "No audit log found.";
|
|
13766
|
-
const
|
|
13767
|
-
const
|
|
13768
|
-
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) {
|
|
13769
14577
|
try {
|
|
13770
14578
|
const e = JSON.parse(line);
|
|
13771
|
-
|
|
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 });
|
|
13772
14593
|
} catch {
|
|
13773
|
-
|
|
14594
|
+
parsed.push({ raw: line, decision: "allow", formatted: line });
|
|
13774
14595
|
}
|
|
13775
|
-
}
|
|
13776
|
-
|
|
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}
|
|
13777
14603
|
|
|
13778
|
-
${
|
|
14604
|
+
${recent.map((e) => e.formatted).join("\n")}`;
|
|
13779
14605
|
}
|
|
13780
14606
|
function handlePolicyGet() {
|
|
13781
14607
|
const config = getConfig();
|
|
@@ -13828,10 +14654,43 @@ function handleRuleAdd(args) {
|
|
|
13828
14654
|
writeGlobalConfigRaw(raw);
|
|
13829
14655
|
return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
|
|
13830
14656
|
}
|
|
13831
|
-
function
|
|
13832
|
-
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
|
+
}
|
|
13833
14691
|
if (history.length === 0) {
|
|
13834
|
-
|
|
14692
|
+
const hint = cwdFilter ? ` for cwd: ${cwdFilter}` : "";
|
|
14693
|
+
return `No snapshots found${hint}. Node9 captures snapshots automatically before file edits.`;
|
|
13835
14694
|
}
|
|
13836
14695
|
const lines = history.slice().reverse().map((entry, i) => {
|
|
13837
14696
|
const date = new Date(entry.timestamp).toLocaleString();
|
|
@@ -13840,7 +14699,39 @@ function handleUndoList() {
|
|
|
13840
14699
|
return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
|
|
13841
14700
|
full hash: ${entry.hash}`;
|
|
13842
14701
|
});
|
|
13843
|
-
|
|
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");
|
|
13844
14735
|
}
|
|
13845
14736
|
function handleUndoRevert(args) {
|
|
13846
14737
|
const hash = args.hash;
|
|
@@ -13908,7 +14799,9 @@ function runMcpServer() {
|
|
|
13908
14799
|
} else if (toolName === "node9_approver_set") {
|
|
13909
14800
|
text = handleApproverSet(toolArgs);
|
|
13910
14801
|
} else if (toolName === "node9_undo_list") {
|
|
13911
|
-
text = handleUndoList();
|
|
14802
|
+
text = handleUndoList(toolArgs);
|
|
14803
|
+
} else if (toolName === "node9_undo_detail") {
|
|
14804
|
+
text = handleUndoDetail(toolArgs);
|
|
13912
14805
|
} else if (toolName === "node9_undo_revert") {
|
|
13913
14806
|
text = handleUndoRevert(toolArgs);
|
|
13914
14807
|
} else if (toolName === "node9_audit_get") {
|
|
@@ -13917,6 +14810,12 @@ function runMcpServer() {
|
|
|
13917
14810
|
text = handlePolicyGet();
|
|
13918
14811
|
} else if (toolName === "node9_rule_add") {
|
|
13919
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);
|
|
13920
14819
|
} else {
|
|
13921
14820
|
process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
|
|
13922
14821
|
return;
|
|
@@ -14155,7 +15054,8 @@ var SETUP_FN = {
|
|
|
14155
15054
|
cursor: setupCursor,
|
|
14156
15055
|
codex: setupCodex,
|
|
14157
15056
|
windsurf: setupWindsurf,
|
|
14158
|
-
vscode: setupVSCode
|
|
15057
|
+
vscode: setupVSCode,
|
|
15058
|
+
claudeDesktop: setupClaudeDesktop
|
|
14159
15059
|
};
|
|
14160
15060
|
var TEARDOWN_FN = {
|
|
14161
15061
|
claude: teardownClaude,
|
|
@@ -14163,7 +15063,8 @@ var TEARDOWN_FN = {
|
|
|
14163
15063
|
cursor: teardownCursor,
|
|
14164
15064
|
codex: teardownCodex,
|
|
14165
15065
|
windsurf: teardownWindsurf,
|
|
14166
|
-
vscode: teardownVSCode
|
|
15066
|
+
vscode: teardownVSCode,
|
|
15067
|
+
claudeDesktop: teardownClaudeDesktop
|
|
14167
15068
|
};
|
|
14168
15069
|
var AGENT_NAMES = Object.keys(SETUP_FN);
|
|
14169
15070
|
function registerAgentsCommand(program2) {
|
|
@@ -14247,6 +15148,22 @@ function claudeModelPrice2(model) {
|
|
|
14247
15148
|
}
|
|
14248
15149
|
return null;
|
|
14249
15150
|
}
|
|
15151
|
+
var GEMINI_PRICING = {
|
|
15152
|
+
"gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
|
|
15153
|
+
"gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
|
|
15154
|
+
"gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
|
|
15155
|
+
"gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
|
|
15156
|
+
"gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
|
|
15157
|
+
"gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
|
|
15158
|
+
};
|
|
15159
|
+
function geminiModelPrice(model) {
|
|
15160
|
+
const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
15161
|
+
for (const [key, p] of Object.entries(GEMINI_PRICING)) {
|
|
15162
|
+
if (base === key || base.startsWith(key)) return p;
|
|
15163
|
+
}
|
|
15164
|
+
if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
|
|
15165
|
+
return null;
|
|
15166
|
+
}
|
|
14250
15167
|
function num2(n) {
|
|
14251
15168
|
return n.toLocaleString();
|
|
14252
15169
|
}
|
|
@@ -14271,11 +15188,18 @@ function preview(input, max) {
|
|
|
14271
15188
|
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
14272
15189
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
14273
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
|
+
);
|
|
14274
15198
|
function buildRuleSources() {
|
|
14275
15199
|
const sources = [];
|
|
14276
15200
|
for (const [shieldName, shield] of Object.entries(SHIELDS)) {
|
|
14277
15201
|
for (const rule of shield.smartRules) {
|
|
14278
|
-
sources.push({ shieldName, shieldLabel: shieldName, rule });
|
|
15202
|
+
sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
|
|
14279
15203
|
}
|
|
14280
15204
|
}
|
|
14281
15205
|
try {
|
|
@@ -14284,9 +15208,12 @@ function buildRuleSources() {
|
|
|
14284
15208
|
if (!rule.name) continue;
|
|
14285
15209
|
if (rule.name.startsWith("shield:")) continue;
|
|
14286
15210
|
const isCloud = rule.name.startsWith("cloud:");
|
|
15211
|
+
const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
|
|
15212
|
+
const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
|
|
14287
15213
|
sources.push({
|
|
14288
|
-
shieldName: isCloud ? "cloud" : "custom",
|
|
14289
|
-
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,
|
|
14290
15217
|
rule
|
|
14291
15218
|
});
|
|
14292
15219
|
}
|
|
@@ -14332,6 +15259,7 @@ function scanClaudeHistory(startDate) {
|
|
|
14332
15259
|
for (const file of files) {
|
|
14333
15260
|
result.filesScanned++;
|
|
14334
15261
|
result.sessions++;
|
|
15262
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
14335
15263
|
let raw;
|
|
14336
15264
|
try {
|
|
14337
15265
|
raw = import_fs33.default.readFileSync(import_path36.default.join(projPath, file), "utf-8");
|
|
@@ -14389,10 +15317,13 @@ function scanClaudeHistory(startDate) {
|
|
|
14389
15317
|
redactedSample: dlpMatch.redactedSample,
|
|
14390
15318
|
toolName,
|
|
14391
15319
|
timestamp: entry.timestamp ?? "",
|
|
14392
|
-
project: projLabel
|
|
15320
|
+
project: projLabel,
|
|
15321
|
+
sessionId,
|
|
15322
|
+
agent: "claude"
|
|
14393
15323
|
});
|
|
14394
15324
|
}
|
|
14395
15325
|
}
|
|
15326
|
+
let ruleMatched = false;
|
|
14396
15327
|
for (const source of ruleSources) {
|
|
14397
15328
|
const { rule } = source;
|
|
14398
15329
|
if (rule.verdict === "allow") continue;
|
|
@@ -14408,130 +15339,642 @@ function scanClaudeHistory(startDate) {
|
|
|
14408
15339
|
toolName,
|
|
14409
15340
|
input,
|
|
14410
15341
|
timestamp: entry.timestamp ?? "",
|
|
14411
|
-
project: projLabel
|
|
15342
|
+
project: projLabel,
|
|
15343
|
+
sessionId,
|
|
15344
|
+
agent: "claude"
|
|
14412
15345
|
});
|
|
14413
15346
|
}
|
|
15347
|
+
ruleMatched = true;
|
|
14414
15348
|
break;
|
|
14415
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
|
+
}
|
|
14416
15382
|
}
|
|
14417
15383
|
}
|
|
14418
15384
|
}
|
|
14419
15385
|
}
|
|
14420
15386
|
return result;
|
|
14421
15387
|
}
|
|
14422
|
-
function
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
15388
|
+
function scanGeminiHistory(startDate) {
|
|
15389
|
+
const tmpDir = import_path36.default.join(import_os29.default.homedir(), ".gemini", "tmp");
|
|
15390
|
+
const result = {
|
|
15391
|
+
filesScanned: 0,
|
|
15392
|
+
sessions: 0,
|
|
15393
|
+
totalToolCalls: 0,
|
|
15394
|
+
bashCalls: 0,
|
|
15395
|
+
findings: [],
|
|
15396
|
+
dlpFindings: [],
|
|
15397
|
+
totalCostUSD: 0,
|
|
15398
|
+
firstDate: null,
|
|
15399
|
+
lastDate: null
|
|
15400
|
+
};
|
|
15401
|
+
if (!import_fs33.default.existsSync(tmpDir)) return result;
|
|
15402
|
+
let slugDirs;
|
|
15403
|
+
try {
|
|
15404
|
+
slugDirs = import_fs33.default.readdirSync(tmpDir);
|
|
15405
|
+
} catch {
|
|
15406
|
+
return result;
|
|
15407
|
+
}
|
|
15408
|
+
const ruleSources = buildRuleSources();
|
|
15409
|
+
for (const slug of slugDirs) {
|
|
15410
|
+
const slugPath = import_path36.default.join(tmpDir, slug);
|
|
15411
|
+
try {
|
|
15412
|
+
if (!import_fs33.default.statSync(slugPath).isDirectory()) continue;
|
|
15413
|
+
} catch {
|
|
15414
|
+
continue;
|
|
14439
15415
|
}
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
console.log(import_chalk21.default.yellow(" No JSONL session files found.\n"));
|
|
14445
|
-
return;
|
|
15416
|
+
let projLabel = slug;
|
|
15417
|
+
try {
|
|
15418
|
+
projLabel = import_fs33.default.readFileSync(import_path36.default.join(slugPath, ".project_root"), "utf-8").trim().replace(import_os29.default.homedir(), "~").slice(0, 40);
|
|
15419
|
+
} catch {
|
|
14446
15420
|
}
|
|
14447
|
-
const
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
for (const f of scan.findings) {
|
|
14455
|
-
const key = f.source.shieldName;
|
|
14456
|
-
const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
|
|
14457
|
-
entry.findings.push(f);
|
|
14458
|
-
byShield.set(key, entry);
|
|
15421
|
+
const chatsDir = import_path36.default.join(slugPath, "chats");
|
|
15422
|
+
if (!import_fs33.default.existsSync(chatsDir)) continue;
|
|
15423
|
+
let chatFiles;
|
|
15424
|
+
try {
|
|
15425
|
+
chatFiles = import_fs33.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
15426
|
+
} catch {
|
|
15427
|
+
continue;
|
|
14459
15428
|
}
|
|
14460
|
-
const
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
);
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
15429
|
+
for (const chatFile of chatFiles) {
|
|
15430
|
+
result.filesScanned++;
|
|
15431
|
+
const sessionId = chatFile.replace(/\.json$/, "");
|
|
15432
|
+
let raw;
|
|
15433
|
+
try {
|
|
15434
|
+
raw = import_fs33.default.readFileSync(import_path36.default.join(chatsDir, chatFile), "utf-8");
|
|
15435
|
+
} catch {
|
|
15436
|
+
continue;
|
|
15437
|
+
}
|
|
15438
|
+
let session;
|
|
15439
|
+
try {
|
|
15440
|
+
session = JSON.parse(raw);
|
|
15441
|
+
} catch {
|
|
15442
|
+
continue;
|
|
15443
|
+
}
|
|
15444
|
+
result.sessions++;
|
|
15445
|
+
for (const msg of session.messages ?? []) {
|
|
15446
|
+
if (msg.type !== "gemini") continue;
|
|
15447
|
+
if (startDate && msg.timestamp && new Date(msg.timestamp) < startDate) continue;
|
|
15448
|
+
if (msg.timestamp) {
|
|
15449
|
+
if (!result.firstDate || msg.timestamp < result.firstDate)
|
|
15450
|
+
result.firstDate = msg.timestamp;
|
|
15451
|
+
if (!result.lastDate || msg.timestamp > result.lastDate) result.lastDate = msg.timestamp;
|
|
15452
|
+
}
|
|
15453
|
+
const tokens = msg.tokens;
|
|
15454
|
+
const model = msg.model;
|
|
15455
|
+
if (tokens && model) {
|
|
15456
|
+
const p = geminiModelPrice(model);
|
|
15457
|
+
if (p) {
|
|
15458
|
+
const nonCached = Math.max(0, tokens.input - tokens.cached);
|
|
15459
|
+
result.totalCostUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
|
|
14489
15460
|
}
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
15461
|
+
}
|
|
15462
|
+
for (const tc of msg.toolCalls ?? []) {
|
|
15463
|
+
result.totalToolCalls++;
|
|
15464
|
+
const toolName = tc.name ?? "";
|
|
15465
|
+
const toolNameLower = toolName.toLowerCase();
|
|
15466
|
+
const input = tc.args ?? {};
|
|
15467
|
+
if (toolNameLower === "run_shell_command" || toolNameLower === "shell") {
|
|
15468
|
+
result.bashCalls++;
|
|
15469
|
+
}
|
|
15470
|
+
const rawCmd = String(input.command ?? "").trimStart();
|
|
15471
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
|
|
15472
|
+
continue;
|
|
15473
|
+
const dlpMatch = scanArgs(input);
|
|
15474
|
+
if (dlpMatch) {
|
|
15475
|
+
const isDupe = result.dlpFindings.some(
|
|
15476
|
+
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
14497
15477
|
);
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
|
|
14509
|
-
)
|
|
14510
|
-
);
|
|
15478
|
+
if (!isDupe) {
|
|
15479
|
+
result.dlpFindings.push({
|
|
15480
|
+
patternName: dlpMatch.patternName,
|
|
15481
|
+
redactedSample: dlpMatch.redactedSample,
|
|
15482
|
+
toolName,
|
|
15483
|
+
timestamp: msg.timestamp ?? "",
|
|
15484
|
+
project: projLabel,
|
|
15485
|
+
sessionId,
|
|
15486
|
+
agent: "gemini"
|
|
15487
|
+
});
|
|
14511
15488
|
}
|
|
14512
15489
|
}
|
|
14513
|
-
|
|
15490
|
+
let ruleMatched = false;
|
|
15491
|
+
for (const source of ruleSources) {
|
|
15492
|
+
const { rule } = source;
|
|
15493
|
+
if (rule.verdict === "allow") continue;
|
|
15494
|
+
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
15495
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
15496
|
+
const inputPreview = preview(input, 120);
|
|
15497
|
+
const isDupe = result.findings.some(
|
|
15498
|
+
(f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
15499
|
+
);
|
|
15500
|
+
if (!isDupe) {
|
|
15501
|
+
result.findings.push({
|
|
15502
|
+
source,
|
|
15503
|
+
toolName,
|
|
15504
|
+
input,
|
|
15505
|
+
timestamp: msg.timestamp ?? "",
|
|
15506
|
+
project: projLabel,
|
|
15507
|
+
sessionId,
|
|
15508
|
+
agent: "gemini"
|
|
15509
|
+
});
|
|
15510
|
+
}
|
|
15511
|
+
ruleMatched = true;
|
|
15512
|
+
break;
|
|
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
|
+
}
|
|
15549
|
+
}
|
|
15550
|
+
}
|
|
15551
|
+
}
|
|
15552
|
+
}
|
|
15553
|
+
return result;
|
|
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
|
+
}
|
|
14514
15737
|
}
|
|
14515
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
|
+
}
|
|
15745
|
+
function mergeScans(a, b) {
|
|
15746
|
+
const dates = [a.firstDate, b.firstDate].filter(Boolean);
|
|
15747
|
+
const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
|
|
15748
|
+
return {
|
|
15749
|
+
filesScanned: a.filesScanned + b.filesScanned,
|
|
15750
|
+
sessions: a.sessions + b.sessions,
|
|
15751
|
+
totalToolCalls: a.totalToolCalls + b.totalToolCalls,
|
|
15752
|
+
bashCalls: a.bashCalls + b.bashCalls,
|
|
15753
|
+
findings: [...a.findings, ...b.findings],
|
|
15754
|
+
dlpFindings: [...a.dlpFindings, ...b.dlpFindings],
|
|
15755
|
+
totalCostUSD: a.totalCostUSD + b.totalCostUSD,
|
|
15756
|
+
firstDate: dates.length ? dates.sort()[0] : null,
|
|
15757
|
+
lastDate: lastDates.length ? lastDates.sort().at(-1) : null
|
|
15758
|
+
};
|
|
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
|
+
}
|
|
15790
|
+
function registerScanCommand(program2) {
|
|
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;
|
|
15795
|
+
const startDate = options.all ? null : (() => {
|
|
15796
|
+
const d = /* @__PURE__ */ new Date();
|
|
15797
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
|
|
15798
|
+
d.setHours(0, 0, 0, 0);
|
|
15799
|
+
return d;
|
|
15800
|
+
})();
|
|
15801
|
+
const isInstalled = import_fs33.default.existsSync(import_path36.default.join(import_os29.default.homedir(), ".node9", "audit.log"));
|
|
15802
|
+
console.log("");
|
|
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
|
+
);
|
|
15815
|
+
console.log("");
|
|
15816
|
+
process.stdout.write(import_chalk21.default.dim(" Scanning\u2026"));
|
|
15817
|
+
const claudeScan = scanClaudeHistory(startDate);
|
|
15818
|
+
const geminiScan = scanGeminiHistory(startDate);
|
|
15819
|
+
const codexScan = scanCodexHistory(startDate);
|
|
15820
|
+
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
15821
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
15822
|
+
if (scan.filesScanned === 0) {
|
|
15823
|
+
console.log(import_chalk21.default.yellow(" No session history found."));
|
|
15824
|
+
console.log(
|
|
15825
|
+
import_chalk21.default.gray(
|
|
15826
|
+
" Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
|
|
15827
|
+
)
|
|
15828
|
+
);
|
|
15829
|
+
return;
|
|
15830
|
+
}
|
|
15831
|
+
const rangeLabel = options.all ? import_chalk21.default.dim("all time") : import_chalk21.default.dim(`last ${options.days ?? 90} days`);
|
|
15832
|
+
const dateRange = scan.firstDate && scan.lastDate ? import_chalk21.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
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(")") : "";
|
|
15841
|
+
console.log(
|
|
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
|
|
15843
|
+
);
|
|
15844
|
+
console.log("");
|
|
15845
|
+
const totalFindings = scan.findings.length;
|
|
15846
|
+
const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
|
|
15847
|
+
const reviewCount = totalFindings - blockedCount;
|
|
15848
|
+
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
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
|
+
);
|
|
15853
|
+
} else {
|
|
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) {
|
|
15863
|
+
console.log(
|
|
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")
|
|
15865
|
+
);
|
|
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")
|
|
15870
|
+
);
|
|
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);
|
|
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("");
|
|
15956
|
+
}
|
|
14516
15957
|
if (scan.dlpFindings.length > 0) {
|
|
14517
15958
|
console.log(" " + import_chalk21.default.dim("\u2500".repeat(70)));
|
|
14518
15959
|
console.log(
|
|
14519
|
-
" " + import_chalk21.default.red.bold("
|
|
15960
|
+
" " + import_chalk21.default.red.bold("\u{1F511} Credential Leaks") + import_chalk21.default.dim(" \xB7 ") + import_chalk21.default.red(
|
|
14520
15961
|
`${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
|
|
14521
15962
|
)
|
|
14522
15963
|
);
|
|
14523
|
-
const shownDlp = scan.dlpFindings.slice(0, topN);
|
|
15964
|
+
const shownDlp = drillDown ? scan.dlpFindings : scan.dlpFindings.slice(0, topN);
|
|
14524
15965
|
for (const f of shownDlp) {
|
|
14525
15966
|
const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
|
|
14526
15967
|
const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
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)}`) : "";
|
|
14527
15970
|
console.log(
|
|
14528
|
-
` ${ts}${proj}` + 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
|
|
14529
15972
|
);
|
|
14530
15973
|
}
|
|
14531
|
-
if (scan.dlpFindings.length > topN) {
|
|
15974
|
+
if (!drillDown && scan.dlpFindings.length > topN) {
|
|
14532
15975
|
console.log(
|
|
14533
15976
|
import_chalk21.default.dim(
|
|
14534
|
-
` \u2026 and ${scan.dlpFindings.length - topN} more
|
|
15977
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
|
|
14535
15978
|
)
|
|
14536
15979
|
);
|
|
14537
15980
|
}
|
|
@@ -14540,21 +15983,45 @@ function registerScanCommand(program2) {
|
|
|
14540
15983
|
}
|
|
14541
15984
|
if (scan.totalCostUSD > 0) {
|
|
14542
15985
|
console.log(
|
|
14543
|
-
" " + import_chalk21.default.bold("
|
|
15986
|
+
" " + import_chalk21.default.bold("Agent spend:") + " " + import_chalk21.default.yellow(fmtCost2(scan.totalCostUSD)) + import_chalk21.default.dim(" (for per-period breakdown: node9 report)")
|
|
14544
15987
|
);
|
|
14545
15988
|
console.log("");
|
|
14546
15989
|
}
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
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."));
|
|
14550
15992
|
console.log(
|
|
14551
|
-
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.")
|
|
14552
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
|
+
}
|
|
14553
16004
|
} else {
|
|
14554
|
-
|
|
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."));
|
|
14555
16020
|
console.log(
|
|
14556
|
-
|
|
16021
|
+
import_chalk21.default.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
|
|
14557
16022
|
);
|
|
16023
|
+
console.log("");
|
|
16024
|
+
console.log(" " + import_chalk21.default.dim("\u2192 ") + import_chalk21.default.underline("https://node9.ai"));
|
|
14558
16025
|
}
|
|
14559
16026
|
console.log("");
|
|
14560
16027
|
});
|
|
@@ -14584,6 +16051,22 @@ function modelPrice(model) {
|
|
|
14584
16051
|
}
|
|
14585
16052
|
return null;
|
|
14586
16053
|
}
|
|
16054
|
+
var GEMINI_PRICING2 = {
|
|
16055
|
+
"gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
|
|
16056
|
+
"gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
|
|
16057
|
+
"gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
|
|
16058
|
+
"gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
|
|
16059
|
+
"gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
|
|
16060
|
+
"gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
|
|
16061
|
+
};
|
|
16062
|
+
function geminiModelPrice2(model) {
|
|
16063
|
+
const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
16064
|
+
for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
|
|
16065
|
+
if (base === key || base.startsWith(key)) return p;
|
|
16066
|
+
}
|
|
16067
|
+
if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
|
|
16068
|
+
return null;
|
|
16069
|
+
}
|
|
14587
16070
|
function encodeProjectPath(projectPath) {
|
|
14588
16071
|
return projectPath.replace(/\//g, "-");
|
|
14589
16072
|
}
|
|
@@ -14699,6 +16182,246 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
14699
16182
|
}
|
|
14700
16183
|
return result;
|
|
14701
16184
|
}
|
|
16185
|
+
function buildGeminiSessions(days, allAuditEntries) {
|
|
16186
|
+
const tmpDir = import_path37.default.join(import_os30.default.homedir(), ".gemini", "tmp");
|
|
16187
|
+
if (!import_fs34.default.existsSync(tmpDir)) return [];
|
|
16188
|
+
const cutoff = days !== null ? (() => {
|
|
16189
|
+
const d = /* @__PURE__ */ new Date();
|
|
16190
|
+
d.setDate(d.getDate() - days);
|
|
16191
|
+
d.setHours(0, 0, 0, 0);
|
|
16192
|
+
return d;
|
|
16193
|
+
})() : null;
|
|
16194
|
+
let slugDirs;
|
|
16195
|
+
try {
|
|
16196
|
+
slugDirs = import_fs34.default.readdirSync(tmpDir);
|
|
16197
|
+
} catch {
|
|
16198
|
+
return [];
|
|
16199
|
+
}
|
|
16200
|
+
const summaries = [];
|
|
16201
|
+
for (const slug of slugDirs) {
|
|
16202
|
+
const slugPath = import_path37.default.join(tmpDir, slug);
|
|
16203
|
+
try {
|
|
16204
|
+
if (!import_fs34.default.statSync(slugPath).isDirectory()) continue;
|
|
16205
|
+
} catch {
|
|
16206
|
+
continue;
|
|
16207
|
+
}
|
|
16208
|
+
let projectRoot = import_path37.default.join(import_os30.default.homedir(), slug);
|
|
16209
|
+
try {
|
|
16210
|
+
projectRoot = import_fs34.default.readFileSync(import_path37.default.join(slugPath, ".project_root"), "utf-8").trim();
|
|
16211
|
+
} catch {
|
|
16212
|
+
}
|
|
16213
|
+
const chatsDir = import_path37.default.join(slugPath, "chats");
|
|
16214
|
+
if (!import_fs34.default.existsSync(chatsDir)) continue;
|
|
16215
|
+
let chatFiles;
|
|
16216
|
+
try {
|
|
16217
|
+
chatFiles = import_fs34.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
16218
|
+
} catch {
|
|
16219
|
+
continue;
|
|
16220
|
+
}
|
|
16221
|
+
for (const chatFile of chatFiles) {
|
|
16222
|
+
let raw;
|
|
16223
|
+
try {
|
|
16224
|
+
raw = import_fs34.default.readFileSync(import_path37.default.join(chatsDir, chatFile), "utf-8");
|
|
16225
|
+
} catch {
|
|
16226
|
+
continue;
|
|
16227
|
+
}
|
|
16228
|
+
let session;
|
|
16229
|
+
try {
|
|
16230
|
+
session = JSON.parse(raw);
|
|
16231
|
+
} catch {
|
|
16232
|
+
continue;
|
|
16233
|
+
}
|
|
16234
|
+
const startTime = session.startTime ?? "";
|
|
16235
|
+
if (!startTime) continue;
|
|
16236
|
+
if (cutoff && new Date(startTime) < cutoff) continue;
|
|
16237
|
+
let firstPrompt = "";
|
|
16238
|
+
for (const msg of session.messages ?? []) {
|
|
16239
|
+
if (msg.type === "user") {
|
|
16240
|
+
const content = msg.content;
|
|
16241
|
+
if (Array.isArray(content) && content[0]?.text) {
|
|
16242
|
+
firstPrompt = content[0].text;
|
|
16243
|
+
} else if (typeof content === "string") {
|
|
16244
|
+
firstPrompt = content;
|
|
16245
|
+
}
|
|
16246
|
+
break;
|
|
16247
|
+
}
|
|
16248
|
+
}
|
|
16249
|
+
const toolCalls = [];
|
|
16250
|
+
let costUSD = 0;
|
|
16251
|
+
const modifiedFiles = [];
|
|
16252
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
16253
|
+
let lastToolTs = "";
|
|
16254
|
+
for (const msg of session.messages ?? []) {
|
|
16255
|
+
if (msg.type !== "gemini") continue;
|
|
16256
|
+
const tokens = msg.tokens;
|
|
16257
|
+
const model = msg.model;
|
|
16258
|
+
if (tokens && model) {
|
|
16259
|
+
const p = geminiModelPrice2(model);
|
|
16260
|
+
if (p) {
|
|
16261
|
+
const nonCached = Math.max(0, tokens.input - tokens.cached);
|
|
16262
|
+
costUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
|
|
16263
|
+
}
|
|
16264
|
+
}
|
|
16265
|
+
for (const tc of msg.toolCalls ?? []) {
|
|
16266
|
+
const tool = tc.name ?? "";
|
|
16267
|
+
const input = tc.args ?? {};
|
|
16268
|
+
const ts = msg.timestamp ?? "";
|
|
16269
|
+
toolCalls.push({ tool, input, timestamp: ts });
|
|
16270
|
+
if (ts > lastToolTs) lastToolTs = ts;
|
|
16271
|
+
const toolLower = tool.toLowerCase();
|
|
16272
|
+
if (toolLower === "write_file" || toolLower === "edit_file" || toolLower === "create_file" || toolLower === "overwrite_file") {
|
|
16273
|
+
const fp = input.file_path ?? input.path ?? input.filename;
|
|
16274
|
+
if (typeof fp === "string" && !seenFiles.has(fp)) {
|
|
16275
|
+
seenFiles.add(fp);
|
|
16276
|
+
modifiedFiles.push(fp);
|
|
16277
|
+
}
|
|
16278
|
+
}
|
|
16279
|
+
}
|
|
16280
|
+
}
|
|
16281
|
+
const windowEnd = new Date(
|
|
16282
|
+
Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
|
|
16283
|
+
).toISOString();
|
|
16284
|
+
const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
|
|
16285
|
+
summaries.push({
|
|
16286
|
+
sessionId: session.sessionId ?? chatFile.replace(".json", ""),
|
|
16287
|
+
project: projectRoot,
|
|
16288
|
+
projectLabel: projectLabel(projectRoot),
|
|
16289
|
+
firstPrompt,
|
|
16290
|
+
startTime,
|
|
16291
|
+
lastActiveTime: lastToolTs || startTime,
|
|
16292
|
+
toolCalls,
|
|
16293
|
+
blockedCalls,
|
|
16294
|
+
costUSD,
|
|
16295
|
+
hasSnapshot: false,
|
|
16296
|
+
modifiedFiles,
|
|
16297
|
+
agent: "gemini"
|
|
16298
|
+
});
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
return summaries;
|
|
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
|
+
}
|
|
14702
16425
|
function buildSessions(days, historyPath) {
|
|
14703
16426
|
const hPath = historyPath ?? import_path37.default.join(import_os30.default.homedir(), ".claude", "history.jsonl");
|
|
14704
16427
|
let historyRaw;
|
|
@@ -14739,20 +16462,27 @@ function buildSessions(days, historyPath) {
|
|
|
14739
16462
|
// 5 min buffer
|
|
14740
16463
|
).toISOString();
|
|
14741
16464
|
const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
|
|
16465
|
+
const lastActiveTime = lastToolTs || entry.timestamp;
|
|
14742
16466
|
summaries.push({
|
|
14743
16467
|
sessionId: entry.sessionId,
|
|
14744
16468
|
project: entry.project,
|
|
14745
16469
|
projectLabel: projectLabel(entry.project),
|
|
14746
16470
|
firstPrompt: entry.display,
|
|
14747
16471
|
startTime: entry.timestamp,
|
|
16472
|
+
lastActiveTime,
|
|
14748
16473
|
toolCalls,
|
|
14749
16474
|
blockedCalls,
|
|
14750
16475
|
costUSD,
|
|
14751
16476
|
hasSnapshot,
|
|
14752
|
-
modifiedFiles
|
|
16477
|
+
modifiedFiles,
|
|
16478
|
+
agent: "claude"
|
|
14753
16479
|
});
|
|
14754
16480
|
}
|
|
14755
|
-
|
|
16481
|
+
if (!historyPath) {
|
|
16482
|
+
summaries.push(...buildGeminiSessions(days, allAuditEntries));
|
|
16483
|
+
summaries.push(...buildCodexSessions(days, allAuditEntries));
|
|
16484
|
+
}
|
|
16485
|
+
summaries.sort((a, b) => a.lastActiveTime > b.lastActiveTime ? -1 : 1);
|
|
14756
16486
|
return summaries;
|
|
14757
16487
|
}
|
|
14758
16488
|
function fmtCost3(usd) {
|
|
@@ -14863,9 +16593,9 @@ function renderSummary(summaries) {
|
|
|
14863
16593
|
const maxGroup = Math.max(...Object.values(groups));
|
|
14864
16594
|
for (const [label, count] of Object.entries(groups)) {
|
|
14865
16595
|
if (count === 0) continue;
|
|
14866
|
-
const
|
|
16596
|
+
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
14867
16597
|
console.log(
|
|
14868
|
-
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk22.default.white(String(count).padStart(4)) + import_chalk22.default.dim(` (${String(
|
|
16598
|
+
" " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk22.default.white(String(count).padStart(4)) + import_chalk22.default.dim(` (${String(pct)}%)`)
|
|
14869
16599
|
);
|
|
14870
16600
|
}
|
|
14871
16601
|
console.log("");
|
|
@@ -14894,21 +16624,25 @@ function renderList(summaries, totalCost) {
|
|
|
14894
16624
|
console.log("");
|
|
14895
16625
|
let lastGroup = "";
|
|
14896
16626
|
for (const s of summaries) {
|
|
14897
|
-
const
|
|
16627
|
+
const activeDate = fmtDate2(s.lastActiveTime);
|
|
16628
|
+
const group = activeDate + " " + s.projectLabel;
|
|
14898
16629
|
if (group !== lastGroup) {
|
|
14899
|
-
console.log(
|
|
14900
|
-
import_chalk22.default.dim(" \u2500\u2500\u2500 ") + import_chalk22.default.bold(fmtDate2(s.startTime)) + import_chalk22.default.dim(" " + s.projectLabel)
|
|
14901
|
-
);
|
|
16630
|
+
console.log(import_chalk22.default.dim(" \u2500\u2500\u2500 ") + import_chalk22.default.bold(activeDate) + import_chalk22.default.dim(" " + s.projectLabel));
|
|
14902
16631
|
lastGroup = group;
|
|
14903
16632
|
}
|
|
16633
|
+
const startDate = fmtDate2(s.startTime);
|
|
16634
|
+
const dateRange = startDate !== activeDate ? import_chalk22.default.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
14904
16635
|
const timeStr = import_chalk22.default.dim(fmtTime(s.startTime));
|
|
14905
16636
|
const prompt = import_chalk22.default.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
14906
16637
|
const tools = s.toolCalls.length > 0 ? import_chalk22.default.dim(String(s.toolCalls.length).padStart(3) + " tools") : import_chalk22.default.dim(" 0 tools");
|
|
14907
16638
|
const cost = s.costUSD > 0 ? import_chalk22.default.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
14908
16639
|
const blocked = s.blockedCalls.length > 0 ? import_chalk22.default.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
14909
16640
|
const snap = s.hasSnapshot ? import_chalk22.default.green(" \u{1F4F8}") : "";
|
|
16641
|
+
const agentBadge = s.agent === "gemini" ? import_chalk22.default.blue(" [Gemini]") : s.agent === "codex" ? import_chalk22.default.magenta(" [Codex]") : import_chalk22.default.cyan(" [Claude]");
|
|
14910
16642
|
const sid = import_chalk22.default.dim(" " + s.sessionId.slice(0, 8));
|
|
14911
|
-
console.log(
|
|
16643
|
+
console.log(
|
|
16644
|
+
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
16645
|
+
);
|
|
14912
16646
|
}
|
|
14913
16647
|
console.log("");
|
|
14914
16648
|
console.log(
|
|
@@ -14923,6 +16657,10 @@ function renderDetail(s) {
|
|
|
14923
16657
|
import_chalk22.default.bold(" Prompt ") + import_chalk22.default.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
14924
16658
|
);
|
|
14925
16659
|
console.log(import_chalk22.default.bold(" Project ") + import_chalk22.default.white(s.projectLabel));
|
|
16660
|
+
if (s.agent) {
|
|
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");
|
|
16662
|
+
console.log(import_chalk22.default.bold(" Agent ") + agentLabel2);
|
|
16663
|
+
}
|
|
14926
16664
|
console.log(import_chalk22.default.bold(" When ") + import_chalk22.default.white(fmtDateTime(s.startTime)));
|
|
14927
16665
|
if (s.costUSD > 0)
|
|
14928
16666
|
console.log(import_chalk22.default.bold(" Cost ") + import_chalk22.default.yellow("~" + fmtCost3(s.costUSD)));
|
|
@@ -14991,7 +16729,12 @@ function registerSessionsCommand(program2) {
|
|
|
14991
16729
|
console.log("");
|
|
14992
16730
|
process.stdout.write(import_chalk22.default.dim(" Loading\u2026"));
|
|
14993
16731
|
const summaries = buildSessions(days);
|
|
14994
|
-
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
|
+
}
|
|
14995
16738
|
if (options.detail) {
|
|
14996
16739
|
const target = summaries.find(
|
|
14997
16740
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
@@ -15630,10 +17373,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15630
17373
|
program.help();
|
|
15631
17374
|
return;
|
|
15632
17375
|
}
|
|
15633
|
-
const
|
|
17376
|
+
const fullCommand2 = runArgs.join(" ");
|
|
15634
17377
|
let result = await authorizeHeadless(
|
|
15635
17378
|
"shell",
|
|
15636
|
-
{ command:
|
|
17379
|
+
{ command: fullCommand2 },
|
|
15637
17380
|
{
|
|
15638
17381
|
agent: "Terminal"
|
|
15639
17382
|
}
|
|
@@ -15641,11 +17384,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15641
17384
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
15642
17385
|
console.error(import_chalk26.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
15643
17386
|
const daemonReady = await autoStartDaemonAndWait();
|
|
15644
|
-
if (daemonReady) result = await authorizeHeadless("shell", { command:
|
|
17387
|
+
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
|
|
15645
17388
|
}
|
|
15646
17389
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
15647
17390
|
const approved = await (0, import_prompts2.confirm)({
|
|
15648
|
-
message: `\u{1F6E1}\uFE0F Node9: Allow "${
|
|
17391
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
|
|
15649
17392
|
default: false
|
|
15650
17393
|
});
|
|
15651
17394
|
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
@@ -15658,7 +17401,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
15658
17401
|
process.exit(1);
|
|
15659
17402
|
}
|
|
15660
17403
|
console.error(import_chalk26.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
15661
|
-
await runProxy(
|
|
17404
|
+
await runProxy(fullCommand2);
|
|
15662
17405
|
} else {
|
|
15663
17406
|
program.help();
|
|
15664
17407
|
}
|