@node9/proxy 1.11.3 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -31
- package/dist/cli.js +1602 -230
- package/dist/cli.mjs +1596 -224
- package/dist/index.js +465 -75
- package/dist/index.mjs +465 -75
- package/dist/shields/builtin/bash-safe.json +18 -4
- package/package.json +2 -2
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:
|
|
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: "
|
|
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: "
|
|
888
|
-
reason: "Force push
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
1185
|
-
regex:
|
|
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
|
-
//
|
|
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_
|
|
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
|
-
|
|
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
|
|
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
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1867
|
-
|
|
1868
|
-
if (!node) return;
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
11057
|
-
|
|
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
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
12876
|
+
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
12217
12877
|
console.log(" " + chalk9.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
12218
12878
|
}
|
|
12219
|
-
if (
|
|
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.
|
|
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: {
|
|
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
|
|
13808
|
-
const
|
|
13809
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
14570
|
+
parsed.push({ raw: line, decision: "allow", formatted: line });
|
|
13815
14571
|
}
|
|
13816
|
-
}
|
|
13817
|
-
|
|
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
|
-
${
|
|
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
|
|
13873
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14625
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
14666
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
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
|
-
|
|
14679
|
-
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
14753
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
17352
|
+
const fullCommand2 = runArgs.join(" ");
|
|
15981
17353
|
let result = await authorizeHeadless(
|
|
15982
17354
|
"shell",
|
|
15983
|
-
{ command:
|
|
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:
|
|
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 "${
|
|
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(
|
|
17380
|
+
await runProxy(fullCommand2);
|
|
16009
17381
|
} else {
|
|
16010
17382
|
program.help();
|
|
16011
17383
|
}
|