@node9/proxy 1.11.3 → 1.11.4

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