@node9/proxy 1.11.3 → 1.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -764,7 +764,7 @@ var init_config = __esm({
764
764
  // 120-second auto-deny timeout
765
765
  flightRecorder: true,
766
766
  auditHashArgs: true,
767
- approvers: { native: true, browser: true, cloud: false, terminal: true },
767
+ approvers: { native: true, browser: false, cloud: false, terminal: true },
768
768
  cloudSyncIntervalHours: 5
769
769
  },
770
770
  policy: {
@@ -871,7 +871,7 @@ var init_config = __esm({
871
871
  },
872
872
  // ── Git safety ────────────────────────────────────────────────────────
873
873
  {
874
- name: "block-force-push",
874
+ name: "review-force-push",
875
875
  tool: "bash",
876
876
  conditions: [
877
877
  {
@@ -884,8 +884,8 @@ var init_config = __esm({
884
884
  }
885
885
  ],
886
886
  conditionMode: "all",
887
- verdict: "block",
888
- reason: "Force push overwrites remote history and cannot be undone",
887
+ verdict: "review",
888
+ reason: "Force push rewrites remote history \u2014 confirm this is intentional",
889
889
  description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
890
890
  },
891
891
  {
@@ -895,14 +895,16 @@ var init_config = __esm({
895
895
  {
896
896
  field: "command",
897
897
  op: "matches",
898
- value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
898
+ // Anchor git as a shell command so node -e / python -c scripts containing
899
+ // "git reset --hard" as a string don't false-positive.
900
+ value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
899
901
  flags: "i"
900
902
  },
901
903
  {
902
904
  field: "command",
903
905
  op: "notMatches",
904
- // Exclude recovery ops these resolve a conflict, not start a destructive action.
905
- value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
906
+ // Exclude recovery ops and routine branch-surgery (--onto) these are not destructive.
907
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
906
908
  flags: "i"
907
909
  }
908
910
  ],
@@ -1130,8 +1132,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1130
1132
  }
1131
1133
  if (typeof args === "string") {
1132
1134
  const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
1135
+ const textLower = text.toLowerCase();
1133
1136
  for (const pattern of DLP_PATTERNS) {
1137
+ if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
1138
+ continue;
1139
+ }
1134
1140
  if (pattern.regex.test(text)) {
1141
+ const matchedValue = (text.match(pattern.regex)?.[0] ?? "").toLowerCase();
1142
+ if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
1135
1143
  return {
1136
1144
  patternName: pattern.name,
1137
1145
  fieldPath,
@@ -1156,8 +1164,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1156
1164
  }
1157
1165
  function scanText(text) {
1158
1166
  const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
1167
+ const tLower = t.toLowerCase();
1159
1168
  for (const pattern of DLP_PATTERNS) {
1169
+ if (pattern.keywords && !pattern.keywords.some((kw) => tLower.includes(kw.toLowerCase()))) {
1170
+ continue;
1171
+ }
1160
1172
  if (pattern.regex.test(t)) {
1173
+ const matchedValue = (t.match(pattern.regex)?.[0] ?? "").toLowerCase();
1174
+ if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
1161
1175
  return {
1162
1176
  patternName: pattern.name,
1163
1177
  fieldPath: "response-text",
@@ -1168,36 +1182,313 @@ function scanText(text) {
1168
1182
  }
1169
1183
  return null;
1170
1184
  }
1171
- var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
1185
+ var DLP_STOPWORDS, DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
1172
1186
  var init_dlp = __esm({
1173
1187
  "src/dlp.ts"() {
1174
1188
  "use strict";
1189
+ DLP_STOPWORDS = [
1190
+ "example",
1191
+ "placeholder",
1192
+ "changeme",
1193
+ "your_key",
1194
+ "your_token",
1195
+ "your_secret",
1196
+ "replace_me",
1197
+ "insert_key",
1198
+ "put_your",
1199
+ "fake",
1200
+ "dummy",
1201
+ "sample",
1202
+ "xxxxxxxx",
1203
+ "aaaaaa",
1204
+ "bbbbbb",
1205
+ "00000000",
1206
+ "${",
1207
+ "{{",
1208
+ "%{",
1209
+ "<your",
1210
+ "test_key",
1211
+ "test_token"
1212
+ ];
1175
1213
  DLP_PATTERNS = [
1176
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
1177
- { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
1178
- // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
1179
- // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
1180
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
1181
- { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
1182
- { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
1214
+ // ── AWS ───────────────────────────────────────────────────────────────────
1183
1215
  {
1184
- name: "Private Key (PEM)",
1185
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
1186
- severity: "block"
1216
+ name: "AWS Access Key ID",
1217
+ regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
1218
+ severity: "block",
1219
+ keywords: ["akia", "asia", "abia", "acca", "a3t"]
1220
+ },
1221
+ // ── GitHub ────────────────────────────────────────────────────────────────
1222
+ {
1223
+ name: "GitHub Token",
1224
+ regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
1225
+ severity: "block",
1226
+ keywords: ["ghp_", "gho_", "ghu_", "ghs_"]
1227
+ },
1228
+ {
1229
+ name: "GitHub Fine-Grained PAT",
1230
+ regex: /\bgithub_pat_\w{82}\b/,
1231
+ severity: "block",
1232
+ keywords: ["github_pat_"]
1233
+ },
1234
+ // ── Slack ─────────────────────────────────────────────────────────────────
1235
+ {
1236
+ name: "Slack Bot Token",
1237
+ // Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
1238
+ regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
1239
+ severity: "block",
1240
+ keywords: ["xoxb-"]
1241
+ },
1242
+ // ── Anthropic ─────────────────────────────────────────────────────────────
1243
+ // Listed before OpenAI — Anthropic keys start with sk-ant- which would also
1244
+ // match the broader OpenAI sk- pattern; more specific rules must come first.
1245
+ {
1246
+ name: "Anthropic API Key",
1247
+ regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
1248
+ severity: "block",
1249
+ keywords: ["sk-ant-api03"]
1250
+ },
1251
+ {
1252
+ name: "Anthropic Admin Key",
1253
+ regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
1254
+ severity: "block",
1255
+ keywords: ["sk-ant-admin01"]
1256
+ },
1257
+ // ── OpenAI ────────────────────────────────────────────────────────────────
1258
+ {
1259
+ name: "OpenAI API Key",
1260
+ regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
1261
+ severity: "block",
1262
+ keywords: ["sk-"]
1263
+ },
1264
+ // ── Stripe ────────────────────────────────────────────────────────────────
1265
+ {
1266
+ name: "Stripe Secret Key",
1267
+ regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
1268
+ severity: "block",
1269
+ keywords: ["sk_live_", "sk_test_"]
1270
+ },
1271
+ // ── GCP ───────────────────────────────────────────────────────────────────
1272
+ {
1273
+ name: "GCP API Key",
1274
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
1275
+ severity: "block",
1276
+ keywords: ["aiza"]
1187
1277
  },
1188
- // GCP service account JSON (detects the type field that uniquely identifies it)
1189
1278
  {
1190
1279
  name: "GCP Service Account",
1191
1280
  regex: /"type"\s*:\s*"service_account"/,
1192
- severity: "block"
1281
+ severity: "block",
1282
+ keywords: ["service_account"]
1283
+ },
1284
+ // ── Azure ─────────────────────────────────────────────────────────────────
1285
+ // Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
1286
+ {
1287
+ name: "Azure AD Client Secret",
1288
+ regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
1289
+ severity: "block",
1290
+ keywords: ["q~"]
1291
+ },
1292
+ // ── Databricks ────────────────────────────────────────────────────────────
1293
+ {
1294
+ name: "Databricks API Token",
1295
+ regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
1296
+ severity: "block",
1297
+ keywords: ["dapi"]
1193
1298
  },
1194
- // NPM auth token in .npmrc format
1299
+ // ── DigitalOcean ──────────────────────────────────────────────────────────
1300
+ {
1301
+ name: "DigitalOcean PAT",
1302
+ regex: /\bdop_v1_[a-f0-9]{64}\b/,
1303
+ severity: "block",
1304
+ keywords: ["dop_v1_"]
1305
+ },
1306
+ {
1307
+ name: "DigitalOcean Access Token",
1308
+ regex: /\bdoo_v1_[a-f0-9]{64}\b/,
1309
+ severity: "block",
1310
+ keywords: ["doo_v1_"]
1311
+ },
1312
+ // ── Doppler ───────────────────────────────────────────────────────────────
1313
+ {
1314
+ name: "Doppler Token",
1315
+ regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
1316
+ severity: "block",
1317
+ keywords: ["dp.pt."]
1318
+ },
1319
+ // ── HashiCorp Vault ───────────────────────────────────────────────────────
1320
+ {
1321
+ name: "HashiCorp Vault Service Token",
1322
+ regex: /\bhvs\.[\w-]{90,120}\b/,
1323
+ severity: "block",
1324
+ keywords: ["hvs."]
1325
+ },
1326
+ {
1327
+ name: "HashiCorp Vault Batch Token",
1328
+ regex: /\bhvb\.[\w-]{138,300}\b/,
1329
+ severity: "block",
1330
+ keywords: ["hvb."]
1331
+ },
1332
+ // ── Hugging Face ──────────────────────────────────────────────────────────
1333
+ { name: "HuggingFace Token", regex: /\bhf_[A-Za-z]{34}\b/, severity: "block", keywords: ["hf_"] },
1334
+ // ── Postman ───────────────────────────────────────────────────────────────
1335
+ {
1336
+ name: "Postman API Token",
1337
+ regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
1338
+ severity: "block",
1339
+ keywords: ["pmak-"]
1340
+ },
1341
+ // ── Pulumi ────────────────────────────────────────────────────────────────
1342
+ {
1343
+ name: "Pulumi Access Token",
1344
+ regex: /\bpul-[a-f0-9]{40}\b/,
1345
+ severity: "block",
1346
+ keywords: ["pul-"]
1347
+ },
1348
+ // ── SendGrid ──────────────────────────────────────────────────────────────
1349
+ {
1350
+ name: "SendGrid API Key",
1351
+ regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
1352
+ severity: "block",
1353
+ keywords: ["sg."]
1354
+ },
1355
+ // ── Private keys (PEM) ────────────────────────────────────────────────────
1356
+ {
1357
+ name: "Private Key (PEM)",
1358
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
1359
+ severity: "block",
1360
+ keywords: ["-----begin"]
1361
+ },
1362
+ // ── NPM ───────────────────────────────────────────────────────────────────
1195
1363
  {
1196
1364
  name: "NPM Auth Token",
1197
- regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
1198
- severity: "block"
1365
+ regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
1366
+ severity: "block",
1367
+ keywords: ["_authtoken"]
1368
+ },
1369
+ // ── JWT ───────────────────────────────────────────────────────────────────
1370
+ // review (not block): JWTs appear legitimately in API calls; flag for human approval
1371
+ {
1372
+ name: "JWT",
1373
+ regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
1374
+ severity: "review",
1375
+ keywords: ["eyj"]
1376
+ },
1377
+ // ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
1378
+ {
1379
+ name: "Stripe Restricted Key",
1380
+ regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
1381
+ severity: "block",
1382
+ keywords: ["rk_live_", "rk_test_", "rk_prod_"]
1383
+ },
1384
+ // ── Slack (app token) ─────────────────────────────────────────────────────
1385
+ {
1386
+ name: "Slack App Token",
1387
+ regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
1388
+ severity: "block",
1389
+ keywords: ["xapp-"]
1390
+ },
1391
+ // ── GitLab ────────────────────────────────────────────────────────────────
1392
+ { name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
1393
+ {
1394
+ name: "GitLab Deploy Token",
1395
+ regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
1396
+ severity: "block",
1397
+ keywords: ["gldt-"]
1398
+ },
1399
+ {
1400
+ name: "GitLab CI Job Token",
1401
+ regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
1402
+ severity: "block",
1403
+ keywords: ["glcbt-"]
1404
+ },
1405
+ // ── npm (publish token) ───────────────────────────────────────────────────
1406
+ {
1407
+ name: "npm Access Token",
1408
+ regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
1409
+ severity: "block",
1410
+ keywords: ["npm_"]
1411
+ },
1412
+ // ── Shopify ───────────────────────────────────────────────────────────────
1413
+ {
1414
+ name: "Shopify Access Token",
1415
+ regex: /\bshpat_[a-fA-F0-9]{32}\b/,
1416
+ severity: "block",
1417
+ keywords: ["shpat_"]
1418
+ },
1419
+ {
1420
+ name: "Shopify Custom Access Token",
1421
+ regex: /\bshpca_[a-fA-F0-9]{32}\b/,
1422
+ severity: "block",
1423
+ keywords: ["shpca_"]
1424
+ },
1425
+ {
1426
+ name: "Shopify Private App Token",
1427
+ regex: /\bshppa_[a-fA-F0-9]{32}\b/,
1428
+ severity: "block",
1429
+ keywords: ["shppa_"]
1430
+ },
1431
+ {
1432
+ name: "Shopify Shared Secret",
1433
+ regex: /\bshpss_[a-fA-F0-9]{32}\b/,
1434
+ severity: "block",
1435
+ keywords: ["shpss_"]
1436
+ },
1437
+ // ── Linear ────────────────────────────────────────────────────────────────
1438
+ {
1439
+ name: "Linear API Key",
1440
+ regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
1441
+ severity: "block",
1442
+ keywords: ["lin_api_"]
1443
+ },
1444
+ // ── PlanetScale ───────────────────────────────────────────────────────────
1445
+ {
1446
+ name: "PlanetScale API Token",
1447
+ regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
1448
+ severity: "block",
1449
+ keywords: ["pscale_tkn_"]
1450
+ },
1451
+ {
1452
+ name: "PlanetScale Password",
1453
+ regex: /\bpscale_pw_[\w.-]{32,64}\b/,
1454
+ severity: "block",
1455
+ keywords: ["pscale_pw_"]
1456
+ },
1457
+ // ── Sentry ────────────────────────────────────────────────────────────────
1458
+ {
1459
+ name: "Sentry User Token",
1460
+ regex: /\bsntryu_[a-f0-9]{64}\b/,
1461
+ severity: "block",
1462
+ keywords: ["sntryu_"]
1463
+ },
1464
+ // ── Grafana ───────────────────────────────────────────────────────────────
1465
+ {
1466
+ name: "Grafana Service Account Token",
1467
+ regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
1468
+ severity: "block",
1469
+ keywords: ["glsa_"]
1470
+ },
1471
+ // ── Heroku ────────────────────────────────────────────────────────────────
1472
+ {
1473
+ name: "Heroku API Key",
1474
+ regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
1475
+ severity: "block",
1476
+ keywords: ["hrku-aa"]
1477
+ },
1478
+ // ── PyPI ──────────────────────────────────────────────────────────────────
1479
+ {
1480
+ name: "PyPI Upload Token",
1481
+ regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
1482
+ severity: "block",
1483
+ keywords: ["pypi-"]
1199
1484
  },
1200
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
1485
+ // ── Bearer Token ─────────────────────────────────────────────────────────
1486
+ {
1487
+ name: "Bearer Token",
1488
+ regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
1489
+ severity: "review",
1490
+ keywords: ["bearer"]
1491
+ }
1201
1492
  ];
1202
1493
  SENSITIVE_PATH_PATTERNS = [
1203
1494
  /[/\\]\.ssh[/\\]/i,
@@ -1751,7 +2042,7 @@ import fs7 from "fs";
1751
2042
  import path8 from "path";
1752
2043
  import os6 from "os";
1753
2044
  import pm from "picomatch";
1754
- import { parse } from "sh-syntax";
2045
+ import mvdanSh from "mvdan-sh";
1755
2046
  function tokenize2(toolName) {
1756
2047
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
1757
2048
  }
@@ -1769,17 +2060,97 @@ function getNestedValue(obj, path43) {
1769
2060
  if (!obj || typeof obj !== "object") return null;
1770
2061
  return path43.split(".").reduce((prev, curr) => prev?.[curr], obj);
1771
2062
  }
1772
- function stripStringArguments(cmd) {
1773
- let result = cmd;
1774
- result = result.replace(
1775
- /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1776
- '$1 $2 ""'
1777
- );
1778
- result = result.replace(
1779
- /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1780
- ' $1 ""'
1781
- );
1782
- return result;
2063
+ function normalizeCommandForPolicy(command) {
2064
+ try {
2065
+ const f = sharedParser.Parse(command, "cmd");
2066
+ const strips = [];
2067
+ syntax.Walk(f, (node) => {
2068
+ if (!node) return false;
2069
+ const n = node;
2070
+ if (syntax.NodeType(n) !== "CallExpr") return true;
2071
+ const args = n.Args || [];
2072
+ for (let i = 0; i < args.length - 1; i++) {
2073
+ const argParts = args[i].Parts || [];
2074
+ if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
2075
+ const flagVal = argParts[0].Value || "";
2076
+ if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
2077
+ const next = args[i + 1];
2078
+ const nextParts = next.Parts || [];
2079
+ if (nextParts.length !== 1) continue;
2080
+ const quotedNode = nextParts[0];
2081
+ const nt = syntax.NodeType(quotedNode);
2082
+ if (nt === "SglQuoted") {
2083
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
2084
+ } else if (nt === "DblQuoted") {
2085
+ const innerParts = quotedNode.Parts || [];
2086
+ const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
2087
+ if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
2088
+ }
2089
+ }
2090
+ return true;
2091
+ });
2092
+ if (strips.length === 0) return command;
2093
+ strips.sort((a, b) => b[0] - a[0]);
2094
+ let result = command;
2095
+ for (const [start, end] of strips) {
2096
+ result = result.slice(0, start) + '""' + result.slice(end);
2097
+ }
2098
+ return result;
2099
+ } catch {
2100
+ return command;
2101
+ }
2102
+ }
2103
+ function scanArgsForDynamicExec(args, startIdx) {
2104
+ let hasCmdSubst = false;
2105
+ let hasParamExp = false;
2106
+ let hasCurl = false;
2107
+ for (let i = startIdx; i < args.length; i++) {
2108
+ syntax.Walk(args[i], (inner) => {
2109
+ if (!inner) return false;
2110
+ const inn = inner;
2111
+ const it = syntax.NodeType(inn);
2112
+ if (it === "CmdSubst") hasCmdSubst = true;
2113
+ if (it === "ParamExp") hasParamExp = true;
2114
+ if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
2115
+ return true;
2116
+ });
2117
+ }
2118
+ if (hasCmdSubst && hasCurl) return "block";
2119
+ if (hasCmdSubst || hasParamExp) return "review";
2120
+ return null;
2121
+ }
2122
+ function detectDangerousShellExec(command) {
2123
+ try {
2124
+ const f = sharedParser.Parse(command, "cmd");
2125
+ let result = null;
2126
+ syntax.Walk(f, (node) => {
2127
+ if (!node || result === "block") return false;
2128
+ const n = node;
2129
+ if (syntax.NodeType(n) !== "CallExpr") return true;
2130
+ const args = n.Args || [];
2131
+ if (args.length === 0) return true;
2132
+ const firstParts = args[0].Parts || [];
2133
+ if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
2134
+ const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
2135
+ if (cmdName === "eval") {
2136
+ const v = scanArgsForDynamicExec(args, 1);
2137
+ if (v === "block" || v === "review" && result === null) result = v;
2138
+ } else if (SHELL_INTERPRETERS.has(cmdName)) {
2139
+ for (let i = 1; i < args.length - 1; i++) {
2140
+ const flagParts = args[i].Parts || [];
2141
+ if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
2142
+ continue;
2143
+ const v = scanArgsForDynamicExec(args, i + 1);
2144
+ if (v === "block" || v === "review" && result === null) result = v;
2145
+ break;
2146
+ }
2147
+ }
2148
+ return true;
2149
+ });
2150
+ return result;
2151
+ } catch {
2152
+ return null;
2153
+ }
1783
2154
  }
1784
2155
  function shouldSnapshot(toolName, args, config) {
1785
2156
  if (!config.settings.enableUndo) return false;
@@ -1799,7 +2170,7 @@ function evaluateSmartConditions(args, rule) {
1799
2170
  const results = rule.conditions.map((cond) => {
1800
2171
  const rawVal = getNestedValue(args, cond.field);
1801
2172
  const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1802
- const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
2173
+ const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1803
2174
  switch (cond.op) {
1804
2175
  case "exists":
1805
2176
  return val !== null && val !== "";
@@ -1847,52 +2218,35 @@ function isSqlTool(toolName, toolInspection) {
1847
2218
  const fieldName = toolInspection[matchingPattern];
1848
2219
  return fieldName === "sql" || fieldName === "query";
1849
2220
  }
1850
- async function analyzeShellCommand(command) {
2221
+ function analyzeShellCommand(command) {
1851
2222
  const actions = [];
1852
2223
  const paths = [];
1853
2224
  const allTokens = [];
1854
2225
  const addToken = (token) => {
1855
2226
  const lower = token.toLowerCase();
1856
2227
  allTokens.push(lower);
1857
- if (lower.includes("/")) {
1858
- const segments = lower.split("/").filter(Boolean);
1859
- allTokens.push(...segments);
1860
- }
1861
- if (lower.startsWith("-")) {
1862
- allTokens.push(lower.replace(/^-+/, ""));
1863
- }
2228
+ if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
2229
+ if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
1864
2230
  };
1865
2231
  try {
1866
- const ast = await parse(command);
1867
- const walk = (node) => {
1868
- if (!node) return;
1869
- if (node.type === "CallExpr") {
1870
- const parts = (node.Args || []).map((arg) => {
1871
- return (arg.Parts || []).map((p) => p.Value || "").join("");
1872
- }).filter((s) => s.length > 0);
1873
- if (parts.length > 0) {
1874
- actions.push(parts[0].toLowerCase());
1875
- parts.forEach((p) => addToken(p));
1876
- parts.slice(1).forEach((p) => {
1877
- if (!p.startsWith("-")) paths.push(p);
1878
- });
1879
- }
1880
- }
1881
- for (const key in node) {
1882
- if (key === "Parent") continue;
1883
- const val = node[key];
1884
- if (Array.isArray(val)) {
1885
- val.forEach((child) => {
1886
- if (child && typeof child === "object" && "type" in child) {
1887
- walk(child);
1888
- }
1889
- });
1890
- } else if (val && typeof val === "object" && "type" in val) {
1891
- walk(val);
1892
- }
2232
+ const f = sharedParser.Parse(command, "cmd");
2233
+ syntax.Walk(f, (node) => {
2234
+ if (!node) return false;
2235
+ const n = node;
2236
+ if (syntax.NodeType(n) !== "CallExpr") return true;
2237
+ const wordValues = (n.Args || []).map((arg) => {
2238
+ return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
2239
+ }).filter((s) => s.length > 0);
2240
+ if (wordValues.length > 0) {
2241
+ const cmd = wordValues[0].toLowerCase();
2242
+ if (!actions.includes(cmd)) actions.push(cmd);
2243
+ wordValues.forEach((w) => addToken(w));
2244
+ wordValues.slice(1).forEach((w) => {
2245
+ if (!w.startsWith("-")) paths.push(w);
2246
+ });
1893
2247
  }
1894
- };
1895
- walk(ast);
2248
+ return true;
2249
+ });
1896
2250
  } catch {
1897
2251
  }
1898
2252
  if (allTokens.length === 0) {
@@ -1917,7 +2271,18 @@ async function analyzeShellCommand(command) {
1917
2271
  }
1918
2272
  async function evaluatePolicy(toolName, args, agent, cwd) {
1919
2273
  const config = getConfig();
1920
- if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
2274
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
2275
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
2276
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2277
+ if (dlpMatch) {
2278
+ return {
2279
+ decision: dlpMatch.severity,
2280
+ blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2281
+ reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2282
+ };
2283
+ }
2284
+ }
2285
+ if (wouldBeIgnored) return { decision: "allow" };
1921
2286
  if (config.policy.smartRules.length > 0) {
1922
2287
  const matchedRule = config.policy.smartRules.find(
1923
2288
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
@@ -1947,13 +2312,30 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1947
2312
  let pathTokens = [];
1948
2313
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1949
2314
  if (shellCommand) {
1950
- const analyzed = await analyzeShellCommand(shellCommand);
2315
+ const analyzed = analyzeShellCommand(shellCommand);
1951
2316
  allTokens = analyzed.allTokens;
1952
2317
  pathTokens = analyzed.paths;
1953
2318
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1954
2319
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1955
2320
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1956
2321
  }
2322
+ const evalVerdict = detectDangerousShellExec(shellCommand);
2323
+ if (evalVerdict === "block") {
2324
+ return {
2325
+ decision: "block",
2326
+ blockedByLabel: "Node9: Eval Remote Execution",
2327
+ reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
2328
+ tier: 3
2329
+ };
2330
+ }
2331
+ if (evalVerdict === "review") {
2332
+ return {
2333
+ decision: "review",
2334
+ blockedByLabel: "Node9: Eval Dynamic Content",
2335
+ reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2336
+ tier: 3
2337
+ };
2338
+ }
1957
2339
  const pipeAnalysis = analyzePipeChain(shellCommand);
1958
2340
  if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1959
2341
  const sinks = pipeAnalysis.sinkTargets;
@@ -2207,7 +2589,7 @@ async function explainPolicy(toolName, args) {
2207
2589
  let pathTokens = [];
2208
2590
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
2209
2591
  if (shellCommand) {
2210
- const analyzed = await analyzeShellCommand(shellCommand);
2592
+ const analyzed = analyzeShellCommand(shellCommand);
2211
2593
  allTokens = analyzed.allTokens;
2212
2594
  pathTokens = analyzed.paths;
2213
2595
  const patterns = Object.keys(config.policy.toolInspection);
@@ -2240,6 +2622,25 @@ async function explainPolicy(toolName, args) {
2240
2622
  outcome: "checked",
2241
2623
  detail: "No inline execution pattern detected"
2242
2624
  });
2625
+ const evalVerdict = detectDangerousShellExec(shellCommand);
2626
+ if (evalVerdict) {
2627
+ const label = evalVerdict === "block" ? "Node9: Eval Remote Execution" : "Node9: Eval Dynamic Content";
2628
+ const detail = evalVerdict === "block" ? "eval of remote download (curl/wget) \u2014 near-certain supply-chain attack" : "eval of dynamic content (variable or subshell expansion) \u2014 requires approval";
2629
+ steps.push({ name: "AST eval detection", outcome: evalVerdict, detail, isFinal: true });
2630
+ return {
2631
+ tool: toolName,
2632
+ args,
2633
+ waterfall,
2634
+ steps,
2635
+ decision: evalVerdict,
2636
+ blockedByLabel: label
2637
+ };
2638
+ }
2639
+ steps.push({
2640
+ name: "AST eval detection",
2641
+ outcome: "checked",
2642
+ detail: "No dangerous eval detected"
2643
+ });
2243
2644
  if (isSqlTool(toolName, config.policy.toolInspection)) {
2244
2645
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
2245
2646
  steps.push({
@@ -2354,7 +2755,7 @@ function isIgnoredTool(toolName) {
2354
2755
  const config = getConfig();
2355
2756
  return matchesPattern(toolName, config.policy.ignoredTools);
2356
2757
  }
2357
- var SQL_DML_KEYWORDS;
2758
+ var syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, SQL_DML_KEYWORDS;
2358
2759
  var init_policy = __esm({
2359
2760
  "src/policy/index.ts"() {
2360
2761
  "use strict";
@@ -2365,6 +2766,20 @@ var init_policy = __esm({
2365
2766
  init_pipe_chain();
2366
2767
  init_ssh_parser();
2367
2768
  init_trusted_hosts();
2769
+ ({ syntax } = mvdanSh);
2770
+ sharedParser = syntax.NewParser();
2771
+ MESSAGE_FLAGS = /* @__PURE__ */ new Set([
2772
+ "-m",
2773
+ "--message",
2774
+ "--body",
2775
+ "--title",
2776
+ "--description",
2777
+ "--comment",
2778
+ "--subject",
2779
+ "--summary"
2780
+ ]);
2781
+ SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
2782
+ DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
2368
2783
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2369
2784
  }
2370
2785
  });
@@ -9311,6 +9726,25 @@ async function setupGemini() {
9311
9726
  printDaemonTip();
9312
9727
  }
9313
9728
  }
9729
+ function claudeDesktopConfigPath(homeDir2 = os11.homedir()) {
9730
+ if (process.platform === "darwin") {
9731
+ return path15.join(
9732
+ homeDir2,
9733
+ "Library",
9734
+ "Application Support",
9735
+ "Claude",
9736
+ "claude_desktop_config.json"
9737
+ );
9738
+ }
9739
+ if (process.platform === "linux") {
9740
+ return path15.join(homeDir2, ".config", "Claude", "claude_desktop_config.json");
9741
+ }
9742
+ if (process.platform === "win32") {
9743
+ const appData = process.env.APPDATA ?? path15.join(homeDir2, "AppData", "Roaming");
9744
+ return path15.join(appData, "Claude", "claude_desktop_config.json");
9745
+ }
9746
+ return null;
9747
+ }
9314
9748
  function detectAgents(homeDir2 = os11.homedir()) {
9315
9749
  const exists = (p) => {
9316
9750
  try {
@@ -9324,13 +9758,15 @@ function detectAgents(homeDir2 = os11.homedir()) {
9324
9758
  return false;
9325
9759
  }
9326
9760
  };
9761
+ const desktopPath = claudeDesktopConfigPath(homeDir2);
9327
9762
  return {
9328
9763
  claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
9329
9764
  gemini: exists(path15.join(homeDir2, ".gemini")),
9330
9765
  cursor: exists(path15.join(homeDir2, ".cursor")),
9331
9766
  codex: exists(path15.join(homeDir2, ".codex")),
9332
9767
  windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
9333
- vscode: exists(path15.join(homeDir2, ".vscode"))
9768
+ vscode: exists(path15.join(homeDir2, ".vscode")),
9769
+ claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath))
9334
9770
  };
9335
9771
  }
9336
9772
  async function setupCursor() {
@@ -9756,6 +10192,105 @@ function teardownVSCode() {
9756
10192
  console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
9757
10193
  }
9758
10194
  }
10195
+ async function setupClaudeDesktop() {
10196
+ const configPath = claudeDesktopConfigPath();
10197
+ if (!configPath) {
10198
+ console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
10199
+ return;
10200
+ }
10201
+ const config = readJson(configPath) ?? {};
10202
+ const servers = config.mcpServers ?? {};
10203
+ let anythingChanged = false;
10204
+ if (!hasNode9McpServer(servers)) {
10205
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
10206
+ config.mcpServers = servers;
10207
+ writeJson(configPath, config);
10208
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
10209
+ anythingChanged = true;
10210
+ }
10211
+ const serversToWrap = [];
10212
+ for (const [name, server] of Object.entries(servers)) {
10213
+ if (!server.command || server.command === "node9") continue;
10214
+ serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
10215
+ }
10216
+ if (serversToWrap.length > 0) {
10217
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
10218
+ console.log(chalk.white(` ${configPath}`));
10219
+ for (const { name, upstream } of serversToWrap) {
10220
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
10221
+ }
10222
+ console.log("");
10223
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
10224
+ if (proceed) {
10225
+ for (const { name, upstream } of serversToWrap) {
10226
+ servers[name] = {
10227
+ ...servers[name],
10228
+ command: "node9",
10229
+ args: ["mcp", "--upstream", upstream]
10230
+ };
10231
+ }
10232
+ config.mcpServers = servers;
10233
+ writeJson(configPath, config);
10234
+ console.log(chalk.green(`
10235
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
10236
+ anythingChanged = true;
10237
+ } else {
10238
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
10239
+ }
10240
+ console.log("");
10241
+ }
10242
+ console.log(
10243
+ chalk.yellow(
10244
+ " \u26A0\uFE0F Note: Claude Desktop does not support pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode."
10245
+ )
10246
+ );
10247
+ console.log("");
10248
+ if (!anythingChanged && serversToWrap.length === 0) {
10249
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Claude Desktop."));
10250
+ printDaemonTip();
10251
+ return;
10252
+ }
10253
+ if (anythingChanged) {
10254
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Desktop via MCP proxy!"));
10255
+ console.log(chalk.gray(" Restart Claude Desktop for changes to take effect."));
10256
+ printDaemonTip();
10257
+ }
10258
+ }
10259
+ function teardownClaudeDesktop() {
10260
+ const configPath = claudeDesktopConfigPath();
10261
+ if (!configPath) {
10262
+ console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
10263
+ return;
10264
+ }
10265
+ const config = readJson(configPath);
10266
+ if (!config?.mcpServers) {
10267
+ console.log(chalk.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
10268
+ return;
10269
+ }
10270
+ let changed = false;
10271
+ if (removeNode9McpServer(config.mcpServers)) {
10272
+ changed = true;
10273
+ console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${configPath}`));
10274
+ }
10275
+ for (const [name, server] of Object.entries(config.mcpServers)) {
10276
+ const args = server.args;
10277
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
10278
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
10279
+ config.mcpServers[name] = {
10280
+ ...server,
10281
+ command: originalCmd,
10282
+ args: originalArgs.length ? originalArgs : void 0
10283
+ };
10284
+ changed = true;
10285
+ }
10286
+ }
10287
+ if (changed) {
10288
+ writeJson(configPath, config);
10289
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
10290
+ } else {
10291
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
10292
+ }
10293
+ }
9759
10294
  function getAgentsStatus(homeDir2 = os11.homedir()) {
9760
10295
  const detected = detectAgents(homeDir2);
9761
10296
  const claudeWired = (() => {
@@ -9826,6 +10361,18 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
9826
10361
  installed: detected.codex,
9827
10362
  wired: codexWired,
9828
10363
  mode: detected.codex ? "mcp" : null
10364
+ },
10365
+ {
10366
+ name: "claudeDesktop",
10367
+ label: "Claude Desktop",
10368
+ installed: detected.claudeDesktop,
10369
+ wired: (() => {
10370
+ const cfgPath = claudeDesktopConfigPath(homeDir2);
10371
+ if (!cfgPath) return false;
10372
+ const cfg = readJson(cfgPath);
10373
+ return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
10374
+ })(),
10375
+ mode: detected.claudeDesktop ? "mcp" : null
9829
10376
  }
9830
10377
  ];
9831
10378
  }
@@ -10939,7 +11486,6 @@ RAW: ${raw}
10939
11486
  // src/cli/commands/log.ts
10940
11487
  init_audit();
10941
11488
  init_config();
10942
- init_policy();
10943
11489
  import fs25 from "fs";
10944
11490
  import path27 from "path";
10945
11491
  import os21 from "os";
@@ -11053,8 +11599,20 @@ function registerLogCommand(program2) {
11053
11599
  }
11054
11600
  const safeCwd = typeof payload.cwd === "string" && path27.isAbsolute(payload.cwd) ? payload.cwd : void 0;
11055
11601
  const config = getConfig(safeCwd);
11056
- if (shouldSnapshot(tool, {}, config)) {
11057
- await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
11602
+ if ((tool === "Bash" || tool === "bash") && config.settings.enableUndo !== false) {
11603
+ const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
11604
+ if (bashCommand) {
11605
+ const effectiveCwd = safeCwd ?? process.cwd();
11606
+ const history = getSnapshotHistory();
11607
+ const hasPrior = history.some((e) => e.cwd === effectiveCwd);
11608
+ if (hasPrior) {
11609
+ await createShadowSnapshot(
11610
+ "Bash",
11611
+ { command: bashCommand },
11612
+ config.policy.snapshot.ignorePaths
11613
+ );
11614
+ }
11615
+ }
11058
11616
  }
11059
11617
  } catch (err2) {
11060
11618
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -11772,6 +12330,18 @@ function isAllow(decision) {
11772
12330
  function isDlp(checkedBy) {
11773
12331
  return !!checkedBy?.includes("dlp");
11774
12332
  }
12333
+ var BLOCK_REASON_LABELS = {
12334
+ timeout: "Popup timeout",
12335
+ "smart-rule-block": "Smart rule",
12336
+ "observe-mode-dlp-would-block": "DLP (observe)",
12337
+ "persistent-deny": "Persistent deny",
12338
+ "local-decision": "User denied",
12339
+ "dlp-block": "DLP block",
12340
+ "loop-detected": "Loop detected"
12341
+ };
12342
+ function humanBlockReason(reason) {
12343
+ return BLOCK_REASON_LABELS[reason] ?? reason;
12344
+ }
11775
12345
  function barStr(value, max, width) {
11776
12346
  if (max === 0 || width <= 0) return "\u2591".repeat(width);
11777
12347
  const filled = Math.max(1, Math.round(value / max * width));
@@ -11890,20 +12460,106 @@ function loadClaudeCost(start, end) {
11890
12460
  }
11891
12461
  return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
11892
12462
  }
11893
- function registerReportCommand(program2) {
11894
- 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) => {
11895
- const period = ["today", "7d", "30d", "month"].includes(
11896
- options.period
11897
- ) ? options.period : "7d";
11898
- const logPath = path30.join(os24.homedir(), ".node9", "audit.log");
11899
- const allEntries = parseAuditLog(logPath);
11900
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
11901
- if (unackedDlp.length > 0) {
11902
- console.log("");
11903
- console.log(
11904
- chalk9.bgRed.white.bold(
11905
- ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
11906
- ) + " " + chalk9.yellow("\u2192 run: node9 dlp")
12463
+ function loadCodexCost(start, end) {
12464
+ const sessionsBase = path30.join(os24.homedir(), ".codex", "sessions");
12465
+ const byDay = /* @__PURE__ */ new Map();
12466
+ let total = 0;
12467
+ let toolCalls = 0;
12468
+ if (!fs28.existsSync(sessionsBase)) return { total, byDay, toolCalls };
12469
+ const jsonlFiles = [];
12470
+ try {
12471
+ for (const year of fs28.readdirSync(sessionsBase)) {
12472
+ const yearPath = path30.join(sessionsBase, year);
12473
+ try {
12474
+ if (!fs28.statSync(yearPath).isDirectory()) continue;
12475
+ } catch {
12476
+ continue;
12477
+ }
12478
+ for (const month of fs28.readdirSync(yearPath)) {
12479
+ const monthPath = path30.join(yearPath, month);
12480
+ try {
12481
+ if (!fs28.statSync(monthPath).isDirectory()) continue;
12482
+ } catch {
12483
+ continue;
12484
+ }
12485
+ for (const day of fs28.readdirSync(monthPath)) {
12486
+ const dayPath = path30.join(monthPath, day);
12487
+ try {
12488
+ if (!fs28.statSync(dayPath).isDirectory()) continue;
12489
+ } catch {
12490
+ continue;
12491
+ }
12492
+ for (const file of fs28.readdirSync(dayPath)) {
12493
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path30.join(dayPath, file));
12494
+ }
12495
+ }
12496
+ }
12497
+ }
12498
+ } catch {
12499
+ return { total, byDay, toolCalls };
12500
+ }
12501
+ for (const filePath of jsonlFiles) {
12502
+ let lines;
12503
+ try {
12504
+ lines = fs28.readFileSync(filePath, "utf-8").split("\n");
12505
+ } catch {
12506
+ continue;
12507
+ }
12508
+ let sessionStart2 = "";
12509
+ let lastTotalInput = 0;
12510
+ let lastTotalCached = 0;
12511
+ let lastTotalOutput = 0;
12512
+ let sessionToolCalls = 0;
12513
+ for (const line of lines) {
12514
+ if (!line.trim()) continue;
12515
+ let entry;
12516
+ try {
12517
+ entry = JSON.parse(line);
12518
+ } catch {
12519
+ continue;
12520
+ }
12521
+ const p = entry.payload ?? {};
12522
+ if (entry.type === "session_meta") {
12523
+ sessionStart2 = String(p["timestamp"] ?? "");
12524
+ continue;
12525
+ }
12526
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
12527
+ const info = p["info"] ?? {};
12528
+ const usage = info["total_token_usage"] ?? {};
12529
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
12530
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
12531
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
12532
+ }
12533
+ if (entry.type === "response_item" && p["type"] === "function_call") {
12534
+ sessionToolCalls++;
12535
+ }
12536
+ }
12537
+ if (!sessionStart2) continue;
12538
+ const ts = new Date(sessionStart2);
12539
+ if (ts < start || ts > end) continue;
12540
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
12541
+ const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
12542
+ total += cost;
12543
+ toolCalls += sessionToolCalls;
12544
+ const dateKey = sessionStart2.slice(0, 10);
12545
+ byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
12546
+ }
12547
+ return { total, byDay, toolCalls };
12548
+ }
12549
+ function registerReportCommand(program2) {
12550
+ program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").action((options) => {
12551
+ const period = ["today", "7d", "30d", "month"].includes(
12552
+ options.period
12553
+ ) ? options.period : "7d";
12554
+ const logPath = path30.join(os24.homedir(), ".node9", "audit.log");
12555
+ const allEntries = parseAuditLog(logPath);
12556
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
12557
+ if (unackedDlp.length > 0) {
12558
+ console.log("");
12559
+ console.log(
12560
+ chalk9.bgRed.white.bold(
12561
+ ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
12562
+ ) + " " + chalk9.yellow("\u2192 run: node9 dlp")
11907
12563
  );
11908
12564
  }
11909
12565
  if (allEntries.length === 0) {
@@ -11914,7 +12570,7 @@ function registerReportCommand(program2) {
11914
12570
  }
11915
12571
  const { start, end } = getDateRange(period);
11916
12572
  const {
11917
- total: costUSD,
12573
+ total: claudeCostUSD,
11918
12574
  byDay: costByDay,
11919
12575
  byModel: costByModel,
11920
12576
  inputTokens: costInputTokens,
@@ -11922,6 +12578,15 @@ function registerReportCommand(program2) {
11922
12578
  cacheWriteTokens: costCacheWrite,
11923
12579
  cacheReadTokens: costCacheRead
11924
12580
  } = loadClaudeCost(start, end);
12581
+ const {
12582
+ total: codexCostUSD,
12583
+ byDay: codexCostByDay,
12584
+ toolCalls: codexToolCalls
12585
+ } = loadCodexCost(start, end);
12586
+ const costUSD = claudeCostUSD + codexCostUSD;
12587
+ for (const [day, c] of codexCostByDay) {
12588
+ costByDay.set(day, (costByDay.get(day) ?? 0) + c);
12589
+ }
11925
12590
  const periodMs = end.getTime() - start.getTime();
11926
12591
  const priorEnd = new Date(start.getTime() - 1);
11927
12592
  const priorStart = new Date(start.getTime() - periodMs);
@@ -12004,6 +12669,7 @@ function registerReportCommand(program2) {
12004
12669
  if (e.testResult === "pass") testPasses++;
12005
12670
  else if (e.testResult === "fail") testFails++;
12006
12671
  }
12672
+ if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
12007
12673
  const total = entries.length;
12008
12674
  const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
12009
12675
  const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
@@ -12130,7 +12796,8 @@ function registerReportCommand(program2) {
12130
12796
  let rightStyled = "";
12131
12797
  if (i < topBlocks.length) {
12132
12798
  const [reason, count] = topBlocks[i];
12133
- const label = reason.length > LABEL - 1 ? reason.slice(0, LABEL - 2) + "\u2026" : reason;
12799
+ const readable = humanBlockReason(reason);
12800
+ const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
12134
12801
  const countStr = num(count).padStart(BLOCK_COUNT_W);
12135
12802
  const b = colorBar(count, maxBlock, BAR);
12136
12803
  rightStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.red(countStr);
@@ -12196,31 +12863,24 @@ function registerReportCommand(program2) {
12196
12863
  console.log("");
12197
12864
  console.log(" " + chalk9.bold("Tokens") + " " + chalk9.dim(`${num(totalTokens)} total`));
12198
12865
  console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
12199
- const tokenRows = [
12866
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
12867
+ const TOK_LABEL = 14;
12868
+ const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
12869
+ const nonCacheRows = [
12200
12870
  ["Input", costInputTokens, chalk9.cyan(num(costInputTokens))],
12201
12871
  ["Output", costOutputTokens, chalk9.white(num(costOutputTokens))],
12202
- ["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))],
12203
- ["Cache read", costCacheRead, chalk9.green(num(costCacheRead))]
12872
+ ["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))]
12204
12873
  ];
12205
- const maxTok = Math.max(
12206
- costInputTokens,
12207
- costOutputTokens,
12208
- costCacheWrite,
12209
- costCacheRead,
12210
- 1
12211
- );
12212
- const TOK_BAR = Math.max(6, Math.min(20, W - 30));
12213
- const TOK_LABEL = 14;
12214
- for (const [label, count, colored] of tokenRows) {
12874
+ for (const [label, count, colored] of nonCacheRows) {
12215
12875
  if (count === 0) continue;
12216
- const b = colorBar(count, maxTok, TOK_BAR);
12876
+ const b = colorBar(count, maxNonCache, TOK_BAR);
12217
12877
  console.log(" " + chalk9.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
12218
12878
  }
12219
- if (cacheHitPct > 0) {
12879
+ if (costCacheRead > 0) {
12880
+ const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
12881
+ const pct = cacheHitPct > 0 ? chalk9.dim(` ${cacheHitPct}% hit rate`) : "";
12220
12882
  console.log(
12221
- " " + chalk9.dim(
12222
- `Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
12223
- )
12883
+ " " + chalk9.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk9.green(num(costCacheRead)) + pct
12224
12884
  );
12225
12885
  }
12226
12886
  }
@@ -12236,6 +12896,11 @@ function registerReportCommand(program2) {
12236
12896
  console.log("");
12237
12897
  console.log(" " + chalk9.bold("Cost") + " " + costHeaderRight);
12238
12898
  console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
12899
+ if (codexCostUSD > 0)
12900
+ costByModel.set(
12901
+ "codex (openai)",
12902
+ (costByModel.get("codex (openai)") ?? 0) + codexCostUSD
12903
+ );
12239
12904
  const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
12240
12905
  const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
12241
12906
  const MODEL_LABEL = 22;
@@ -12662,6 +13327,7 @@ function registerInitCommand(program2) {
12662
13327
  else if (agent === "codex") await setupCodex();
12663
13328
  else if (agent === "windsurf") await setupWindsurf();
12664
13329
  else if (agent === "vscode") await setupVSCode();
13330
+ else if (agent === "claudeDesktop") await setupClaudeDesktop();
12665
13331
  console.log("");
12666
13332
  }
12667
13333
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -13491,6 +14157,7 @@ import readline4 from "readline";
13491
14157
  import fs32 from "fs";
13492
14158
  import os28 from "os";
13493
14159
  import path35 from "path";
14160
+ import { spawnSync as spawnSync7 } from "child_process";
13494
14161
  init_core();
13495
14162
  init_daemon();
13496
14163
  init_shields();
@@ -13570,8 +14237,31 @@ var TOOLS = [
13570
14237
  },
13571
14238
  {
13572
14239
  name: "node9_undo_list",
13573
- 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.",
13574
- inputSchema: { type: "object", properties: {}, required: [] }
14240
+ description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert or node9_undo_detail.",
14241
+ inputSchema: {
14242
+ type: "object",
14243
+ properties: {
14244
+ cwd: {
14245
+ type: "string",
14246
+ description: "Filter to snapshots for a specific project directory. Omit to show all projects."
14247
+ }
14248
+ },
14249
+ required: []
14250
+ }
14251
+ },
14252
+ {
14253
+ name: "node9_undo_detail",
14254
+ description: "Show the full details of a specific node9 snapshot: unified diff, exact files changed, tool that triggered it, command summary, working directory, and timestamp. Use this to understand exactly what a snapshot contains before deciding to revert.",
14255
+ inputSchema: {
14256
+ type: "object",
14257
+ properties: {
14258
+ hash: {
14259
+ type: "string",
14260
+ description: "The git commit hash (full or 7-char prefix) from node9_undo_list."
14261
+ }
14262
+ },
14263
+ required: ["hash"]
14264
+ }
13575
14265
  },
13576
14266
  {
13577
14267
  name: "node9_undo_revert",
@@ -13593,13 +14283,18 @@ var TOOLS = [
13593
14283
  },
13594
14284
  {
13595
14285
  name: "node9_audit_get",
13596
- description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), and agent. Use this to review what AI actions have been taken recently.",
14286
+ description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), command/args, and agent. Use this to review what AI actions have been taken recently, especially blocked or reviewed ops.",
13597
14287
  inputSchema: {
13598
14288
  type: "object",
13599
14289
  properties: {
13600
14290
  limit: {
13601
14291
  type: "number",
13602
14292
  description: "Number of recent entries to return (default: 20, max: 100)."
14293
+ },
14294
+ filter: {
14295
+ type: "string",
14296
+ enum: ["all", "block", "review"],
14297
+ description: 'Filter by decision. Omit or use "all" to show every entry.'
13603
14298
  }
13604
14299
  },
13605
14300
  required: []
@@ -13610,6 +14305,53 @@ var TOOLS = [
13610
14305
  description: "Show all active smart rules in detail \u2014 name, tool, verdict, conditions, and reason. Includes default rules, shield rules, and any custom project rules. Use this to understand exactly what is being blocked or reviewed.",
13611
14306
  inputSchema: { type: "object", properties: {}, required: [] }
13612
14307
  },
14308
+ {
14309
+ name: "node9_scan",
14310
+ description: "Scan all AI agent history (Claude + Gemini) and report what node9 would have blocked or flagged. Shows blocked operations, reviewed commands, credential leaks, and agent spend. Use this to audit past activity and find security gaps before they become incidents.",
14311
+ inputSchema: {
14312
+ type: "object",
14313
+ properties: {
14314
+ drill_down: {
14315
+ type: "boolean",
14316
+ description: "Show full commands and session IDs for every finding (default: false for a clean summary)."
14317
+ }
14318
+ },
14319
+ required: []
14320
+ }
14321
+ },
14322
+ {
14323
+ name: "node9_report",
14324
+ description: "Show an activity and security report: tool call counts, blocks, DLP findings, agent cost, and daily trends for a chosen period. Covers all AI agents (Claude, Gemini, etc.).",
14325
+ inputSchema: {
14326
+ type: "object",
14327
+ properties: {
14328
+ period: {
14329
+ type: "string",
14330
+ enum: ["today", "7d", "30d", "month"],
14331
+ description: "Time period for the report (default: 7d)."
14332
+ },
14333
+ no_tests: {
14334
+ type: "boolean",
14335
+ description: "Exclude test runner calls (npm test, vitest, pytest\u2026) from stats."
14336
+ }
14337
+ },
14338
+ required: []
14339
+ }
14340
+ },
14341
+ {
14342
+ name: "node9_session",
14343
+ description: "List recent AI agent sessions with per-session summaries: tool calls, cost, modified files, and any blocked operations. Pass a session_id to see the full tool trace for that session.",
14344
+ inputSchema: {
14345
+ type: "object",
14346
+ properties: {
14347
+ detail: {
14348
+ type: "string",
14349
+ description: "Session ID to show the full tool trace for. Omit to list all recent sessions."
14350
+ }
14351
+ },
14352
+ required: []
14353
+ }
14354
+ },
13613
14355
  {
13614
14356
  name: "node9_rule_add",
13615
14357
  description: 'Add a new protective smart rule to the global node9 config (~/.node9/config.json). Rules can block or send dangerous commands for human review based on regex conditions. IMPORTANT: only "block" and "review" verdicts are permitted \u2014 "allow" rules are never accepted because they would weaken node9 security. Rules can only be added, never removed.',
@@ -13802,21 +14544,40 @@ function handleApproverSet(args) {
13802
14544
  }
13803
14545
  function handleAuditGet(args) {
13804
14546
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
14547
+ const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
13805
14548
  const auditPath = path35.join(os28.homedir(), ".node9", "audit.log");
13806
14549
  if (!fs32.existsSync(auditPath)) return "No audit log found.";
13807
- const lines = fs32.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13808
- const recent = lines.slice(-limit);
13809
- const entries = recent.map((line) => {
14550
+ const rawLines = fs32.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
14551
+ const parsed = [];
14552
+ for (const line of rawLines) {
13810
14553
  try {
13811
14554
  const e = JSON.parse(line);
13812
- return `${e.ts} ${String(e.tool).padEnd(20)} ${String(e.decision).padEnd(8)} ${e.agent ?? ""}`;
14555
+ const decision = String(e.decision ?? "allow");
14556
+ if (filter && decision !== filter) continue;
14557
+ const argsObj = e.args;
14558
+ let detail = "";
14559
+ if (argsObj) {
14560
+ const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
14561
+ if (typeof cmd === "string" && cmd) {
14562
+ detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
14563
+ }
14564
+ }
14565
+ const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
14566
+ const toolPad = String(e.tool ?? "").padEnd(20);
14567
+ const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
14568
+ parsed.push({ raw: line, decision, formatted: line2 });
13813
14569
  } catch {
13814
- return line;
14570
+ parsed.push({ raw: line, decision: "allow", formatted: line });
13815
14571
  }
13816
- });
13817
- return `Last ${entries.length} audit entries:
14572
+ }
14573
+ const recent = parsed.slice(-limit);
14574
+ if (recent.length === 0) {
14575
+ return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
14576
+ }
14577
+ const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
14578
+ return `${header}
13818
14579
 
13819
- ${entries.join("\n")}`;
14580
+ ${recent.map((e) => e.formatted).join("\n")}`;
13820
14581
  }
13821
14582
  function handlePolicyGet() {
13822
14583
  const config = getConfig();
@@ -13869,10 +14630,43 @@ function handleRuleAdd(args) {
13869
14630
  writeGlobalConfigRaw(raw);
13870
14631
  return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
13871
14632
  }
13872
- function handleUndoList() {
13873
- const history = getSnapshotHistory();
14633
+ function runCliCommand(subArgs) {
14634
+ const result = spawnSync7(process.execPath, [process.argv[1], ...subArgs], {
14635
+ encoding: "utf-8",
14636
+ timeout: 6e4,
14637
+ // Disable colors — stdout is piped (not a TTY), chalk auto-detects, but be explicit
14638
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" }
14639
+ });
14640
+ if (result.error) throw result.error;
14641
+ const out = (result.stdout ?? "").trimEnd();
14642
+ if (!out && result.stderr) throw new Error(result.stderr.trimEnd());
14643
+ return out || "(no output)";
14644
+ }
14645
+ function handleScanMcp(args) {
14646
+ const cliArgs = ["scan"];
14647
+ if (args.drill_down === true) cliArgs.push("--drill-down");
14648
+ return runCliCommand(cliArgs);
14649
+ }
14650
+ function handleReportMcp(args) {
14651
+ const cliArgs = ["report"];
14652
+ if (typeof args.period === "string") cliArgs.push("--period", args.period);
14653
+ if (args.no_tests === true) cliArgs.push("--no-tests");
14654
+ return runCliCommand(cliArgs);
14655
+ }
14656
+ function handleSessionMcp(args) {
14657
+ const cliArgs = ["sessions"];
14658
+ if (typeof args.detail === "string" && args.detail) cliArgs.push("--detail", args.detail);
14659
+ return runCliCommand(cliArgs);
14660
+ }
14661
+ function handleUndoList(args) {
14662
+ const cwdFilter = typeof args.cwd === "string" && args.cwd ? args.cwd : null;
14663
+ let history = getSnapshotHistory();
14664
+ if (cwdFilter) {
14665
+ history = history.filter((e) => e.cwd === cwdFilter);
14666
+ }
13874
14667
  if (history.length === 0) {
13875
- return "No snapshots found. Node9 captures snapshots automatically before file edits.";
14668
+ const hint = cwdFilter ? ` for cwd: ${cwdFilter}` : "";
14669
+ return `No snapshots found${hint}. Node9 captures snapshots automatically before file edits.`;
13876
14670
  }
13877
14671
  const lines = history.slice().reverse().map((entry, i) => {
13878
14672
  const date = new Date(entry.timestamp).toLocaleString();
@@ -13881,7 +14675,39 @@ function handleUndoList() {
13881
14675
  return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
13882
14676
  full hash: ${entry.hash}`;
13883
14677
  });
13884
- return lines.join("\n\n");
14678
+ const header = cwdFilter ? `${lines.length} snapshot(s) for ${cwdFilter}:` : `${lines.length} snapshot(s) across all projects:`;
14679
+ return `${header}
14680
+
14681
+ ${lines.join("\n\n")}`;
14682
+ }
14683
+ function handleUndoDetail(args) {
14684
+ const hash = args.hash;
14685
+ if (typeof hash !== "string" || !hash) {
14686
+ throw new Error("hash is required");
14687
+ }
14688
+ const history = getSnapshotHistory();
14689
+ const entry = history.find((e) => e.hash === hash || e.hash.startsWith(hash));
14690
+ if (!entry) {
14691
+ throw new Error(`Snapshot ${hash} not found. Run node9_undo_list to see available snapshots.`);
14692
+ }
14693
+ const lines = [];
14694
+ lines.push(`Hash: ${entry.hash}`);
14695
+ lines.push(`Tool: ${entry.tool}`);
14696
+ lines.push(`Summary: ${entry.argsSummary || "(none)"}`);
14697
+ lines.push(`CWD: ${entry.cwd}`);
14698
+ lines.push(`Time: ${new Date(entry.timestamp).toLocaleString()}`);
14699
+ lines.push(`Files: ${entry.files?.length ? entry.files.join(", ") : "(none recorded)"}`);
14700
+ if (entry.diff) {
14701
+ lines.push("");
14702
+ lines.push("\u2500\u2500 Diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14703
+ lines.push(entry.diff);
14704
+ } else {
14705
+ lines.push("");
14706
+ lines.push(
14707
+ "No diff available (first snapshot for this project, or snapshot predates diff capture)."
14708
+ );
14709
+ }
14710
+ return lines.join("\n");
13885
14711
  }
13886
14712
  function handleUndoRevert(args) {
13887
14713
  const hash = args.hash;
@@ -13949,7 +14775,9 @@ function runMcpServer() {
13949
14775
  } else if (toolName === "node9_approver_set") {
13950
14776
  text = handleApproverSet(toolArgs);
13951
14777
  } else if (toolName === "node9_undo_list") {
13952
- text = handleUndoList();
14778
+ text = handleUndoList(toolArgs);
14779
+ } else if (toolName === "node9_undo_detail") {
14780
+ text = handleUndoDetail(toolArgs);
13953
14781
  } else if (toolName === "node9_undo_revert") {
13954
14782
  text = handleUndoRevert(toolArgs);
13955
14783
  } else if (toolName === "node9_audit_get") {
@@ -13958,6 +14786,12 @@ function runMcpServer() {
13958
14786
  text = handlePolicyGet();
13959
14787
  } else if (toolName === "node9_rule_add") {
13960
14788
  text = handleRuleAdd(toolArgs);
14789
+ } else if (toolName === "node9_scan") {
14790
+ text = handleScanMcp(toolArgs);
14791
+ } else if (toolName === "node9_report") {
14792
+ text = handleReportMcp(toolArgs);
14793
+ } else if (toolName === "node9_session") {
14794
+ text = handleSessionMcp(toolArgs);
13961
14795
  } else {
13962
14796
  process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
13963
14797
  return;
@@ -14196,7 +15030,8 @@ var SETUP_FN = {
14196
15030
  cursor: setupCursor,
14197
15031
  codex: setupCodex,
14198
15032
  windsurf: setupWindsurf,
14199
- vscode: setupVSCode
15033
+ vscode: setupVSCode,
15034
+ claudeDesktop: setupClaudeDesktop
14200
15035
  };
14201
15036
  var TEARDOWN_FN = {
14202
15037
  claude: teardownClaude,
@@ -14204,7 +15039,8 @@ var TEARDOWN_FN = {
14204
15039
  cursor: teardownCursor,
14205
15040
  codex: teardownCodex,
14206
15041
  windsurf: teardownWindsurf,
14207
- vscode: teardownVSCode
15042
+ vscode: teardownVSCode,
15043
+ claudeDesktop: teardownClaudeDesktop
14208
15044
  };
14209
15045
  var AGENT_NAMES = Object.keys(SETUP_FN);
14210
15046
  function registerAgentsCommand(program2) {
@@ -14328,11 +15164,18 @@ function preview(input, max) {
14328
15164
  const s = String(cmd).replace(/\s+/g, " ").trim();
14329
15165
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
14330
15166
  }
15167
+ function fullCommand(input) {
15168
+ const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
15169
+ return String(cmd).replace(/\s+/g, " ").trim();
15170
+ }
15171
+ var DEFAULT_RULE_NAMES = new Set(
15172
+ DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
15173
+ );
14331
15174
  function buildRuleSources() {
14332
15175
  const sources = [];
14333
15176
  for (const [shieldName, shield] of Object.entries(SHIELDS)) {
14334
15177
  for (const rule of shield.smartRules) {
14335
- sources.push({ shieldName, shieldLabel: shieldName, rule });
15178
+ sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
14336
15179
  }
14337
15180
  }
14338
15181
  try {
@@ -14341,9 +15184,12 @@ function buildRuleSources() {
14341
15184
  if (!rule.name) continue;
14342
15185
  if (rule.name.startsWith("shield:")) continue;
14343
15186
  const isCloud = rule.name.startsWith("cloud:");
15187
+ const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
15188
+ const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
14344
15189
  sources.push({
14345
- shieldName: isCloud ? "cloud" : "custom",
14346
- shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
15190
+ shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
15191
+ shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
15192
+ sourceType,
14347
15193
  rule
14348
15194
  });
14349
15195
  }
@@ -14389,6 +15235,7 @@ function scanClaudeHistory(startDate) {
14389
15235
  for (const file of files) {
14390
15236
  result.filesScanned++;
14391
15237
  result.sessions++;
15238
+ const sessionId = file.replace(/\.jsonl$/, "");
14392
15239
  let raw;
14393
15240
  try {
14394
15241
  raw = fs33.readFileSync(path36.join(projPath, file), "utf-8");
@@ -14447,10 +15294,12 @@ function scanClaudeHistory(startDate) {
14447
15294
  toolName,
14448
15295
  timestamp: entry.timestamp ?? "",
14449
15296
  project: projLabel,
15297
+ sessionId,
14450
15298
  agent: "claude"
14451
15299
  });
14452
15300
  }
14453
15301
  }
15302
+ let ruleMatched = false;
14454
15303
  for (const source of ruleSources) {
14455
15304
  const { rule } = source;
14456
15305
  if (rule.verdict === "allow") continue;
@@ -14467,11 +15316,45 @@ function scanClaudeHistory(startDate) {
14467
15316
  input,
14468
15317
  timestamp: entry.timestamp ?? "",
14469
15318
  project: projLabel,
15319
+ sessionId,
14470
15320
  agent: "claude"
14471
15321
  });
14472
15322
  }
15323
+ ruleMatched = true;
14473
15324
  break;
14474
15325
  }
15326
+ if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
15327
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
15328
+ if (shellVerdict) {
15329
+ const astRule = {
15330
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
15331
+ tool: "bash",
15332
+ conditions: [],
15333
+ verdict: shellVerdict,
15334
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
15335
+ };
15336
+ const inputPreview = preview(input, 120);
15337
+ const isDupe = result.findings.some(
15338
+ (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
15339
+ );
15340
+ if (!isDupe) {
15341
+ result.findings.push({
15342
+ source: {
15343
+ shieldName: "bash-safe",
15344
+ shieldLabel: "bash-safe (AST)",
15345
+ sourceType: "shield",
15346
+ rule: astRule
15347
+ },
15348
+ toolName,
15349
+ input,
15350
+ timestamp: entry.timestamp ?? "",
15351
+ project: projLabel,
15352
+ sessionId,
15353
+ agent: "claude"
15354
+ });
15355
+ }
15356
+ }
15357
+ }
14475
15358
  }
14476
15359
  }
14477
15360
  }
@@ -14521,6 +15404,7 @@ function scanGeminiHistory(startDate) {
14521
15404
  }
14522
15405
  for (const chatFile of chatFiles) {
14523
15406
  result.filesScanned++;
15407
+ const sessionId = chatFile.replace(/\.json$/, "");
14524
15408
  let raw;
14525
15409
  try {
14526
15410
  raw = fs33.readFileSync(path36.join(chatsDir, chatFile), "utf-8");
@@ -14574,10 +15458,12 @@ function scanGeminiHistory(startDate) {
14574
15458
  toolName,
14575
15459
  timestamp: msg.timestamp ?? "",
14576
15460
  project: projLabel,
15461
+ sessionId,
14577
15462
  agent: "gemini"
14578
15463
  });
14579
15464
  }
14580
15465
  }
15466
+ let ruleMatched = false;
14581
15467
  for (const source of ruleSources) {
14582
15468
  const { rule } = source;
14583
15469
  if (rule.verdict === "allow") continue;
@@ -14594,17 +15480,244 @@ function scanGeminiHistory(startDate) {
14594
15480
  input,
14595
15481
  timestamp: msg.timestamp ?? "",
14596
15482
  project: projLabel,
15483
+ sessionId,
14597
15484
  agent: "gemini"
14598
15485
  });
14599
15486
  }
15487
+ ruleMatched = true;
14600
15488
  break;
14601
15489
  }
15490
+ const isShellTool = ["bash", "execute_bash", "run_shell_command", "shell"].includes(
15491
+ toolNameLower
15492
+ );
15493
+ if (!ruleMatched && isShellTool) {
15494
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
15495
+ if (shellVerdict) {
15496
+ const astRule = {
15497
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
15498
+ tool: "bash",
15499
+ conditions: [],
15500
+ verdict: shellVerdict,
15501
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
15502
+ };
15503
+ const inputPreview = preview(input, 120);
15504
+ const isDupe = result.findings.some(
15505
+ (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
15506
+ );
15507
+ if (!isDupe) {
15508
+ result.findings.push({
15509
+ source: {
15510
+ shieldName: "bash-safe",
15511
+ shieldLabel: "bash-safe (AST)",
15512
+ sourceType: "shield",
15513
+ rule: astRule
15514
+ },
15515
+ toolName,
15516
+ input,
15517
+ timestamp: msg.timestamp ?? "",
15518
+ project: projLabel,
15519
+ sessionId,
15520
+ agent: "gemini"
15521
+ });
15522
+ }
15523
+ }
15524
+ }
14602
15525
  }
14603
15526
  }
14604
15527
  }
14605
15528
  }
14606
15529
  return result;
14607
15530
  }
15531
+ function scanCodexHistory(startDate) {
15532
+ const sessionsBase = path36.join(os29.homedir(), ".codex", "sessions");
15533
+ const result = {
15534
+ filesScanned: 0,
15535
+ sessions: 0,
15536
+ totalToolCalls: 0,
15537
+ bashCalls: 0,
15538
+ findings: [],
15539
+ dlpFindings: [],
15540
+ totalCostUSD: 0,
15541
+ firstDate: null,
15542
+ lastDate: null
15543
+ };
15544
+ if (!fs33.existsSync(sessionsBase)) return result;
15545
+ const jsonlFiles = [];
15546
+ try {
15547
+ for (const year of fs33.readdirSync(sessionsBase)) {
15548
+ const yearPath = path36.join(sessionsBase, year);
15549
+ try {
15550
+ if (!fs33.statSync(yearPath).isDirectory()) continue;
15551
+ } catch {
15552
+ continue;
15553
+ }
15554
+ for (const month of fs33.readdirSync(yearPath)) {
15555
+ const monthPath = path36.join(yearPath, month);
15556
+ try {
15557
+ if (!fs33.statSync(monthPath).isDirectory()) continue;
15558
+ } catch {
15559
+ continue;
15560
+ }
15561
+ for (const day of fs33.readdirSync(monthPath)) {
15562
+ const dayPath = path36.join(monthPath, day);
15563
+ try {
15564
+ if (!fs33.statSync(dayPath).isDirectory()) continue;
15565
+ } catch {
15566
+ continue;
15567
+ }
15568
+ for (const file of fs33.readdirSync(dayPath)) {
15569
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path36.join(dayPath, file));
15570
+ }
15571
+ }
15572
+ }
15573
+ }
15574
+ } catch {
15575
+ return result;
15576
+ }
15577
+ const ruleSources = buildRuleSources();
15578
+ for (const filePath of jsonlFiles) {
15579
+ result.filesScanned++;
15580
+ let lines;
15581
+ try {
15582
+ lines = fs33.readFileSync(filePath, "utf-8").split("\n");
15583
+ } catch {
15584
+ continue;
15585
+ }
15586
+ let sessionId = "";
15587
+ let startTime = "";
15588
+ let projLabel = "";
15589
+ result.sessions++;
15590
+ let lastTotalInput = 0;
15591
+ let lastTotalCached = 0;
15592
+ let lastTotalOutput = 0;
15593
+ for (const line of lines) {
15594
+ if (!line.trim()) continue;
15595
+ let entry;
15596
+ try {
15597
+ entry = JSON.parse(line);
15598
+ } catch {
15599
+ continue;
15600
+ }
15601
+ const payload = entry.payload ?? {};
15602
+ if (entry.type === "session_meta") {
15603
+ sessionId = String(payload["id"] ?? filePath);
15604
+ startTime = String(payload["timestamp"] ?? "");
15605
+ const cwd = String(payload["cwd"] ?? "");
15606
+ projLabel = cwd.replace(os29.homedir(), "~").slice(0, 40);
15607
+ continue;
15608
+ }
15609
+ if (entry.type === "event_msg" && payload["type"] === "token_count") {
15610
+ const info = payload["info"];
15611
+ const usage = info?.["total_token_usage"] ?? {};
15612
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
15613
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
15614
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
15615
+ continue;
15616
+ }
15617
+ if (entry.type !== "response_item") continue;
15618
+ if (payload["type"] !== "function_call") continue;
15619
+ const ts = startTime;
15620
+ if (startDate && ts && new Date(ts) < startDate) continue;
15621
+ if (ts) {
15622
+ if (!result.firstDate || ts < result.firstDate) result.firstDate = ts;
15623
+ if (!result.lastDate || ts > result.lastDate) result.lastDate = ts;
15624
+ }
15625
+ result.totalToolCalls++;
15626
+ const toolName = String(payload["name"] ?? "");
15627
+ const toolNameLower = toolName.toLowerCase();
15628
+ let input = {};
15629
+ try {
15630
+ input = JSON.parse(String(payload["arguments"] ?? "{}"));
15631
+ } catch {
15632
+ }
15633
+ if ("cmd" in input && !("command" in input)) {
15634
+ input = { ...input, command: input["cmd"] };
15635
+ }
15636
+ if (toolNameLower === "exec_command" || toolNameLower === "shell") {
15637
+ result.bashCalls++;
15638
+ }
15639
+ const rawCmd = String(input["command"] ?? "").trimStart();
15640
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
15641
+ const dlpMatch = scanArgs(input);
15642
+ if (dlpMatch) {
15643
+ const isDupe = result.dlpFindings.some(
15644
+ (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
15645
+ );
15646
+ if (!isDupe) {
15647
+ result.dlpFindings.push({
15648
+ patternName: dlpMatch.patternName,
15649
+ redactedSample: dlpMatch.redactedSample,
15650
+ toolName,
15651
+ timestamp: ts,
15652
+ project: projLabel,
15653
+ sessionId,
15654
+ agent: "codex"
15655
+ });
15656
+ }
15657
+ }
15658
+ let ruleMatched = false;
15659
+ for (const source of ruleSources) {
15660
+ const { rule } = source;
15661
+ if (rule.verdict === "allow") continue;
15662
+ if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
15663
+ continue;
15664
+ if (!evaluateSmartConditions(input, rule)) continue;
15665
+ const inputPreview = preview(input, 120);
15666
+ const isDupe = result.findings.some(
15667
+ (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
15668
+ );
15669
+ if (!isDupe) {
15670
+ result.findings.push({
15671
+ source,
15672
+ toolName,
15673
+ input,
15674
+ timestamp: ts,
15675
+ project: projLabel,
15676
+ sessionId,
15677
+ agent: "codex"
15678
+ });
15679
+ }
15680
+ ruleMatched = true;
15681
+ break;
15682
+ }
15683
+ if (!ruleMatched && (toolNameLower === "exec_command" || toolNameLower === "shell")) {
15684
+ const shellVerdict = detectDangerousShellExec(String(input["command"] ?? ""));
15685
+ if (shellVerdict) {
15686
+ const astRule = {
15687
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
15688
+ tool: "bash",
15689
+ conditions: [],
15690
+ verdict: shellVerdict,
15691
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
15692
+ };
15693
+ const inputPreview = preview(input, 120);
15694
+ const isDupe = result.findings.some(
15695
+ (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
15696
+ );
15697
+ if (!isDupe) {
15698
+ result.findings.push({
15699
+ source: {
15700
+ shieldName: "bash-safe",
15701
+ shieldLabel: "bash-safe (AST)",
15702
+ sourceType: "shield",
15703
+ rule: astRule
15704
+ },
15705
+ toolName,
15706
+ input,
15707
+ timestamp: ts,
15708
+ project: projLabel,
15709
+ sessionId,
15710
+ agent: "codex"
15711
+ });
15712
+ }
15713
+ }
15714
+ }
15715
+ }
15716
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
15717
+ result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
15718
+ }
15719
+ return result;
15720
+ }
14608
15721
  function mergeScans(a, b) {
14609
15722
  const dates = [a.firstDate, b.firstDate].filter(Boolean);
14610
15723
  const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
@@ -14620,22 +15733,67 @@ function mergeScans(a, b) {
14620
15733
  lastDate: lastDates.length ? lastDates.sort().at(-1) : null
14621
15734
  };
14622
15735
  }
15736
+ function verdictIcon(verdict) {
15737
+ return verdict === "block" ? "\u{1F6D1}" : "\u{1F441} ";
15738
+ }
15739
+ function printFindingRow(f, drillDown, showSessionId, previewWidth) {
15740
+ const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
15741
+ const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
15742
+ const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : f.agent === "codex" ? chalk21.magenta("[Codex] ") : chalk21.cyan("[Claude] ");
15743
+ const cmd = drillDown ? chalk21.gray(fullCommand(f.input)) : chalk21.gray(preview(f.input, previewWidth));
15744
+ const sessionSuffix = showSessionId && f.sessionId ? chalk21.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
15745
+ console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
15746
+ }
15747
+ function printRuleGroup(ruleFindings, topN, drillDown, previewWidth) {
15748
+ const rule = ruleFindings[0].source.rule;
15749
+ const ruleCount = ruleFindings.length;
15750
+ const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
15751
+ const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
15752
+ const icon = verdictIcon(rule.verdict ?? "review");
15753
+ console.log(
15754
+ " " + icon + " " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
15755
+ );
15756
+ const shown = drillDown ? ruleFindings : ruleFindings.slice(0, topN);
15757
+ for (const f of shown) {
15758
+ printFindingRow(f, drillDown, drillDown, previewWidth);
15759
+ }
15760
+ if (!drillDown && ruleFindings.length > topN) {
15761
+ console.log(
15762
+ chalk21.dim(` \u2026 and ${ruleFindings.length - topN} more (--drill-down for full list)`)
15763
+ );
15764
+ }
15765
+ }
14623
15766
  function registerScanCommand(program2) {
14624
- 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) => {
14625
- const topN = Math.max(1, parseInt(options.top, 10) || 5);
15767
+ program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").action((options) => {
15768
+ const drillDown = options.drillDown ?? false;
15769
+ const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
15770
+ const previewWidth = 70;
14626
15771
  const startDate = options.all ? null : (() => {
14627
15772
  const d = /* @__PURE__ */ new Date();
14628
15773
  d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
14629
15774
  d.setHours(0, 0, 0, 0);
14630
15775
  return d;
14631
15776
  })();
15777
+ const isInstalled = fs33.existsSync(path36.join(os29.homedir(), ".node9", "audit.log"));
14632
15778
  console.log("");
14633
- console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
15779
+ if (!isInstalled) {
15780
+ console.log(
15781
+ chalk21.bold("\u{1F6E1} node9") + chalk21.dim(" \u2014 security layer for AI coding agents")
15782
+ );
15783
+ console.log(
15784
+ chalk21.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
15785
+ );
15786
+ console.log("");
15787
+ }
15788
+ console.log(
15789
+ chalk21.cyan.bold("\u{1F50D} Scanning your AI history") + chalk21.dim(" \u2014 what would node9 have caught?")
15790
+ );
14634
15791
  console.log("");
14635
15792
  process.stdout.write(chalk21.dim(" Scanning\u2026"));
14636
15793
  const claudeScan = scanClaudeHistory(startDate);
14637
15794
  const geminiScan = scanGeminiHistory(startDate);
14638
- const scan = mergeScans(claudeScan, geminiScan);
15795
+ const codexScan = scanCodexHistory(startDate);
15796
+ const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
14639
15797
  process.stdout.write("\r" + " ".repeat(20) + "\r");
14640
15798
  if (scan.filesScanned === 0) {
14641
15799
  console.log(chalk21.yellow(" No session history found."));
@@ -14648,95 +15806,151 @@ function registerScanCommand(program2) {
14648
15806
  }
14649
15807
  const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
14650
15808
  const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
14651
- const sessionBreakdown = claudeScan.sessions > 0 && geminiScan.sessions > 0 ? chalk21.dim("(") + chalk21.cyan(String(claudeScan.sessions)) + chalk21.dim(" Claude \xB7 ") + chalk21.blue(String(geminiScan.sessions)) + chalk21.dim(" Gemini)") : "";
15809
+ const breakdownParts = [];
15810
+ if (claudeScan.sessions > 0)
15811
+ breakdownParts.push(chalk21.cyan(String(claudeScan.sessions)) + chalk21.dim(" Claude"));
15812
+ if (geminiScan.sessions > 0)
15813
+ breakdownParts.push(chalk21.blue(String(geminiScan.sessions)) + chalk21.dim(" Gemini"));
15814
+ if (codexScan.sessions > 0)
15815
+ breakdownParts.push(chalk21.magenta(String(codexScan.sessions)) + chalk21.dim(" Codex"));
15816
+ const sessionBreakdown = breakdownParts.length > 1 ? chalk21.dim("(") + breakdownParts.join(chalk21.dim(" \xB7 ")) + chalk21.dim(")") : "";
14652
15817
  console.log(
14653
15818
  " " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
14654
15819
  );
14655
15820
  console.log("");
14656
- const byShield = /* @__PURE__ */ new Map();
14657
- for (const f of scan.findings) {
14658
- const key = f.source.shieldName;
14659
- const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
14660
- entry.findings.push(f);
14661
- byShield.set(key, entry);
14662
- }
14663
15821
  const totalFindings = scan.findings.length;
15822
+ const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
15823
+ const reviewCount = totalFindings - blockedCount;
14664
15824
  if (totalFindings === 0 && scan.dlpFindings.length === 0) {
14665
- console.log(chalk21.green(" \u2705 No findings across all shields and rules."));
14666
- console.log(chalk21.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
15825
+ console.log(chalk21.green(" \u2705 No risky operations found in your history."));
15826
+ console.log(
15827
+ chalk21.dim(" node9 is still worth running \u2014 it monitors every tool call in real time.\n")
15828
+ );
14667
15829
  } else {
14668
- if (totalFindings > 0) {
15830
+ const totalRisky = totalFindings + scan.dlpFindings.length;
15831
+ const heroLine = isInstalled ? chalk21.bold(
15832
+ ` Found ${chalk21.yellow(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} in your history`
15833
+ ) : chalk21.bold(
15834
+ ` ${chalk21.red.bold(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} found \u2014 none were blocked`
15835
+ );
15836
+ console.log(heroLine);
15837
+ console.log("");
15838
+ if (blockedCount > 0) {
14669
15839
  console.log(
14670
- " " + chalk21.bold("If node9 had been installed:") + " " + chalk21.yellow.bold(
14671
- `${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
14672
- )
15840
+ " " + chalk21.red("\u{1F6D1} Would have blocked") + " " + chalk21.red.bold(String(blockedCount).padStart(5)) + chalk21.dim(" operations stopped before execution")
14673
15841
  );
14674
- console.log("");
14675
- const sorted = [...byShield.entries()].sort(
14676
- (a, b) => b[1].findings.length - a[1].findings.length
15842
+ }
15843
+ if (reviewCount > 0) {
15844
+ console.log(
15845
+ " " + chalk21.yellow("\u{1F441} Would have flagged") + " " + chalk21.yellow.bold(String(reviewCount).padStart(5)) + chalk21.dim(" sent to you for approval")
14677
15846
  );
14678
- for (const [shieldName, { label, findings }] of sorted) {
14679
- const count = findings.length;
14680
- const isUserRule = shieldName === "custom" || shieldName === "cloud";
14681
- const shieldBadge = isUserRule ? chalk21.magenta(label) : chalk21.cyan(label);
14682
- console.log(" " + chalk21.dim("\u2500".repeat(70)));
14683
- console.log(
14684
- " " + shieldBadge + chalk21.dim(" \xB7 ") + chalk21.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : chalk21.dim(` \u2192 node9 shield enable ${shieldName}`))
14685
- );
14686
- const byRule = /* @__PURE__ */ new Map();
14687
- for (const f of findings) {
14688
- const ruleKey = f.source.rule.name ?? "unnamed";
14689
- const arr = byRule.get(ruleKey) ?? [];
14690
- arr.push(f);
14691
- byRule.set(ruleKey, arr);
14692
- }
14693
- for (const [, ruleFindings] of byRule) {
14694
- const rule = ruleFindings[0].source.rule;
14695
- const ruleCount = ruleFindings.length;
14696
- const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
14697
- const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
14698
- console.log(
14699
- " " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
14700
- );
14701
- const shown = ruleFindings.slice(0, topN);
14702
- for (const f of shown) {
14703
- const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14704
- const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14705
- const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : chalk21.cyan("[Claude] ");
14706
- const cmd = chalk21.gray(preview(f.input, 45));
14707
- console.log(` ${ts}${proj}${agentBadge}${cmd}`);
14708
- }
14709
- if (ruleFindings.length > topN) {
14710
- console.log(
14711
- chalk21.dim(
14712
- ` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
14713
- )
14714
- );
14715
- }
14716
- }
14717
- console.log("");
15847
+ }
15848
+ if (scan.dlpFindings.length > 0) {
15849
+ console.log(
15850
+ " " + chalk21.red("\u{1F511} Credential leak") + " " + chalk21.red.bold(String(scan.dlpFindings.length).padStart(5)) + chalk21.dim(" secret detected in tool call")
15851
+ );
15852
+ }
15853
+ console.log("");
15854
+ const sections = [];
15855
+ const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
15856
+ if (defaultFindings.length > 0) {
15857
+ sections.push({
15858
+ label: "Default Rules",
15859
+ subtitle: "built-in, always on",
15860
+ findings: defaultFindings
15861
+ });
15862
+ }
15863
+ const byShield = /* @__PURE__ */ new Map();
15864
+ for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
15865
+ const arr = byShield.get(f.source.shieldName) ?? [];
15866
+ arr.push(f);
15867
+ byShield.set(f.source.shieldName, arr);
15868
+ }
15869
+ const shieldsWithFindings = [...byShield.entries()].sort(
15870
+ (a, b) => b[1].length - a[1].length
15871
+ );
15872
+ for (const [shieldName, findings] of shieldsWithFindings) {
15873
+ const description = SHIELDS[shieldName]?.description ?? "";
15874
+ sections.push({
15875
+ label: shieldName,
15876
+ subtitle: description,
15877
+ shieldKey: shieldName,
15878
+ findings
15879
+ });
15880
+ }
15881
+ const userFindings = scan.findings.filter(
15882
+ (f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
15883
+ );
15884
+ if (userFindings.length > 0) {
15885
+ sections.push({
15886
+ label: "Your Rules",
15887
+ subtitle: "added in node9.config.json",
15888
+ findings: userFindings
15889
+ });
15890
+ }
15891
+ for (const section of sections) {
15892
+ const sectionBlocked = section.findings.filter(
15893
+ (f) => f.source.rule.verdict === "block"
15894
+ ).length;
15895
+ const sectionReview = section.findings.length - sectionBlocked;
15896
+ const countParts = [];
15897
+ if (sectionBlocked > 0) countParts.push(chalk21.red(`${sectionBlocked} blocked`));
15898
+ if (sectionReview > 0) countParts.push(chalk21.yellow(`${sectionReview} review`));
15899
+ const countStr = countParts.join(chalk21.dim(" \xB7 "));
15900
+ const enableHint = section.shieldKey ? chalk21.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
15901
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
15902
+ console.log(
15903
+ " " + chalk21.bold(section.label) + (section.subtitle ? chalk21.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
15904
+ );
15905
+ const byRule = /* @__PURE__ */ new Map();
15906
+ for (const f of section.findings) {
15907
+ const ruleKey = f.source.rule.name ?? "unnamed";
15908
+ const arr = byRule.get(ruleKey) ?? [];
15909
+ arr.push(f);
15910
+ byRule.set(ruleKey, arr);
15911
+ }
15912
+ const sortedRules = [...byRule.entries()].sort((a, b) => {
15913
+ const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
15914
+ const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
15915
+ if (bBlock !== aBlock) return bBlock - aBlock;
15916
+ return b[1].length - a[1].length;
15917
+ });
15918
+ for (const [, ruleFindings] of sortedRules) {
15919
+ printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
14718
15920
  }
15921
+ console.log("");
15922
+ }
15923
+ const emptyShields = Object.keys(SHIELDS).filter((n) => !byShield.has(n)).sort();
15924
+ if (emptyShields.length > 0) {
15925
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
15926
+ console.log(
15927
+ " " + chalk21.bold("Shields") + chalk21.dim(" \xB7 no findings in your history") + " " + chalk21.green("\u2713")
15928
+ );
15929
+ console.log(" " + chalk21.dim(emptyShields.join(" \xB7 ")));
15930
+ console.log(" " + chalk21.dim("\u2192 node9 shield enable <name> to activate any shield"));
15931
+ console.log("");
14719
15932
  }
14720
15933
  if (scan.dlpFindings.length > 0) {
14721
15934
  console.log(" " + chalk21.dim("\u2500".repeat(70)));
14722
15935
  console.log(
14723
- " " + chalk21.red.bold("Secrets / DLP") + chalk21.dim(" \xB7 ") + chalk21.red(
15936
+ " " + chalk21.red.bold("\u{1F511} Credential Leaks") + chalk21.dim(" \xB7 ") + chalk21.red(
14724
15937
  `${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
14725
15938
  )
14726
15939
  );
14727
- const shownDlp = scan.dlpFindings.slice(0, topN);
15940
+ const shownDlp = drillDown ? scan.dlpFindings : scan.dlpFindings.slice(0, topN);
14728
15941
  for (const f of shownDlp) {
14729
15942
  const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14730
15943
  const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14731
- const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : chalk21.cyan("[Claude] ");
15944
+ const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : f.agent === "codex" ? chalk21.magenta("[Codex] ") : chalk21.cyan("[Claude] ");
15945
+ const sessionSuffix = f.sessionId ? chalk21.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
14732
15946
  console.log(
14733
- ` ${ts}${proj}${agentBadge}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
15947
+ ` ${ts}${proj}${agentBadge}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample) + sessionSuffix
14734
15948
  );
14735
15949
  }
14736
- if (scan.dlpFindings.length > topN) {
15950
+ if (!drillDown && scan.dlpFindings.length > topN) {
14737
15951
  console.log(
14738
15952
  chalk21.dim(
14739
- ` \u2026 and ${scan.dlpFindings.length - topN} more (--top ${scan.dlpFindings.length})`
15953
+ ` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
14740
15954
  )
14741
15955
  );
14742
15956
  }
@@ -14749,17 +15963,41 @@ function registerScanCommand(program2) {
14749
15963
  );
14750
15964
  console.log("");
14751
15965
  }
14752
- const auditLog = path36.join(os29.homedir(), ".node9", "audit.log");
14753
- if (fs33.existsSync(auditLog)) {
14754
- console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
15966
+ if (isInstalled) {
15967
+ console.log(chalk21.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
14755
15968
  console.log(
14756
- chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
15969
+ chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live protection stats.")
14757
15970
  );
15971
+ if (drillDown) {
15972
+ console.log(
15973
+ chalk21.dim(" Run ") + chalk21.cyan("node9 sessions --detail <session-id>") + chalk21.dim(" to see the full conversation for any session above.")
15974
+ );
15975
+ } else {
15976
+ console.log(
15977
+ chalk21.dim(" Run ") + chalk21.cyan("node9 scan --drill-down") + chalk21.dim(" to see full commands and session IDs.")
15978
+ );
15979
+ }
14758
15980
  } else {
14759
- console.log(chalk21.yellow.bold(" \u26A1 node9 was not running during these sessions."));
15981
+ const riskySummary = totalFindings + scan.dlpFindings.length;
15982
+ if (riskySummary > 0) {
15983
+ console.log(
15984
+ chalk21.yellow.bold(
15985
+ ` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
15986
+ ) + chalk21.dim(" node9 would have caught them.")
15987
+ );
15988
+ }
15989
+ console.log("");
15990
+ console.log(chalk21.bold(" Protect your next session in 30 seconds:"));
15991
+ console.log("");
15992
+ console.log(" " + chalk21.cyan("npm install -g @node9/proxy"));
15993
+ console.log(" " + chalk21.cyan("node9 init"));
15994
+ console.log("");
15995
+ console.log(chalk21.dim(" node9 hooks into Claude Code automatically."));
14760
15996
  console.log(
14761
- " " + chalk21.white("Run ") + chalk21.cyan("node9 init") + chalk21.white(" to start protecting your AI agents.")
15997
+ chalk21.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
14762
15998
  );
15999
+ console.log("");
16000
+ console.log(" " + chalk21.dim("\u2192 ") + chalk21.underline("https://node9.ai"));
14763
16001
  }
14764
16002
  console.log("");
14765
16003
  });
@@ -15026,6 +16264,7 @@ function buildGeminiSessions(days, allAuditEntries) {
15026
16264
  projectLabel: projectLabel(projectRoot),
15027
16265
  firstPrompt,
15028
16266
  startTime,
16267
+ lastActiveTime: lastToolTs || startTime,
15029
16268
  toolCalls,
15030
16269
  blockedCalls,
15031
16270
  costUSD,
@@ -15037,6 +16276,128 @@ function buildGeminiSessions(days, allAuditEntries) {
15037
16276
  }
15038
16277
  return summaries;
15039
16278
  }
16279
+ function buildCodexSessions(days, allAuditEntries) {
16280
+ const sessionsBase = path37.join(os30.homedir(), ".codex", "sessions");
16281
+ if (!fs34.existsSync(sessionsBase)) return [];
16282
+ const cutoff = days !== null ? (() => {
16283
+ const d = /* @__PURE__ */ new Date();
16284
+ d.setDate(d.getDate() - days);
16285
+ d.setHours(0, 0, 0, 0);
16286
+ return d;
16287
+ })() : null;
16288
+ const jsonlFiles = [];
16289
+ try {
16290
+ for (const year of fs34.readdirSync(sessionsBase)) {
16291
+ const yearPath = path37.join(sessionsBase, year);
16292
+ try {
16293
+ if (!fs34.statSync(yearPath).isDirectory()) continue;
16294
+ } catch {
16295
+ continue;
16296
+ }
16297
+ for (const month of fs34.readdirSync(yearPath)) {
16298
+ const monthPath = path37.join(yearPath, month);
16299
+ try {
16300
+ if (!fs34.statSync(monthPath).isDirectory()) continue;
16301
+ } catch {
16302
+ continue;
16303
+ }
16304
+ for (const day of fs34.readdirSync(monthPath)) {
16305
+ const dayPath = path37.join(monthPath, day);
16306
+ try {
16307
+ if (!fs34.statSync(dayPath).isDirectory()) continue;
16308
+ } catch {
16309
+ continue;
16310
+ }
16311
+ for (const file of fs34.readdirSync(dayPath)) {
16312
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path37.join(dayPath, file));
16313
+ }
16314
+ }
16315
+ }
16316
+ }
16317
+ } catch {
16318
+ return [];
16319
+ }
16320
+ const summaries = [];
16321
+ for (const filePath of jsonlFiles) {
16322
+ let lines;
16323
+ try {
16324
+ lines = fs34.readFileSync(filePath, "utf-8").split("\n");
16325
+ } catch {
16326
+ continue;
16327
+ }
16328
+ let sessionId = "";
16329
+ let startTime = "";
16330
+ let cwd = "";
16331
+ let firstPrompt = "";
16332
+ const toolCalls = [];
16333
+ let lastToolTs = "";
16334
+ let lastTotalInput = 0;
16335
+ let lastTotalCached = 0;
16336
+ let lastTotalOutput = 0;
16337
+ for (const line of lines) {
16338
+ if (!line.trim()) continue;
16339
+ let entry;
16340
+ try {
16341
+ entry = JSON.parse(line);
16342
+ } catch {
16343
+ continue;
16344
+ }
16345
+ const p = entry.payload ?? {};
16346
+ if (entry.type === "session_meta") {
16347
+ sessionId = String(p["id"] ?? "");
16348
+ startTime = String(p["timestamp"] ?? "");
16349
+ cwd = String(p["cwd"] ?? "");
16350
+ continue;
16351
+ }
16352
+ if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
16353
+ firstPrompt = String(p["message"] ?? "");
16354
+ continue;
16355
+ }
16356
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
16357
+ const info = p["info"] ?? {};
16358
+ const usage = info["total_token_usage"] ?? {};
16359
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16360
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16361
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16362
+ continue;
16363
+ }
16364
+ if (entry.type === "response_item" && p["type"] === "function_call") {
16365
+ const tool = String(p["name"] ?? "");
16366
+ let input = {};
16367
+ try {
16368
+ input = JSON.parse(String(p["arguments"] ?? "{}"));
16369
+ } catch {
16370
+ }
16371
+ const ts = entry.timestamp ?? startTime;
16372
+ toolCalls.push({ tool, input, timestamp: ts });
16373
+ if (ts > lastToolTs) lastToolTs = ts;
16374
+ }
16375
+ }
16376
+ if (!sessionId || !startTime) continue;
16377
+ if (cutoff && new Date(startTime) < cutoff) continue;
16378
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16379
+ const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16380
+ const windowEnd = new Date(
16381
+ Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
16382
+ ).toISOString();
16383
+ const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
16384
+ summaries.push({
16385
+ sessionId,
16386
+ project: cwd,
16387
+ projectLabel: projectLabel(cwd),
16388
+ firstPrompt,
16389
+ startTime,
16390
+ lastActiveTime: lastToolTs || startTime,
16391
+ toolCalls,
16392
+ blockedCalls,
16393
+ costUSD,
16394
+ hasSnapshot: false,
16395
+ modifiedFiles: [],
16396
+ agent: "codex"
16397
+ });
16398
+ }
16399
+ return summaries;
16400
+ }
15040
16401
  function buildSessions(days, historyPath) {
15041
16402
  const hPath = historyPath ?? path37.join(os30.homedir(), ".claude", "history.jsonl");
15042
16403
  let historyRaw;
@@ -15077,12 +16438,14 @@ function buildSessions(days, historyPath) {
15077
16438
  // 5 min buffer
15078
16439
  ).toISOString();
15079
16440
  const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
16441
+ const lastActiveTime = lastToolTs || entry.timestamp;
15080
16442
  summaries.push({
15081
16443
  sessionId: entry.sessionId,
15082
16444
  project: entry.project,
15083
16445
  projectLabel: projectLabel(entry.project),
15084
16446
  firstPrompt: entry.display,
15085
16447
  startTime: entry.timestamp,
16448
+ lastActiveTime,
15086
16449
  toolCalls,
15087
16450
  blockedCalls,
15088
16451
  costUSD,
@@ -15093,8 +16456,9 @@ function buildSessions(days, historyPath) {
15093
16456
  }
15094
16457
  if (!historyPath) {
15095
16458
  summaries.push(...buildGeminiSessions(days, allAuditEntries));
16459
+ summaries.push(...buildCodexSessions(days, allAuditEntries));
15096
16460
  }
15097
- summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
16461
+ summaries.sort((a, b) => a.lastActiveTime > b.lastActiveTime ? -1 : 1);
15098
16462
  return summaries;
15099
16463
  }
15100
16464
  function fmtCost3(usd) {
@@ -15236,22 +16600,25 @@ function renderList(summaries, totalCost) {
15236
16600
  console.log("");
15237
16601
  let lastGroup = "";
15238
16602
  for (const s of summaries) {
15239
- const group = fmtDate2(s.startTime) + " " + s.projectLabel;
16603
+ const activeDate = fmtDate2(s.lastActiveTime);
16604
+ const group = activeDate + " " + s.projectLabel;
15240
16605
  if (group !== lastGroup) {
15241
- console.log(
15242
- chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
15243
- );
16606
+ console.log(chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(activeDate) + chalk22.dim(" " + s.projectLabel));
15244
16607
  lastGroup = group;
15245
16608
  }
16609
+ const startDate = fmtDate2(s.startTime);
16610
+ const dateRange = startDate !== activeDate ? chalk22.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
15246
16611
  const timeStr = chalk22.dim(fmtTime(s.startTime));
15247
16612
  const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
15248
16613
  const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
15249
16614
  const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
15250
16615
  const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
15251
16616
  const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
15252
- const agentBadge = s.agent === "gemini" ? chalk22.blue(" [Gemini]") : chalk22.cyan(" [Claude]");
16617
+ const agentBadge = s.agent === "gemini" ? chalk22.blue(" [Gemini]") : s.agent === "codex" ? chalk22.magenta(" [Codex]") : chalk22.cyan(" [Claude]");
15253
16618
  const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
15254
- console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}`);
16619
+ console.log(
16620
+ ` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
16621
+ );
15255
16622
  }
15256
16623
  console.log("");
15257
16624
  console.log(
@@ -15267,7 +16634,7 @@ function renderDetail(s) {
15267
16634
  );
15268
16635
  console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
15269
16636
  if (s.agent) {
15270
- const agentLabel2 = s.agent === "gemini" ? chalk22.blue("Gemini CLI") : chalk22.cyan("Claude Code");
16637
+ const agentLabel2 = s.agent === "gemini" ? chalk22.blue("Gemini CLI") : s.agent === "codex" ? chalk22.magenta("Codex") : chalk22.cyan("Claude Code");
15271
16638
  console.log(chalk22.bold(" Agent ") + agentLabel2);
15272
16639
  }
15273
16640
  console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
@@ -15338,7 +16705,12 @@ function registerSessionsCommand(program2) {
15338
16705
  console.log("");
15339
16706
  process.stdout.write(chalk22.dim(" Loading\u2026"));
15340
16707
  const summaries = buildSessions(days);
15341
- process.stdout.write("\r" + " ".repeat(20) + "\r");
16708
+ if (process.stdout.isTTY) {
16709
+ process.stdout.clearLine(0);
16710
+ process.stdout.cursorTo(0);
16711
+ } else {
16712
+ process.stdout.write("\n");
16713
+ }
15342
16714
  if (options.detail) {
15343
16715
  const target = summaries.find(
15344
16716
  (s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
@@ -15977,10 +17349,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
15977
17349
  program.help();
15978
17350
  return;
15979
17351
  }
15980
- const fullCommand = runArgs.join(" ");
17352
+ const fullCommand2 = runArgs.join(" ");
15981
17353
  let result = await authorizeHeadless(
15982
17354
  "shell",
15983
- { command: fullCommand },
17355
+ { command: fullCommand2 },
15984
17356
  {
15985
17357
  agent: "Terminal"
15986
17358
  }
@@ -15988,11 +17360,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
15988
17360
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
15989
17361
  console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
15990
17362
  const daemonReady = await autoStartDaemonAndWait();
15991
- if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
17363
+ if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
15992
17364
  }
15993
17365
  if (result.noApprovalMechanism && process.stdout.isTTY) {
15994
17366
  const approved = await confirm2({
15995
- message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
17367
+ message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
15996
17368
  default: false
15997
17369
  });
15998
17370
  result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
@@ -16005,7 +17377,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
16005
17377
  process.exit(1);
16006
17378
  }
16007
17379
  console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
16008
- await runProxy(fullCommand);
17380
+ await runProxy(fullCommand2);
16009
17381
  } else {
16010
17382
  program.help();
16011
17383
  }