@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/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: true, cloud: false, terminal: true },
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: "block-force-push",
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: "block",
910
- reason: "Force push overwrites remote history and cannot be undone",
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
- value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
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 these resolve a conflict, not start a destructive action.
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
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
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: "Private Key (PEM)",
1207
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
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
- // NPM auth token in .npmrc format
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_\-]{20,}/,
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
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
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 stripStringArguments(cmd) {
1790
- let result = cmd;
1791
- result = result.replace(
1792
- /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1793
- '$1 $2 ""'
1794
- );
1795
- result = result.replace(
1796
- /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1797
- ' $1 ""'
1798
- );
1799
- return result;
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 ? stripStringArguments(normalized) : normalized;
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
- async function analyzeShellCommand(command) {
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
- const segments = lower.split("/").filter(Boolean);
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 ast = await (0, import_sh_syntax.parse)(command);
1884
- const walk = (node) => {
1885
- if (!node) return;
1886
- if (node.type === "CallExpr") {
1887
- const parts = (node.Args || []).map((arg) => {
1888
- return (arg.Parts || []).map((p) => p.Value || "").join("");
1889
- }).filter((s) => s.length > 0);
1890
- if (parts.length > 0) {
1891
- actions.push(parts[0].toLowerCase());
1892
- parts.forEach((p) => addToken(p));
1893
- parts.slice(1).forEach((p) => {
1894
- if (!p.startsWith("-")) paths.push(p);
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
- walk(ast);
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
- if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
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 = await analyzeShellCommand(shellCommand);
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 = await analyzeShellCommand(shellCommand);
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, import_sh_syntax, SQL_DML_KEYWORDS;
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
- import_sh_syntax = require("sh-syntax");
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, import_child_process15.spawn)(process.execPath, [process.argv[1], "daemon"], {
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, import_child_process15.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
8746
+ if (process.platform === "darwin") (0, import_child_process16.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
8332
8747
  else if (process.platform === "win32")
8333
- (0, import_child_process15.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
8334
- else (0, import_child_process15.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
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, import_child_process15, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
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
- import_child_process15 = require("child_process");
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(pct2, warnAt = 70, critAt = 85) {
8651
- const filled = Math.round(Math.min(pct2, 100) / 100 * BAR_WIDTH);
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 = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
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 pct2 = Math.round(rl.five_hour.used_percentage);
8870
- const bar = progressBar(pct2, 60, 80);
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} ${pct2}%${left}`);
9287
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
8873
9288
  }
8874
9289
  if (rl?.seven_day?.used_percentage !== void 0) {
8875
- const pct2 = Math.round(rl.seven_day.used_percentage);
8876
- const bar = progressBar(pct2, 60, 80);
8877
- parts.push(`${dim("\u2502")} 7d ${bar} ${pct2}%`);
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 (shouldSnapshot(tool, {}, config)) {
11081
- await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
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 registerReportCommand(program2) {
11922
- 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) => {
11923
- const period = ["today", "7d", "30d", "month"].includes(
11924
- options.period
11925
- ) ? options.period : "7d";
11926
- const logPath = import_path30.default.join(import_os24.default.homedir(), ".node9", "audit.log");
11927
- const allEntries = parseAuditLog(logPath);
11928
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
11929
- if (unackedDlp.length > 0) {
11930
- console.log("");
11931
- console.log(
11932
- import_chalk9.default.bgRed.white.bold(
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: costUSD,
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 allowed = 0;
11984
- let blocked = 0;
11985
- let dlpHits = 0;
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
- if (allow) allowed++;
11999
- else blocked++;
12000
- if (isDlp(e.checkedBy)) dlpHits++;
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
- console.log("");
12052
- const blockLabel = blocked > 0 ? import_chalk9.default.red(`\u{1F6D1} ${num(blocked)} blocked`) : import_chalk9.default.dim("\u{1F6D1} 0 blocked");
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 import_chalk9.default.dim(`${pct(blocked, total)} block rate`);
12726
+ if (priorBlockRate === null) return "";
12058
12727
  const delta = Math.round((currentRate - priorBlockRate) * 100);
12059
- const arrow = delta > 0 ? import_chalk9.default.red(`\u25B2${delta}%`) : delta < 0 ? import_chalk9.default.green(`\u25BC${Math.abs(delta)}%`) : import_chalk9.default.dim("\u2013");
12060
- return import_chalk9.default.dim(`${pct(blocked, total)} block rate `) + arrow + import_chalk9.default.dim(" vs prior");
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.green(`\u2705 ${num(allowed)} allowed`) + " " + blockLabel + " " + dlpLabel + " " + loopLabel + " " + trendLabel
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
- console.log(" " + ratioLabel + " " + testLabel);
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 label = reason.length > LABEL - 1 ? reason.slice(0, LABEL - 2) + "\u2026" : reason;
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 tokenRows = [
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 maxTok = Math.max(
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, maxTok, TOK_BAR);
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 (cacheHitPct > 0) {
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.dim(
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: { type: "object", properties: {}, required: [] }
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 lines = import_fs32.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13767
- const recent = lines.slice(-limit);
13768
- const entries = recent.map((line) => {
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
- return `${e.ts} ${String(e.tool).padEnd(20)} ${String(e.decision).padEnd(8)} ${e.agent ?? ""}`;
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
- return line;
14594
+ parsed.push({ raw: line, decision: "allow", formatted: line });
13774
14595
  }
13775
- });
13776
- return `Last ${entries.length} audit entries:
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
- ${entries.join("\n")}`;
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 handleUndoList() {
13832
- const history = getSnapshotHistory();
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
- return "No snapshots found. Node9 captures snapshots automatically before file edits.";
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
- return lines.join("\n\n");
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 registerScanCommand(program2) {
14423
- program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per shield", "5").action((options) => {
14424
- const topN = Math.max(1, parseInt(options.top, 10) || 5);
14425
- const startDate = options.all ? null : (() => {
14426
- const d = /* @__PURE__ */ new Date();
14427
- d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
14428
- d.setHours(0, 0, 0, 0);
14429
- return d;
14430
- })();
14431
- console.log("");
14432
- console.log(import_chalk21.default.cyan.bold("\u{1F50D} node9 scan") + import_chalk21.default.dim(" \u2014 what would node9 catch?"));
14433
- console.log("");
14434
- const projectsDir = import_path36.default.join(import_os29.default.homedir(), ".claude", "projects");
14435
- if (!import_fs33.default.existsSync(projectsDir)) {
14436
- console.log(import_chalk21.default.yellow(" No Claude history found at ~/.claude/projects/"));
14437
- console.log(import_chalk21.default.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14438
- return;
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
- process.stdout.write(import_chalk21.default.dim(" Scanning\u2026"));
14441
- const scan = scanClaudeHistory(startDate);
14442
- process.stdout.write("\r" + " ".repeat(20) + "\r");
14443
- if (scan.filesScanned === 0) {
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 rangeLabel = options.all ? import_chalk21.default.dim("all time") : import_chalk21.default.dim(`last ${options.days ?? 90} days`);
14448
- const dateRange = scan.firstDate && scan.lastDate ? import_chalk21.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
14449
- console.log(
14450
- " " + import_chalk21.default.white(num2(scan.sessions)) + import_chalk21.default.dim(" sessions ") + 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
14451
- );
14452
- console.log("");
14453
- const byShield = /* @__PURE__ */ new Map();
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 totalFindings = scan.findings.length;
14461
- if (totalFindings === 0 && scan.dlpFindings.length === 0) {
14462
- console.log(import_chalk21.default.green(" \u2705 No findings across all shields and rules."));
14463
- console.log(import_chalk21.default.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
14464
- } else {
14465
- if (totalFindings > 0) {
14466
- console.log(
14467
- " " + import_chalk21.default.bold("If node9 had been installed:") + " " + import_chalk21.default.yellow.bold(
14468
- `${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
14469
- )
14470
- );
14471
- console.log("");
14472
- const sorted = [...byShield.entries()].sort(
14473
- (a, b) => b[1].findings.length - a[1].findings.length
14474
- );
14475
- for (const [shieldName, { label, findings }] of sorted) {
14476
- const count = findings.length;
14477
- const isUserRule = shieldName === "custom" || shieldName === "cloud";
14478
- const shieldBadge = isUserRule ? import_chalk21.default.magenta(label) : import_chalk21.default.cyan(label);
14479
- console.log(" " + import_chalk21.default.dim("\u2500".repeat(70)));
14480
- console.log(
14481
- " " + shieldBadge + import_chalk21.default.dim(" \xB7 ") + import_chalk21.default.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : import_chalk21.default.dim(` \u2192 node9 shield enable ${shieldName}`))
14482
- );
14483
- const byRule = /* @__PURE__ */ new Map();
14484
- for (const f of findings) {
14485
- const ruleKey = f.source.rule.name ?? "unnamed";
14486
- const arr = byRule.get(ruleKey) ?? [];
14487
- arr.push(f);
14488
- byRule.set(ruleKey, arr);
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
- for (const [, ruleFindings] of byRule) {
14491
- const rule = ruleFindings[0].source.rule;
14492
- const ruleCount = ruleFindings.length;
14493
- const countBadge = ruleCount > 1 ? import_chalk21.default.white(` \xD7${ruleCount}`) : "";
14494
- const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
14495
- console.log(
14496
- " " + import_chalk21.default.white(shortName) + countBadge + (rule.reason ? import_chalk21.default.dim(` \u2014 ${rule.reason}`) : "")
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
- const shown = ruleFindings.slice(0, topN);
14499
- for (const f of shown) {
14500
- const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
14501
- const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
14502
- const cmd = import_chalk21.default.gray(preview(f.input, 55));
14503
- console.log(` ${ts}${proj}${cmd}`);
14504
- }
14505
- if (ruleFindings.length > topN) {
14506
- console.log(
14507
- import_chalk21.default.dim(
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
- console.log("");
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("Secrets / DLP") + import_chalk21.default.dim(" \xB7 ") + import_chalk21.default.red(
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 (--top ${scan.dlpFindings.length})`
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("Claude spend:") + " " + import_chalk21.default.yellow(fmtCost2(scan.totalCostUSD)) + import_chalk21.default.dim(" (for per-period breakdown: node9 report)")
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
- const auditLog = import_path36.default.join(import_os29.default.homedir(), ".node9", "audit.log");
14548
- if (import_fs33.default.existsSync(auditLog)) {
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
- console.log(import_chalk21.default.yellow.bold(" \u26A1 node9 was not running during these sessions."));
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
- " " + import_chalk21.default.white("Run ") + import_chalk21.default.cyan("node9 init") + import_chalk21.default.white(" to start protecting your AI agents.")
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
- summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
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 pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
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(pct2)}%)`)
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 group = fmtDate2(s.startTime) + " " + s.projectLabel;
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(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
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.write("\r" + " ".repeat(20) + "\r");
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 fullCommand = runArgs.join(" ");
17376
+ const fullCommand2 = runArgs.join(" ");
15634
17377
  let result = await authorizeHeadless(
15635
17378
  "shell",
15636
- { command: fullCommand },
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: fullCommand });
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 "${fullCommand}"?`,
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(fullCommand);
17404
+ await runProxy(fullCommand2);
15662
17405
  } else {
15663
17406
  program.help();
15664
17407
  }