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