@node9/proxy 1.19.4 → 1.20.1
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 +57 -102
- package/dist/cli.js +1660 -925
- package/dist/cli.mjs +1660 -924
- package/dist/dashboard.mjs +3503 -1343
- package/dist/index.js +127 -13
- package/dist/index.mjs +127 -13
- package/package.json +3 -1
package/dist/cli.mjs
CHANGED
|
@@ -99,12 +99,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
99
99
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
100
100
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
101
101
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
102
|
+
const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
|
|
102
103
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
103
104
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
105
|
tool: toolName,
|
|
105
106
|
...argsField,
|
|
106
107
|
decision,
|
|
107
108
|
checkedBy,
|
|
109
|
+
...ruleNameField,
|
|
108
110
|
...testRun,
|
|
109
111
|
agent: meta?.agent,
|
|
110
112
|
mcpServer: meta?.mcpServer,
|
|
@@ -691,7 +693,12 @@ function analyzeFsOperationImpl(command) {
|
|
|
691
693
|
for (const p of paths) {
|
|
692
694
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
693
695
|
if (sp.match(p)) {
|
|
694
|
-
result = {
|
|
696
|
+
result = {
|
|
697
|
+
ruleName: sp.rule,
|
|
698
|
+
verdict: sp.verdict ?? "block",
|
|
699
|
+
reason: sp.reason,
|
|
700
|
+
path: p
|
|
701
|
+
};
|
|
695
702
|
return false;
|
|
696
703
|
}
|
|
697
704
|
}
|
|
@@ -2345,15 +2352,50 @@ var init_dist = __esm({
|
|
|
2345
2352
|
match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
|
|
2346
2353
|
},
|
|
2347
2354
|
{
|
|
2355
|
+
// Mirrors the JSON shield's `.env` pattern (project-jail.json's
|
|
2356
|
+
// review-read-env-any-tool) so the AST FS-op path catches the
|
|
2357
|
+
// same set the regex shield does — including Next.js / Vite's
|
|
2358
|
+
// `.env.<env>.local` double-suffix overrides which are commonly
|
|
2359
|
+
// gitignored AND commonly contain real secrets.
|
|
2360
|
+
//
|
|
2361
|
+
// Intentional non-matches (dev fixtures): .env.example, .env.sample,
|
|
2362
|
+
// .env.template, .env.test, .envrc. See shields.test.ts:983-995
|
|
2363
|
+
// for the canonical test-asserted contract.
|
|
2348
2364
|
rule: "shield:project-jail:block-read-env",
|
|
2349
2365
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
2350
|
-
match: (p) => /(?:^|[\\/])\.env(?:\.local
|
|
2366
|
+
match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
|
|
2367
|
+
p
|
|
2368
|
+
)
|
|
2351
2369
|
},
|
|
2352
2370
|
{
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2371
|
+
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
2372
|
+
// documented in commit 29327a8. SSH keys and AWS credentials are
|
|
2373
|
+
// cryptographic material with no legitimate read use-case for
|
|
2374
|
+
// an AI agent → hard `block`. But .netrc / .npmrc / .docker /
|
|
2375
|
+
// .kube / gcloud are CONFIG files that hold tokens AND have
|
|
2376
|
+
// legitimate diagnostic reads ("which registry am I configured
|
|
2377
|
+
// for", "what cluster am I on"). Hard-blocking those creates
|
|
2378
|
+
// friction without much safety win because the review gate
|
|
2379
|
+
// still catches genuine exfiltration attempts.
|
|
2380
|
+
//
|
|
2381
|
+
// The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
|
|
2382
|
+
// returns a deny verdict via the orchestrator's timeout branch),
|
|
2383
|
+
// so a stuck or unattended approval does NOT silently grant
|
|
2384
|
+
// credential access. If the threat model demands strict block,
|
|
2385
|
+
// a future per-shield strict-mode toggle is the right fix —
|
|
2386
|
+
// not a regex-level upgrade here.
|
|
2387
|
+
rule: "shield:project-jail:review-read-credentials",
|
|
2388
|
+
reason: "Reading credential files requires approval (project-jail shield)",
|
|
2389
|
+
verdict: "review",
|
|
2390
|
+
match: (p) => (
|
|
2391
|
+
// .kube/config holds Kubernetes cluster credentials and was
|
|
2392
|
+
// flagged as missing by the node9-pr-agent review (the comment
|
|
2393
|
+
// above mentioned .kube but the regex didn't include it — a
|
|
2394
|
+
// textbook code-comment vs code drift). The JSON shield's
|
|
2395
|
+
// review-read-credentials-any-tool already had it. Now aligned.
|
|
2396
|
+
/(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
|
|
2397
|
+
p
|
|
2398
|
+
)
|
|
2357
2399
|
)
|
|
2358
2400
|
}
|
|
2359
2401
|
];
|
|
@@ -2369,7 +2411,7 @@ var init_dist = __esm({
|
|
|
2369
2411
|
"shield:project-jail:block-read-ssh",
|
|
2370
2412
|
"shield:project-jail:block-read-aws",
|
|
2371
2413
|
"shield:project-jail:block-read-env",
|
|
2372
|
-
"shield:project-jail:
|
|
2414
|
+
"shield:project-jail:review-read-credentials"
|
|
2373
2415
|
]);
|
|
2374
2416
|
FS_OP_CACHE_MAX = 5e3;
|
|
2375
2417
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
@@ -3057,7 +3099,7 @@ var init_dist = __esm({
|
|
|
3057
3099
|
{
|
|
3058
3100
|
field: "command",
|
|
3059
3101
|
op: "matches",
|
|
3060
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s
|
|
3102
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
|
|
3061
3103
|
flags: "i"
|
|
3062
3104
|
}
|
|
3063
3105
|
],
|
|
@@ -3071,7 +3113,7 @@ var init_dist = __esm({
|
|
|
3071
3113
|
{
|
|
3072
3114
|
field: "command",
|
|
3073
3115
|
op: "matches",
|
|
3074
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s
|
|
3116
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
|
|
3075
3117
|
flags: "i"
|
|
3076
3118
|
}
|
|
3077
3119
|
],
|
|
@@ -3093,7 +3135,7 @@ var init_dist = __esm({
|
|
|
3093
3135
|
reason: "Reading .env files is blocked by project-jail shield"
|
|
3094
3136
|
},
|
|
3095
3137
|
{
|
|
3096
|
-
name: "shield:project-jail:
|
|
3138
|
+
name: "shield:project-jail:review-read-credentials",
|
|
3097
3139
|
tool: "bash",
|
|
3098
3140
|
conditions: [
|
|
3099
3141
|
{
|
|
@@ -3103,8 +3145,64 @@ var init_dist = __esm({
|
|
|
3103
3145
|
flags: "i"
|
|
3104
3146
|
}
|
|
3105
3147
|
],
|
|
3148
|
+
verdict: "review",
|
|
3149
|
+
reason: "Reading credential files requires approval (project-jail shield)"
|
|
3150
|
+
},
|
|
3151
|
+
{
|
|
3152
|
+
name: "shield:project-jail:block-read-ssh-any-tool",
|
|
3153
|
+
tool: "*",
|
|
3154
|
+
conditions: [
|
|
3155
|
+
{
|
|
3156
|
+
field: "file_path",
|
|
3157
|
+
op: "matches",
|
|
3158
|
+
value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
|
|
3159
|
+
flags: "i"
|
|
3160
|
+
}
|
|
3161
|
+
],
|
|
3162
|
+
verdict: "block",
|
|
3163
|
+
reason: "Reading SSH private keys is blocked by project-jail shield"
|
|
3164
|
+
},
|
|
3165
|
+
{
|
|
3166
|
+
name: "shield:project-jail:block-read-aws-any-tool",
|
|
3167
|
+
tool: "*",
|
|
3168
|
+
conditions: [
|
|
3169
|
+
{
|
|
3170
|
+
field: "file_path",
|
|
3171
|
+
op: "matches",
|
|
3172
|
+
value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
|
|
3173
|
+
flags: "i"
|
|
3174
|
+
}
|
|
3175
|
+
],
|
|
3106
3176
|
verdict: "block",
|
|
3107
|
-
reason: "Reading
|
|
3177
|
+
reason: "Reading AWS credentials is blocked by project-jail shield"
|
|
3178
|
+
},
|
|
3179
|
+
{
|
|
3180
|
+
name: "shield:project-jail:review-read-env-any-tool",
|
|
3181
|
+
tool: "*",
|
|
3182
|
+
conditions: [
|
|
3183
|
+
{
|
|
3184
|
+
field: "file_path",
|
|
3185
|
+
op: "matches",
|
|
3186
|
+
value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
|
|
3187
|
+
flags: "i"
|
|
3188
|
+
}
|
|
3189
|
+
],
|
|
3190
|
+
verdict: "review",
|
|
3191
|
+
reason: "Reading .env files requires approval (project-jail shield)"
|
|
3192
|
+
},
|
|
3193
|
+
{
|
|
3194
|
+
name: "shield:project-jail:review-read-credentials-any-tool",
|
|
3195
|
+
tool: "*",
|
|
3196
|
+
conditions: [
|
|
3197
|
+
{
|
|
3198
|
+
field: "file_path",
|
|
3199
|
+
op: "matches",
|
|
3200
|
+
value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
|
|
3201
|
+
flags: "i"
|
|
3202
|
+
}
|
|
3203
|
+
],
|
|
3204
|
+
verdict: "review",
|
|
3205
|
+
reason: "Reading credential files requires approval (project-jail shield)"
|
|
3108
3206
|
}
|
|
3109
3207
|
],
|
|
3110
3208
|
dangerousWords: []
|
|
@@ -5770,7 +5868,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
5770
5868
|
args,
|
|
5771
5869
|
"deny",
|
|
5772
5870
|
"smart-rule-block-override",
|
|
5773
|
-
|
|
5871
|
+
// Same rationale as the smart-rule-block path above —
|
|
5872
|
+
// pass the specific rule name so [2] SHIELDS can
|
|
5873
|
+
// attribute this override-block to its owning shield.
|
|
5874
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5774
5875
|
hashAuditArgs
|
|
5775
5876
|
);
|
|
5776
5877
|
if (approvers.cloud && creds?.apiKey)
|
|
@@ -5800,7 +5901,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
5800
5901
|
}
|
|
5801
5902
|
} else {
|
|
5802
5903
|
if (!isManual)
|
|
5803
|
-
appendLocalAudit(
|
|
5904
|
+
appendLocalAudit(
|
|
5905
|
+
toolName,
|
|
5906
|
+
args,
|
|
5907
|
+
"deny",
|
|
5908
|
+
"smart-rule-block",
|
|
5909
|
+
// Include policyResult.ruleName so the [2] Report SHIELDS
|
|
5910
|
+
// panel can attribute this block to its specific shield
|
|
5911
|
+
// (e.g. `shield:project-jail:block-read-ssh`) via the
|
|
5912
|
+
// rule→shield map. checkedBy stays as the generic
|
|
5913
|
+
// `smart-rule-block` for backward compat with existing
|
|
5914
|
+
// log readers.
|
|
5915
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5916
|
+
hashAuditArgs
|
|
5917
|
+
);
|
|
5804
5918
|
if (approvers.cloud && creds?.apiKey)
|
|
5805
5919
|
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
|
|
5806
5920
|
ruleName: policyResult.ruleName,
|
|
@@ -7594,6 +7708,7 @@ var init_blast = __esm({
|
|
|
7594
7708
|
|
|
7595
7709
|
// src/cli/render/scan-derive.ts
|
|
7596
7710
|
import chalk3 from "chalk";
|
|
7711
|
+
import stringWidth from "string-width";
|
|
7597
7712
|
function classifyScore(score) {
|
|
7598
7713
|
if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
|
|
7599
7714
|
if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
|
|
@@ -7621,9 +7736,62 @@ function computeLoopWaste(loops, totalToolCalls) {
|
|
|
7621
7736
|
const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
|
|
7622
7737
|
return { wastedCalls, wastePct };
|
|
7623
7738
|
}
|
|
7739
|
+
function rollupByShield(sections, topRulesPerShield = 3) {
|
|
7740
|
+
const out = [];
|
|
7741
|
+
for (const section of sections) {
|
|
7742
|
+
if (section.sourceType !== "shield") continue;
|
|
7743
|
+
if (!section.shieldKey) continue;
|
|
7744
|
+
const totalCatches = section.blockedCount + section.reviewCount;
|
|
7745
|
+
const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
|
|
7746
|
+
out.push({
|
|
7747
|
+
shieldName: section.shieldKey,
|
|
7748
|
+
totalCatches,
|
|
7749
|
+
blockCatches: section.blockedCount,
|
|
7750
|
+
reviewCatches: section.reviewCount,
|
|
7751
|
+
topRuleLabels
|
|
7752
|
+
});
|
|
7753
|
+
}
|
|
7754
|
+
return out.sort((a, b) => b.totalCatches - a.totalCatches);
|
|
7755
|
+
}
|
|
7756
|
+
function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
|
|
7757
|
+
const inner = width - 4;
|
|
7758
|
+
const out = [];
|
|
7759
|
+
const titlePad = ` ${title} `;
|
|
7760
|
+
const titleWidth = stringWidth(titlePad);
|
|
7761
|
+
const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
|
|
7762
|
+
const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
|
|
7763
|
+
out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
|
|
7764
|
+
for (const line of bodyLines) {
|
|
7765
|
+
const padding = " ".repeat(Math.max(0, inner - line.width));
|
|
7766
|
+
out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
|
|
7767
|
+
}
|
|
7768
|
+
out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
|
|
7769
|
+
return out;
|
|
7770
|
+
}
|
|
7771
|
+
function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
7772
|
+
const t = new Date(timestamp).getTime();
|
|
7773
|
+
if (Number.isNaN(t)) return "?";
|
|
7774
|
+
const days = Math.floor((now.getTime() - t) / 864e5);
|
|
7775
|
+
if (days < 1) return "today";
|
|
7776
|
+
if (days > 90) return "90d+";
|
|
7777
|
+
return `${days}d`;
|
|
7778
|
+
}
|
|
7779
|
+
var PANEL_WIDTH;
|
|
7624
7780
|
var init_scan_derive = __esm({
|
|
7625
7781
|
"src/cli/render/scan-derive.ts"() {
|
|
7626
7782
|
"use strict";
|
|
7783
|
+
PANEL_WIDTH = 76;
|
|
7784
|
+
}
|
|
7785
|
+
});
|
|
7786
|
+
|
|
7787
|
+
// src/protection.ts
|
|
7788
|
+
var PROTECTIVE_SHIELD_DISCOUNTS;
|
|
7789
|
+
var init_protection = __esm({
|
|
7790
|
+
"src/protection.ts"() {
|
|
7791
|
+
"use strict";
|
|
7792
|
+
PROTECTIVE_SHIELD_DISCOUNTS = {
|
|
7793
|
+
"project-jail": 0.7
|
|
7794
|
+
};
|
|
7627
7795
|
}
|
|
7628
7796
|
});
|
|
7629
7797
|
|
|
@@ -7813,6 +7981,7 @@ async function ensurePricingLoaded() {
|
|
|
7813
7981
|
if (fromDisk && Object.keys(fromDisk).length > 0) {
|
|
7814
7982
|
memCache = fromDisk;
|
|
7815
7983
|
memCacheAt = Date.now();
|
|
7984
|
+
lookupCache.clear();
|
|
7816
7985
|
return;
|
|
7817
7986
|
}
|
|
7818
7987
|
const fetched = await fetchLiteLLMPricing();
|
|
@@ -7820,30 +7989,42 @@ async function ensurePricingLoaded() {
|
|
|
7820
7989
|
memCache = fetched;
|
|
7821
7990
|
memCacheAt = Date.now();
|
|
7822
7991
|
writeCache(fetched);
|
|
7992
|
+
lookupCache.clear();
|
|
7823
7993
|
return;
|
|
7824
7994
|
}
|
|
7825
7995
|
memCache = { ...BUNDLED_PRICING };
|
|
7826
7996
|
memCacheAt = Date.now();
|
|
7997
|
+
lookupCache.clear();
|
|
7827
7998
|
}
|
|
7828
7999
|
function pricingFor(model) {
|
|
7829
8000
|
const norm = normalizeModel(model);
|
|
8001
|
+
const cached = lookupCache.get(norm);
|
|
8002
|
+
if (cached !== void 0) return cached;
|
|
7830
8003
|
const sources = [];
|
|
7831
8004
|
if (memCache) sources.push(memCache);
|
|
7832
8005
|
sources.push(BUNDLED_PRICING);
|
|
8006
|
+
let resolved = null;
|
|
7833
8007
|
for (const source of sources) {
|
|
7834
8008
|
const exact = source[norm];
|
|
7835
|
-
if (exact)
|
|
8009
|
+
if (exact) {
|
|
8010
|
+
resolved = exact;
|
|
8011
|
+
break;
|
|
8012
|
+
}
|
|
7836
8013
|
let best = null;
|
|
7837
8014
|
for (const key of Object.keys(source)) {
|
|
7838
8015
|
if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
|
|
7839
8016
|
best = key;
|
|
7840
8017
|
}
|
|
7841
8018
|
}
|
|
7842
|
-
if (best)
|
|
8019
|
+
if (best) {
|
|
8020
|
+
resolved = source[best];
|
|
8021
|
+
break;
|
|
8022
|
+
}
|
|
7843
8023
|
}
|
|
7844
|
-
|
|
8024
|
+
lookupCache.set(norm, resolved);
|
|
8025
|
+
return resolved;
|
|
7845
8026
|
}
|
|
7846
|
-
var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt;
|
|
8027
|
+
var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
|
|
7847
8028
|
var init_litellm = __esm({
|
|
7848
8029
|
"src/pricing/litellm.ts"() {
|
|
7849
8030
|
"use strict";
|
|
@@ -7877,6 +8058,7 @@ var init_litellm = __esm({
|
|
|
7877
8058
|
TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7878
8059
|
memCache = null;
|
|
7879
8060
|
memCacheAt = 0;
|
|
8061
|
+
lookupCache = /* @__PURE__ */ new Map();
|
|
7880
8062
|
}
|
|
7881
8063
|
});
|
|
7882
8064
|
|
|
@@ -7946,7 +8128,7 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
|
|
|
7946
8128
|
}
|
|
7947
8129
|
return daily;
|
|
7948
8130
|
}
|
|
7949
|
-
function collectEntries() {
|
|
8131
|
+
function collectEntries(sinceMs) {
|
|
7950
8132
|
const projectsDir = path18.join(os15.homedir(), ".claude", "projects");
|
|
7951
8133
|
if (!fs16.existsSync(projectsDir)) return [];
|
|
7952
8134
|
const combined = /* @__PURE__ */ new Map();
|
|
@@ -7971,7 +8153,15 @@ function collectEntries() {
|
|
|
7971
8153
|
}
|
|
7972
8154
|
const fallbackWorkingDir = decodeProjectDirName(dir);
|
|
7973
8155
|
for (const file of files) {
|
|
7974
|
-
const
|
|
8156
|
+
const filePath = path18.join(dirPath, file);
|
|
8157
|
+
if (sinceMs !== void 0) {
|
|
8158
|
+
try {
|
|
8159
|
+
if (fs16.statSync(filePath).mtimeMs < sinceMs) continue;
|
|
8160
|
+
} catch {
|
|
8161
|
+
continue;
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
const entries = parseJSONLFile(filePath, fallbackWorkingDir);
|
|
7975
8165
|
for (const [key, e] of entries) {
|
|
7976
8166
|
const prev = combined.get(key);
|
|
7977
8167
|
if (prev) {
|
|
@@ -8715,6 +8905,7 @@ import chalk5 from "chalk";
|
|
|
8715
8905
|
import fs19 from "fs";
|
|
8716
8906
|
import path21 from "path";
|
|
8717
8907
|
import os18 from "os";
|
|
8908
|
+
import stringWidth2 from "string-width";
|
|
8718
8909
|
function claudeModelPrice(model) {
|
|
8719
8910
|
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
8720
8911
|
for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
|
|
@@ -8807,7 +8998,16 @@ function buildRecurringPatternSet(findings) {
|
|
|
8807
8998
|
}
|
|
8808
8999
|
return recurring;
|
|
8809
9000
|
}
|
|
8810
|
-
function
|
|
9001
|
+
function emptyScanDedup() {
|
|
9002
|
+
return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
|
|
9003
|
+
}
|
|
9004
|
+
function findingKey(ruleName, inputPreview, projLabel) {
|
|
9005
|
+
return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
|
|
9006
|
+
}
|
|
9007
|
+
function dlpKey(patternName, redactedSample, projLabel) {
|
|
9008
|
+
return `${patternName}|${redactedSample}|${projLabel}`;
|
|
9009
|
+
}
|
|
9010
|
+
function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
|
|
8811
9011
|
const fsVerdict = analyzeFsOperation(command);
|
|
8812
9012
|
if (!fsVerdict) return false;
|
|
8813
9013
|
const synthRule = {
|
|
@@ -8830,10 +9030,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
|
|
|
8830
9030
|
rule: synthRule
|
|
8831
9031
|
};
|
|
8832
9032
|
const inputPreview = preview(input, 120);
|
|
8833
|
-
const
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
if (!isDupe) {
|
|
9033
|
+
const k = findingKey(synthRule.name, inputPreview, projLabel);
|
|
9034
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9035
|
+
dedup.findingsKeys.add(k);
|
|
8837
9036
|
result.findings.push({
|
|
8838
9037
|
source: synthSource,
|
|
8839
9038
|
toolName,
|
|
@@ -8918,22 +9117,15 @@ function buildRuleSources() {
|
|
|
8918
9117
|
sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
|
|
8919
9118
|
}
|
|
8920
9119
|
}
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
|
|
8931
|
-
shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
|
|
8932
|
-
sourceType,
|
|
8933
|
-
rule
|
|
8934
|
-
});
|
|
8935
|
-
}
|
|
8936
|
-
} catch {
|
|
9120
|
+
for (const rule of DEFAULT_CONFIG.policy.smartRules) {
|
|
9121
|
+
if (!rule.name) continue;
|
|
9122
|
+
if (rule.name.startsWith("shield:")) continue;
|
|
9123
|
+
sources.push({
|
|
9124
|
+
shieldName: "default",
|
|
9125
|
+
shieldLabel: "Default Rules",
|
|
9126
|
+
sourceType: "default",
|
|
9127
|
+
rule
|
|
9128
|
+
});
|
|
8937
9129
|
}
|
|
8938
9130
|
return sources;
|
|
8939
9131
|
}
|
|
@@ -9019,178 +9211,53 @@ function renderProgressBar(done, total, lines) {
|
|
|
9019
9211
|
`\r ${chalk5.cyan("Scanning")} [${chalk5.cyan(bar)}] ${chalk5.dim(fileLabel)}${lineLabel} `
|
|
9020
9212
|
);
|
|
9021
9213
|
}
|
|
9022
|
-
function
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
bashCalls: 0,
|
|
9029
|
-
findings: [],
|
|
9030
|
-
dlpFindings: [],
|
|
9031
|
-
loopFindings: [],
|
|
9032
|
-
totalCostUSD: 0,
|
|
9033
|
-
firstDate: null,
|
|
9034
|
-
lastDate: null,
|
|
9035
|
-
sessionsWithEarlySecrets: 0
|
|
9036
|
-
};
|
|
9037
|
-
if (!fs19.existsSync(projectsDir)) return result;
|
|
9038
|
-
let projDirs;
|
|
9214
|
+
function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
|
|
9215
|
+
result.filesScanned++;
|
|
9216
|
+
result.sessions++;
|
|
9217
|
+
onProgress?.(result.filesScanned);
|
|
9218
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
9219
|
+
let raw;
|
|
9039
9220
|
try {
|
|
9040
|
-
|
|
9221
|
+
raw = fs19.readFileSync(path21.join(projPath, file), "utf-8");
|
|
9041
9222
|
} catch {
|
|
9042
|
-
return
|
|
9223
|
+
return;
|
|
9043
9224
|
}
|
|
9044
|
-
const
|
|
9045
|
-
|
|
9046
|
-
|
|
9225
|
+
const sessionCalls = [];
|
|
9226
|
+
const toolUseFilePaths = /* @__PURE__ */ new Map();
|
|
9227
|
+
let firstDlpTs = null;
|
|
9228
|
+
let firstEditTs = null;
|
|
9229
|
+
for (const line of raw.split("\n")) {
|
|
9230
|
+
if (!line.trim()) continue;
|
|
9231
|
+
onLine?.();
|
|
9232
|
+
let entry;
|
|
9047
9233
|
try {
|
|
9048
|
-
|
|
9234
|
+
entry = JSON.parse(line);
|
|
9049
9235
|
} catch {
|
|
9050
9236
|
continue;
|
|
9051
9237
|
}
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
let files;
|
|
9056
|
-
try {
|
|
9057
|
-
files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
9058
|
-
} catch {
|
|
9059
|
-
continue;
|
|
9238
|
+
if (entry.type !== "assistant" && entry.type !== "user") continue;
|
|
9239
|
+
if (startDate && entry.timestamp) {
|
|
9240
|
+
if (new Date(entry.timestamp) < startDate) continue;
|
|
9060
9241
|
}
|
|
9061
|
-
|
|
9062
|
-
result.
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
const sessionCalls = [];
|
|
9073
|
-
const toolUseFilePaths = /* @__PURE__ */ new Map();
|
|
9074
|
-
let firstDlpTs = null;
|
|
9075
|
-
let firstEditTs = null;
|
|
9076
|
-
for (const line of raw.split("\n")) {
|
|
9077
|
-
if (!line.trim()) continue;
|
|
9078
|
-
onLine?.();
|
|
9079
|
-
let entry;
|
|
9080
|
-
try {
|
|
9081
|
-
entry = JSON.parse(line);
|
|
9082
|
-
} catch {
|
|
9083
|
-
continue;
|
|
9084
|
-
}
|
|
9085
|
-
if (entry.type !== "assistant" && entry.type !== "user") continue;
|
|
9086
|
-
if (startDate && entry.timestamp) {
|
|
9087
|
-
if (new Date(entry.timestamp) < startDate) continue;
|
|
9088
|
-
}
|
|
9089
|
-
if (entry.timestamp) {
|
|
9090
|
-
if (!result.firstDate || entry.timestamp < result.firstDate)
|
|
9091
|
-
result.firstDate = entry.timestamp;
|
|
9092
|
-
if (!result.lastDate || entry.timestamp > result.lastDate)
|
|
9093
|
-
result.lastDate = entry.timestamp;
|
|
9094
|
-
}
|
|
9095
|
-
if (entry.type === "user") {
|
|
9096
|
-
const content2 = entry.message?.content;
|
|
9097
|
-
if (Array.isArray(content2)) {
|
|
9098
|
-
const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
|
|
9099
|
-
if (text) {
|
|
9100
|
-
const dlpMatch = scanArgs({ text });
|
|
9101
|
-
if (dlpMatch) {
|
|
9102
|
-
const isDupe = result.dlpFindings.some(
|
|
9103
|
-
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
9104
|
-
);
|
|
9105
|
-
if (!isDupe) {
|
|
9106
|
-
result.dlpFindings.push({
|
|
9107
|
-
patternName: dlpMatch.patternName,
|
|
9108
|
-
redactedSample: dlpMatch.redactedSample,
|
|
9109
|
-
toolName: "user-prompt",
|
|
9110
|
-
timestamp: entry.timestamp ?? "",
|
|
9111
|
-
project: projLabel,
|
|
9112
|
-
sessionId,
|
|
9113
|
-
agent: "claude"
|
|
9114
|
-
});
|
|
9115
|
-
}
|
|
9116
|
-
}
|
|
9117
|
-
}
|
|
9118
|
-
for (const block of content2) {
|
|
9119
|
-
if (block.type !== "tool_result") continue;
|
|
9120
|
-
const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
|
|
9121
|
-
if (filePath) {
|
|
9122
|
-
const ext = path21.extname(filePath).toLowerCase();
|
|
9123
|
-
if (CODE_EXTENSIONS.has(ext)) continue;
|
|
9124
|
-
}
|
|
9125
|
-
const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
|
|
9126
|
-
if (!resultText) continue;
|
|
9127
|
-
if (isNode9SelfOutput(resultText)) continue;
|
|
9128
|
-
const dlpMatch = scanArgs({ text: resultText });
|
|
9129
|
-
if (dlpMatch) {
|
|
9130
|
-
if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
|
|
9131
|
-
if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
|
|
9132
|
-
const isDupe = result.dlpFindings.some(
|
|
9133
|
-
(f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
|
|
9134
|
-
);
|
|
9135
|
-
if (!isDupe) {
|
|
9136
|
-
result.dlpFindings.push({
|
|
9137
|
-
patternName: dlpMatch.patternName,
|
|
9138
|
-
redactedSample: dlpMatch.redactedSample,
|
|
9139
|
-
toolName: "tool-result",
|
|
9140
|
-
timestamp: entry.timestamp ?? "",
|
|
9141
|
-
project: projLabel,
|
|
9142
|
-
sessionId,
|
|
9143
|
-
agent: "claude"
|
|
9144
|
-
});
|
|
9145
|
-
}
|
|
9146
|
-
}
|
|
9147
|
-
}
|
|
9148
|
-
}
|
|
9149
|
-
continue;
|
|
9150
|
-
}
|
|
9151
|
-
const usage = entry.message?.usage;
|
|
9152
|
-
const model = entry.message?.model;
|
|
9153
|
-
if (usage && model) {
|
|
9154
|
-
const p = claudeModelPrice(model);
|
|
9155
|
-
if (p) {
|
|
9156
|
-
result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
|
|
9157
|
-
}
|
|
9158
|
-
}
|
|
9159
|
-
const content = entry.message?.content;
|
|
9160
|
-
if (!Array.isArray(content)) continue;
|
|
9161
|
-
for (const block of content) {
|
|
9162
|
-
if (block.type !== "tool_use") continue;
|
|
9163
|
-
result.totalToolCalls++;
|
|
9164
|
-
const toolName = block.name ?? "";
|
|
9165
|
-
const toolNameLower = toolName.toLowerCase();
|
|
9166
|
-
const input = block.input ?? {};
|
|
9167
|
-
if (block.id && typeof input.file_path === "string") {
|
|
9168
|
-
toolUseFilePaths.set(block.id, input.file_path);
|
|
9169
|
-
}
|
|
9170
|
-
sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
|
|
9171
|
-
if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
|
|
9172
|
-
result.bashCalls++;
|
|
9173
|
-
}
|
|
9174
|
-
if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
|
|
9175
|
-
firstEditTs = entry.timestamp ?? null;
|
|
9176
|
-
}
|
|
9177
|
-
const rawCmd = String(input.command ?? "").trimStart();
|
|
9178
|
-
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
|
|
9179
|
-
continue;
|
|
9180
|
-
const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
9181
|
-
const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
|
|
9182
|
-
if (CODE_EXTENSIONS.has(inputFileExt)) continue;
|
|
9183
|
-
const dlpMatch = scanArgs(input);
|
|
9242
|
+
if (entry.timestamp) {
|
|
9243
|
+
if (!result.firstDate || entry.timestamp < result.firstDate)
|
|
9244
|
+
result.firstDate = entry.timestamp;
|
|
9245
|
+
if (!result.lastDate || entry.timestamp > result.lastDate) result.lastDate = entry.timestamp;
|
|
9246
|
+
}
|
|
9247
|
+
if (entry.type === "user") {
|
|
9248
|
+
const content2 = entry.message?.content;
|
|
9249
|
+
if (Array.isArray(content2)) {
|
|
9250
|
+
const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
|
|
9251
|
+
if (text) {
|
|
9252
|
+
const dlpMatch = scanArgs({ text });
|
|
9184
9253
|
if (dlpMatch) {
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
(
|
|
9188
|
-
);
|
|
9189
|
-
if (!isDupe) {
|
|
9254
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9255
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9256
|
+
dedup.dlpKeys.add(k);
|
|
9190
9257
|
result.dlpFindings.push({
|
|
9191
9258
|
patternName: dlpMatch.patternName,
|
|
9192
9259
|
redactedSample: dlpMatch.redactedSample,
|
|
9193
|
-
toolName,
|
|
9260
|
+
toolName: "user-prompt",
|
|
9194
9261
|
timestamp: entry.timestamp ?? "",
|
|
9195
9262
|
project: projLabel,
|
|
9196
9263
|
sessionId,
|
|
@@ -9198,102 +9265,252 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
|
9198
9265
|
});
|
|
9199
9266
|
}
|
|
9200
9267
|
}
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
if (
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
entry.timestamp ?? "",
|
|
9209
|
-
projLabel,
|
|
9210
|
-
sessionId,
|
|
9211
|
-
"claude",
|
|
9212
|
-
result
|
|
9213
|
-
);
|
|
9268
|
+
}
|
|
9269
|
+
for (const block of content2) {
|
|
9270
|
+
if (block.type !== "tool_result") continue;
|
|
9271
|
+
const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
|
|
9272
|
+
if (filePath) {
|
|
9273
|
+
const ext = path21.extname(filePath).toLowerCase();
|
|
9274
|
+
if (CODE_EXTENSIONS.has(ext)) continue;
|
|
9214
9275
|
}
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
if (
|
|
9221
|
-
if (
|
|
9222
|
-
const
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
toolName,
|
|
9230
|
-
input,
|
|
9276
|
+
const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
|
|
9277
|
+
if (!resultText) continue;
|
|
9278
|
+
if (isNode9SelfOutput(resultText)) continue;
|
|
9279
|
+
const dlpMatch = scanArgs({ text: resultText });
|
|
9280
|
+
if (dlpMatch) {
|
|
9281
|
+
if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
|
|
9282
|
+
if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
|
|
9283
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9284
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9285
|
+
dedup.dlpKeys.add(k);
|
|
9286
|
+
result.dlpFindings.push({
|
|
9287
|
+
patternName: dlpMatch.patternName,
|
|
9288
|
+
redactedSample: dlpMatch.redactedSample,
|
|
9289
|
+
toolName: "tool-result",
|
|
9231
9290
|
timestamp: entry.timestamp ?? "",
|
|
9232
9291
|
project: projLabel,
|
|
9233
9292
|
sessionId,
|
|
9234
9293
|
agent: "claude"
|
|
9235
9294
|
});
|
|
9236
9295
|
}
|
|
9237
|
-
ruleMatched = true;
|
|
9238
|
-
break;
|
|
9239
|
-
}
|
|
9240
|
-
if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
|
|
9241
|
-
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
9242
|
-
if (shellVerdict) {
|
|
9243
|
-
const astRule = {
|
|
9244
|
-
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
9245
|
-
tool: "bash",
|
|
9246
|
-
conditions: [],
|
|
9247
|
-
verdict: shellVerdict,
|
|
9248
|
-
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
9249
|
-
};
|
|
9250
|
-
const inputPreview = preview(input, 120);
|
|
9251
|
-
const isDupe = result.findings.some(
|
|
9252
|
-
(f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
|
|
9253
|
-
);
|
|
9254
|
-
if (!isDupe) {
|
|
9255
|
-
result.findings.push({
|
|
9256
|
-
source: {
|
|
9257
|
-
shieldName: "bash-safe",
|
|
9258
|
-
shieldLabel: "bash-safe (AST)",
|
|
9259
|
-
sourceType: "shield",
|
|
9260
|
-
rule: astRule
|
|
9261
|
-
},
|
|
9262
|
-
toolName,
|
|
9263
|
-
input,
|
|
9264
|
-
timestamp: entry.timestamp ?? "",
|
|
9265
|
-
project: projLabel,
|
|
9266
|
-
sessionId,
|
|
9267
|
-
agent: "claude"
|
|
9268
|
-
});
|
|
9269
|
-
}
|
|
9270
|
-
}
|
|
9271
9296
|
}
|
|
9272
9297
|
}
|
|
9273
9298
|
}
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9299
|
+
continue;
|
|
9300
|
+
}
|
|
9301
|
+
const usage = entry.message?.usage;
|
|
9302
|
+
const model = entry.message?.model;
|
|
9303
|
+
if (usage && model) {
|
|
9304
|
+
const p = claudeModelPrice(model);
|
|
9305
|
+
if (p) {
|
|
9306
|
+
result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
|
|
9277
9307
|
}
|
|
9278
9308
|
}
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9309
|
+
const content = entry.message?.content;
|
|
9310
|
+
if (!Array.isArray(content)) continue;
|
|
9311
|
+
for (const block of content) {
|
|
9312
|
+
if (block.type !== "tool_use") continue;
|
|
9313
|
+
result.totalToolCalls++;
|
|
9314
|
+
const toolName = block.name ?? "";
|
|
9315
|
+
const toolNameLower = toolName.toLowerCase();
|
|
9316
|
+
const input = block.input ?? {};
|
|
9317
|
+
if (block.id && typeof input.file_path === "string") {
|
|
9318
|
+
toolUseFilePaths.set(block.id, input.file_path);
|
|
9319
|
+
}
|
|
9320
|
+
sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
|
|
9321
|
+
if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
|
|
9322
|
+
result.bashCalls++;
|
|
9323
|
+
}
|
|
9324
|
+
if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
|
|
9325
|
+
firstEditTs = entry.timestamp ?? null;
|
|
9326
|
+
}
|
|
9327
|
+
const rawCmd = String(input.command ?? "").trimStart();
|
|
9328
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
|
|
9329
|
+
const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
9330
|
+
const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
|
|
9331
|
+
if (CODE_EXTENSIONS.has(inputFileExt)) continue;
|
|
9332
|
+
const dlpMatch = scanArgs(input);
|
|
9333
|
+
if (dlpMatch) {
|
|
9334
|
+
if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
|
|
9335
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9336
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9337
|
+
dedup.dlpKeys.add(k);
|
|
9338
|
+
result.dlpFindings.push({
|
|
9339
|
+
patternName: dlpMatch.patternName,
|
|
9340
|
+
redactedSample: dlpMatch.redactedSample,
|
|
9341
|
+
toolName,
|
|
9342
|
+
timestamp: entry.timestamp ?? "",
|
|
9343
|
+
project: projLabel,
|
|
9344
|
+
sessionId,
|
|
9345
|
+
agent: "claude"
|
|
9346
|
+
});
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
let astFsMatched = false;
|
|
9350
|
+
const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
|
|
9351
|
+
if (astRanForBash) {
|
|
9352
|
+
astFsMatched = pushFsOpAstFinding(
|
|
9353
|
+
String(input.command ?? ""),
|
|
9354
|
+
toolName,
|
|
9355
|
+
input,
|
|
9356
|
+
entry.timestamp ?? "",
|
|
9357
|
+
projLabel,
|
|
9358
|
+
sessionId,
|
|
9359
|
+
"claude",
|
|
9360
|
+
result,
|
|
9361
|
+
dedup
|
|
9362
|
+
);
|
|
9363
|
+
}
|
|
9364
|
+
let ruleMatched = astFsMatched;
|
|
9365
|
+
for (const source of ruleSources) {
|
|
9366
|
+
const { rule } = source;
|
|
9367
|
+
if (rule.verdict === "allow") continue;
|
|
9368
|
+
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
9369
|
+
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
9370
|
+
if (!evaluateSmartConditions(input, rule)) continue;
|
|
9371
|
+
const inputPreview = preview(input, 120);
|
|
9372
|
+
const k = findingKey(rule.name, inputPreview, projLabel);
|
|
9373
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9374
|
+
dedup.findingsKeys.add(k);
|
|
9375
|
+
result.findings.push({
|
|
9376
|
+
source,
|
|
9377
|
+
toolName,
|
|
9378
|
+
input,
|
|
9379
|
+
timestamp: entry.timestamp ?? "",
|
|
9380
|
+
project: projLabel,
|
|
9381
|
+
sessionId,
|
|
9382
|
+
agent: "claude"
|
|
9383
|
+
});
|
|
9384
|
+
}
|
|
9385
|
+
ruleMatched = true;
|
|
9386
|
+
break;
|
|
9387
|
+
}
|
|
9388
|
+
if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
|
|
9389
|
+
const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
|
|
9390
|
+
if (shellVerdict) {
|
|
9391
|
+
const astRule = {
|
|
9392
|
+
name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
|
|
9393
|
+
tool: "bash",
|
|
9394
|
+
conditions: [],
|
|
9395
|
+
verdict: shellVerdict,
|
|
9396
|
+
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
9397
|
+
};
|
|
9398
|
+
const inputPreview = preview(input, 120);
|
|
9399
|
+
const k = findingKey(astRule.name, inputPreview, projLabel);
|
|
9400
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9401
|
+
dedup.findingsKeys.add(k);
|
|
9402
|
+
result.findings.push({
|
|
9403
|
+
source: {
|
|
9404
|
+
shieldName: "bash-safe",
|
|
9405
|
+
shieldLabel: "bash-safe (AST)",
|
|
9406
|
+
sourceType: "shield",
|
|
9407
|
+
rule: astRule
|
|
9408
|
+
},
|
|
9409
|
+
toolName,
|
|
9410
|
+
input,
|
|
9411
|
+
timestamp: entry.timestamp ?? "",
|
|
9412
|
+
project: projLabel,
|
|
9413
|
+
sessionId,
|
|
9414
|
+
agent: "claude"
|
|
9415
|
+
});
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
|
|
9422
|
+
if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
|
|
9423
|
+
result.sessionsWithEarlySecrets++;
|
|
9424
|
+
}
|
|
9425
|
+
}
|
|
9426
|
+
function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
|
|
9427
|
+
const projPath = path21.join(projectsDir, proj);
|
|
9428
|
+
try {
|
|
9429
|
+
if (!fs19.statSync(projPath).isDirectory()) return;
|
|
9430
|
+
} catch {
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os18.homedir(), "~")).slice(
|
|
9434
|
+
0,
|
|
9435
|
+
40
|
|
9436
|
+
);
|
|
9437
|
+
let files;
|
|
9438
|
+
try {
|
|
9439
|
+
files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
9440
|
+
} catch {
|
|
9441
|
+
return;
|
|
9442
|
+
}
|
|
9443
|
+
for (const file of files) {
|
|
9444
|
+
processClaudeFile(
|
|
9445
|
+
file,
|
|
9446
|
+
projPath,
|
|
9447
|
+
projLabel,
|
|
9448
|
+
ruleSources,
|
|
9449
|
+
startDate,
|
|
9450
|
+
result,
|
|
9451
|
+
dedup,
|
|
9452
|
+
onProgress,
|
|
9453
|
+
onLine
|
|
9454
|
+
);
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
function emptyClaudeScan() {
|
|
9458
|
+
return {
|
|
9459
|
+
filesScanned: 0,
|
|
9460
|
+
sessions: 0,
|
|
9461
|
+
totalToolCalls: 0,
|
|
9462
|
+
bashCalls: 0,
|
|
9463
|
+
findings: [],
|
|
9464
|
+
dlpFindings: [],
|
|
9465
|
+
loopFindings: [],
|
|
9466
|
+
totalCostUSD: 0,
|
|
9467
|
+
firstDate: null,
|
|
9468
|
+
lastDate: null,
|
|
9469
|
+
sessionsWithEarlySecrets: 0
|
|
9470
|
+
};
|
|
9471
|
+
}
|
|
9472
|
+
function scanClaudeHistory(startDate, onProgress, onLine) {
|
|
9473
|
+
const projectsDir = path21.join(os18.homedir(), ".claude", "projects");
|
|
9474
|
+
const result = emptyClaudeScan();
|
|
9475
|
+
if (!fs19.existsSync(projectsDir)) return result;
|
|
9476
|
+
let projDirs;
|
|
9477
|
+
try {
|
|
9478
|
+
projDirs = fs19.readdirSync(projectsDir);
|
|
9479
|
+
} catch {
|
|
9480
|
+
return result;
|
|
9481
|
+
}
|
|
9482
|
+
const ruleSources = buildRuleSources();
|
|
9483
|
+
const dedup = emptyScanDedup();
|
|
9484
|
+
for (const proj of projDirs) {
|
|
9485
|
+
processClaudeProject(
|
|
9486
|
+
proj,
|
|
9487
|
+
projectsDir,
|
|
9488
|
+
ruleSources,
|
|
9489
|
+
startDate,
|
|
9490
|
+
result,
|
|
9491
|
+
dedup,
|
|
9492
|
+
onProgress,
|
|
9493
|
+
onLine
|
|
9494
|
+
);
|
|
9495
|
+
}
|
|
9496
|
+
return result;
|
|
9497
|
+
}
|
|
9498
|
+
function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
9499
|
+
const tmpDir = path21.join(os18.homedir(), ".gemini", "tmp");
|
|
9500
|
+
const result = {
|
|
9501
|
+
filesScanned: 0,
|
|
9502
|
+
sessions: 0,
|
|
9503
|
+
totalToolCalls: 0,
|
|
9504
|
+
bashCalls: 0,
|
|
9505
|
+
findings: [],
|
|
9506
|
+
dlpFindings: [],
|
|
9291
9507
|
loopFindings: [],
|
|
9292
9508
|
totalCostUSD: 0,
|
|
9293
9509
|
firstDate: null,
|
|
9294
9510
|
lastDate: null,
|
|
9295
9511
|
sessionsWithEarlySecrets: 0
|
|
9296
9512
|
};
|
|
9513
|
+
const dedup = emptyScanDedup();
|
|
9297
9514
|
if (!fs19.existsSync(tmpDir)) return result;
|
|
9298
9515
|
let slugDirs;
|
|
9299
9516
|
try {
|
|
@@ -9350,10 +9567,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
9350
9567
|
if (text) {
|
|
9351
9568
|
const dlpMatch = scanArgs({ text });
|
|
9352
9569
|
if (dlpMatch) {
|
|
9353
|
-
const
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
if (!isDupe) {
|
|
9570
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9571
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9572
|
+
dedup.dlpKeys.add(k);
|
|
9357
9573
|
result.dlpFindings.push({
|
|
9358
9574
|
patternName: dlpMatch.patternName,
|
|
9359
9575
|
redactedSample: dlpMatch.redactedSample,
|
|
@@ -9398,10 +9614,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
9398
9614
|
continue;
|
|
9399
9615
|
const dlpMatch = scanArgs(input);
|
|
9400
9616
|
if (dlpMatch) {
|
|
9401
|
-
const
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
if (!isDupe) {
|
|
9617
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9618
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9619
|
+
dedup.dlpKeys.add(k);
|
|
9405
9620
|
result.dlpFindings.push({
|
|
9406
9621
|
patternName: dlpMatch.patternName,
|
|
9407
9622
|
redactedSample: dlpMatch.redactedSample,
|
|
@@ -9424,7 +9639,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
9424
9639
|
projLabel,
|
|
9425
9640
|
sessionId,
|
|
9426
9641
|
"gemini",
|
|
9427
|
-
result
|
|
9642
|
+
result,
|
|
9643
|
+
dedup
|
|
9428
9644
|
);
|
|
9429
9645
|
}
|
|
9430
9646
|
let ruleMatched = astFsMatched;
|
|
@@ -9435,10 +9651,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
9435
9651
|
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
9436
9652
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
9437
9653
|
const inputPreview = preview(input, 120);
|
|
9438
|
-
const
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
if (!isDupe) {
|
|
9654
|
+
const k = findingKey(rule.name, inputPreview, projLabel);
|
|
9655
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9656
|
+
dedup.findingsKeys.add(k);
|
|
9442
9657
|
result.findings.push({
|
|
9443
9658
|
source,
|
|
9444
9659
|
toolName,
|
|
@@ -9466,10 +9681,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
9466
9681
|
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
9467
9682
|
};
|
|
9468
9683
|
const inputPreview = preview(input, 120);
|
|
9469
|
-
const
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
if (!isDupe) {
|
|
9684
|
+
const k = findingKey(astRule.name, inputPreview, projLabel);
|
|
9685
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9686
|
+
dedup.findingsKeys.add(k);
|
|
9473
9687
|
result.findings.push({
|
|
9474
9688
|
source: {
|
|
9475
9689
|
shieldName: "bash-safe",
|
|
@@ -9509,6 +9723,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9509
9723
|
lastDate: null,
|
|
9510
9724
|
sessionsWithEarlySecrets: 0
|
|
9511
9725
|
};
|
|
9726
|
+
const dedup = emptyScanDedup();
|
|
9512
9727
|
if (!fs19.existsSync(sessionsBase)) return result;
|
|
9513
9728
|
const jsonlFiles = [];
|
|
9514
9729
|
try {
|
|
@@ -9590,10 +9805,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9590
9805
|
if (text) {
|
|
9591
9806
|
const dlpMatch2 = scanArgs({ text });
|
|
9592
9807
|
if (dlpMatch2) {
|
|
9593
|
-
const
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
if (!isDupe) {
|
|
9808
|
+
const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
|
|
9809
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9810
|
+
dedup.dlpKeys.add(k);
|
|
9597
9811
|
result.dlpFindings.push({
|
|
9598
9812
|
patternName: dlpMatch2.patternName,
|
|
9599
9813
|
redactedSample: dlpMatch2.redactedSample,
|
|
@@ -9635,10 +9849,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9635
9849
|
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
|
|
9636
9850
|
const dlpMatch = scanArgs(input);
|
|
9637
9851
|
if (dlpMatch) {
|
|
9638
|
-
const
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
if (!isDupe) {
|
|
9852
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
|
|
9853
|
+
if (!dedup.dlpKeys.has(k)) {
|
|
9854
|
+
dedup.dlpKeys.add(k);
|
|
9642
9855
|
result.dlpFindings.push({
|
|
9643
9856
|
patternName: dlpMatch.patternName,
|
|
9644
9857
|
redactedSample: dlpMatch.redactedSample,
|
|
@@ -9661,7 +9874,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9661
9874
|
projLabel,
|
|
9662
9875
|
sessionId,
|
|
9663
9876
|
"codex",
|
|
9664
|
-
result
|
|
9877
|
+
result,
|
|
9878
|
+
dedup
|
|
9665
9879
|
);
|
|
9666
9880
|
}
|
|
9667
9881
|
let ruleMatched = astFsMatched;
|
|
@@ -9673,10 +9887,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9673
9887
|
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
9674
9888
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
9675
9889
|
const inputPreview = preview(input, 120);
|
|
9676
|
-
const
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
if (!isDupe) {
|
|
9890
|
+
const k = findingKey(rule.name, inputPreview, projLabel);
|
|
9891
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9892
|
+
dedup.findingsKeys.add(k);
|
|
9680
9893
|
result.findings.push({
|
|
9681
9894
|
source,
|
|
9682
9895
|
toolName,
|
|
@@ -9701,10 +9914,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
9701
9914
|
reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
|
|
9702
9915
|
};
|
|
9703
9916
|
const inputPreview = preview(input, 120);
|
|
9704
|
-
const
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
if (!isDupe) {
|
|
9917
|
+
const k = findingKey(astRule.name, inputPreview, projLabel);
|
|
9918
|
+
if (!dedup.findingsKeys.has(k)) {
|
|
9919
|
+
dedup.findingsKeys.add(k);
|
|
9708
9920
|
result.findings.push({
|
|
9709
9921
|
source: {
|
|
9710
9922
|
shieldName: "bash-safe",
|
|
@@ -9735,6 +9947,7 @@ function scanShellConfig() {
|
|
|
9735
9947
|
(f) => path21.join(home, f)
|
|
9736
9948
|
);
|
|
9737
9949
|
const findings = [];
|
|
9950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9738
9951
|
for (const filePath of configFiles) {
|
|
9739
9952
|
if (!fs19.existsSync(filePath)) continue;
|
|
9740
9953
|
let lines;
|
|
@@ -9749,10 +9962,9 @@ function scanShellConfig() {
|
|
|
9749
9962
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
9750
9963
|
const dlpMatch = scanArgs({ text: trimmed });
|
|
9751
9964
|
if (!dlpMatch) continue;
|
|
9752
|
-
const
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
if (!isDupe) {
|
|
9965
|
+
const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, shortPath);
|
|
9966
|
+
if (!seen.has(k)) {
|
|
9967
|
+
seen.add(k);
|
|
9756
9968
|
findings.push({
|
|
9757
9969
|
patternName: dlpMatch.patternName,
|
|
9758
9970
|
redactedSample: dlpMatch.redactedSample,
|
|
@@ -10011,6 +10223,267 @@ function renderNarrativeScorecard(input) {
|
|
|
10011
10223
|
console.log(chalk5.dim("\u2192 github.com/node9-ai/node9-proxy"));
|
|
10012
10224
|
console.log("");
|
|
10013
10225
|
}
|
|
10226
|
+
function mkLine(...parts) {
|
|
10227
|
+
let rendered = "";
|
|
10228
|
+
let width = 0;
|
|
10229
|
+
for (const [text, fmt] of parts) {
|
|
10230
|
+
rendered += fmt ? fmt(text) : text;
|
|
10231
|
+
width += stringWidth2(text);
|
|
10232
|
+
}
|
|
10233
|
+
return { rendered, width };
|
|
10234
|
+
}
|
|
10235
|
+
function shortRule(name, width) {
|
|
10236
|
+
const stripped = name.replace(/^shield:[^:]+:/, "");
|
|
10237
|
+
if (stripped.length <= width) return stripped.padEnd(width);
|
|
10238
|
+
return stripped.slice(0, width - 1) + "\u2026";
|
|
10239
|
+
}
|
|
10240
|
+
function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
10241
|
+
const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
|
|
10242
|
+
const topLines = [];
|
|
10243
|
+
if (scan.dlpFindings.length > 0) {
|
|
10244
|
+
const latest = scan.dlpFindings[0];
|
|
10245
|
+
const rel = relativeDate(latest.timestamp, now);
|
|
10246
|
+
const noun = `credential leak${scan.dlpFindings.length !== 1 ? "s" : ""}`;
|
|
10247
|
+
topLines.push(
|
|
10248
|
+
mkLine(
|
|
10249
|
+
["\u{1F6A8} ", chalk5.red],
|
|
10250
|
+
[`${scan.dlpFindings.length} ${noun} in tool input `, chalk5.bold],
|
|
10251
|
+
[`(latest: ${rel} ago, ${latest.patternName})`, chalk5.dim]
|
|
10252
|
+
)
|
|
10253
|
+
);
|
|
10254
|
+
}
|
|
10255
|
+
if (blockedCount > 0) {
|
|
10256
|
+
const topBlocked = topRulesByVerdict(summary.sections, "block", 2).map(
|
|
10257
|
+
(r) => r.count > 1 ? `${shortRule(r.name, 20).trimEnd()} \xD7${r.count}` : shortRule(r.name, 20).trimEnd()
|
|
10258
|
+
).join(", ");
|
|
10259
|
+
topLines.push(
|
|
10260
|
+
mkLine(
|
|
10261
|
+
["\u{1F6D1} ", chalk5.red],
|
|
10262
|
+
[`${blockedCount} ops node9 would have blocked `, chalk5.bold],
|
|
10263
|
+
[`(${topBlocked})`, chalk5.dim]
|
|
10264
|
+
)
|
|
10265
|
+
);
|
|
10266
|
+
}
|
|
10267
|
+
if (scan.loopFindings.length > 0) {
|
|
10268
|
+
const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
|
|
10269
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
10270
|
+
for (const f of scan.loopFindings) {
|
|
10271
|
+
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
|
|
10272
|
+
}
|
|
10273
|
+
const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
10274
|
+
const wasteSuffix = wastePct > 0 ? `, ${wastePct}% wasted` : "";
|
|
10275
|
+
const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
|
|
10276
|
+
topLines.push(
|
|
10277
|
+
mkLine(
|
|
10278
|
+
["\u{1F501} ", chalk5.yellow],
|
|
10279
|
+
[`${scan.loopFindings.length} agent loops detected `, chalk5.bold],
|
|
10280
|
+
[detail, chalk5.dim]
|
|
10281
|
+
)
|
|
10282
|
+
);
|
|
10283
|
+
}
|
|
10284
|
+
if (blastExposures > 0) {
|
|
10285
|
+
const exposed2 = Math.max(0, 100 - blast.score);
|
|
10286
|
+
const pjDiscount = PROTECTIVE_SHIELD_DISCOUNTS["project-jail"] ?? 0;
|
|
10287
|
+
const pjBonus = Math.round(exposed2 * pjDiscount);
|
|
10288
|
+
const cta = pjBonus > 0 ? ` \u2192 enable project-jail (+${pjBonus} pts)` : "";
|
|
10289
|
+
topLines.push(
|
|
10290
|
+
mkLine(
|
|
10291
|
+
["\u{1F52D} ", chalk5.red],
|
|
10292
|
+
[`${blastExposures} secrets reachable on disk`, chalk5.bold],
|
|
10293
|
+
[cta, chalk5.dim]
|
|
10294
|
+
)
|
|
10295
|
+
);
|
|
10296
|
+
}
|
|
10297
|
+
if (topLines.length > 0) {
|
|
10298
|
+
for (const ln of boxPanel("TOP FINDINGS", topLines)) console.log(" " + ln);
|
|
10299
|
+
console.log("");
|
|
10300
|
+
}
|
|
10301
|
+
if (summary.leaks.length > 0) {
|
|
10302
|
+
const leakLines = [];
|
|
10303
|
+
for (const leak of summary.leaks.slice(0, 5)) {
|
|
10304
|
+
const rel = relativeDate(leak.timestamp, now);
|
|
10305
|
+
leakLines.push(
|
|
10306
|
+
mkLine(
|
|
10307
|
+
[rel.padStart(4) + " ", chalk5.dim],
|
|
10308
|
+
[leak.patternName.padEnd(14), chalk5.red.bold],
|
|
10309
|
+
[" "],
|
|
10310
|
+
[leak.redactedSample.padEnd(20), chalk5.red],
|
|
10311
|
+
[" "],
|
|
10312
|
+
[`[${leak.toolName}]`.padEnd(15), chalk5.dim],
|
|
10313
|
+
[" "],
|
|
10314
|
+
[leak.agent, chalk5.dim]
|
|
10315
|
+
)
|
|
10316
|
+
);
|
|
10317
|
+
}
|
|
10318
|
+
const remaining = summary.leaks.length - 5;
|
|
10319
|
+
if (remaining > 0) {
|
|
10320
|
+
leakLines.push(mkLine([`\u2026 +${remaining} more`, chalk5.dim]));
|
|
10321
|
+
}
|
|
10322
|
+
const title = `LEAKS \xB7 ${summary.leaks.length} secret${summary.leaks.length !== 1 ? "s" : ""} in plain text`;
|
|
10323
|
+
for (const ln of boxPanel(title, leakLines)) console.log(" " + ln);
|
|
10324
|
+
console.log("");
|
|
10325
|
+
}
|
|
10326
|
+
if (blockedCount > 0) {
|
|
10327
|
+
const blockedLines = [];
|
|
10328
|
+
const ruleEntries = topRulesByVerdict(summary.sections, "block", 12);
|
|
10329
|
+
for (const r of ruleEntries) {
|
|
10330
|
+
const origin = originForRule(r.name, summary.sections);
|
|
10331
|
+
blockedLines.push(
|
|
10332
|
+
mkLine(
|
|
10333
|
+
["\u2717 ", chalk5.red],
|
|
10334
|
+
[shortRule(r.name, 24), chalk5.bold],
|
|
10335
|
+
[" \xD7" + String(r.count).padEnd(4), chalk5.bold],
|
|
10336
|
+
[" "],
|
|
10337
|
+
[origin, chalk5.dim]
|
|
10338
|
+
)
|
|
10339
|
+
);
|
|
10340
|
+
}
|
|
10341
|
+
const title = `BLOCKED \xB7 ${blockedCount} ops node9 would have stopped`;
|
|
10342
|
+
for (const ln of boxPanel(title, blockedLines)) console.log(" " + ln);
|
|
10343
|
+
console.log("");
|
|
10344
|
+
}
|
|
10345
|
+
if (reviewCount > 0) {
|
|
10346
|
+
const reviewLines = [];
|
|
10347
|
+
const ruleEntries = topRulesByVerdict(summary.sections, "review", 12);
|
|
10348
|
+
for (const r of ruleEntries) {
|
|
10349
|
+
const origin = originForRule(r.name, summary.sections);
|
|
10350
|
+
reviewLines.push(
|
|
10351
|
+
mkLine(
|
|
10352
|
+
// VS-16 (U+FE0F) forces emoji-presentation so string-width
|
|
10353
|
+
// returns 2 cells (matching how modern terminals actually
|
|
10354
|
+
// render it). Without VS-16 string-width says 1 cell — and
|
|
10355
|
+
// the right border drifts off. Same applies to 🛡 / ⚠ below.
|
|
10356
|
+
["\u{1F441}\uFE0F ", chalk5.yellow],
|
|
10357
|
+
[shortRule(r.name, 24), chalk5.bold],
|
|
10358
|
+
[" \xD7" + String(r.count).padEnd(4), chalk5.bold],
|
|
10359
|
+
[" "],
|
|
10360
|
+
[origin, chalk5.dim]
|
|
10361
|
+
)
|
|
10362
|
+
);
|
|
10363
|
+
}
|
|
10364
|
+
const title = `REVIEW QUEUE \xB7 ${reviewCount} ops flagged for approval`;
|
|
10365
|
+
for (const ln of boxPanel(title, reviewLines)) console.log(" " + ln);
|
|
10366
|
+
console.log("");
|
|
10367
|
+
}
|
|
10368
|
+
if (scan.loopFindings.length > 0) {
|
|
10369
|
+
const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
|
|
10370
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
10371
|
+
let totalRepeats = 0;
|
|
10372
|
+
for (const f of scan.loopFindings) {
|
|
10373
|
+
const repeats = Math.max(0, f.count - 1);
|
|
10374
|
+
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
|
|
10375
|
+
totalRepeats += repeats;
|
|
10376
|
+
}
|
|
10377
|
+
const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
10378
|
+
const loopLines = [];
|
|
10379
|
+
for (const [tool, repeats] of toolEntries) {
|
|
10380
|
+
const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
|
|
10381
|
+
loopLines.push(
|
|
10382
|
+
mkLine(
|
|
10383
|
+
[tool.padEnd(10), chalk5.bold],
|
|
10384
|
+
[`\xD7${num(repeats)} repeats`.padEnd(16)],
|
|
10385
|
+
[`(${pct}%)`, chalk5.dim]
|
|
10386
|
+
)
|
|
10387
|
+
);
|
|
10388
|
+
}
|
|
10389
|
+
const topStuck = [...scan.loopFindings].sort((a, b) => b.count - a.count).slice(0, 3);
|
|
10390
|
+
if (topStuck.length > 0) {
|
|
10391
|
+
loopLines.push(mkLine([""]));
|
|
10392
|
+
loopLines.push(mkLine(["Top stuck patterns:", chalk5.dim]));
|
|
10393
|
+
for (const f of topStuck) {
|
|
10394
|
+
const raw = f.commandPreview || f.toolName;
|
|
10395
|
+
const target = raw.length > 60 ? "\u2026" + raw.slice(raw.length - 59) : raw.padEnd(60);
|
|
10396
|
+
loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, chalk5.bold], [target, chalk5.dim]));
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
const wasteSuffix = wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : "";
|
|
10400
|
+
const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} repeated patterns${wasteSuffix}`;
|
|
10401
|
+
for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
|
|
10402
|
+
console.log("");
|
|
10403
|
+
}
|
|
10404
|
+
if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
|
|
10405
|
+
const blastLines = [];
|
|
10406
|
+
const DESC_W = 33;
|
|
10407
|
+
for (const r of blast.reachable.slice(0, 8)) {
|
|
10408
|
+
const trimmed = r.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
|
|
10409
|
+
const desc = trimmed.length > DESC_W ? trimmed.slice(0, DESC_W - 1) + "\u2026" : trimmed;
|
|
10410
|
+
blastLines.push(mkLine(["\u2717 ", chalk5.red], [r.label.padEnd(36)], [desc, chalk5.dim]));
|
|
10411
|
+
}
|
|
10412
|
+
for (const e of blast.envFindings.slice(0, 3)) {
|
|
10413
|
+
blastLines.push(
|
|
10414
|
+
mkLine(["\u26A0\uFE0F ", chalk5.yellow], [`${e.key} `], [`(${e.patternName})`, chalk5.dim])
|
|
10415
|
+
);
|
|
10416
|
+
}
|
|
10417
|
+
const totalExposed = blast.reachable.length + blast.envFindings.length;
|
|
10418
|
+
if (totalExposed > 8) {
|
|
10419
|
+
blastLines.push(mkLine([`\u2026 +${totalExposed - 8} more`, chalk5.dim]));
|
|
10420
|
+
}
|
|
10421
|
+
const title = `BLAST RADIUS \xB7 ${totalExposed} path${totalExposed !== 1 ? "s" : ""} reachable right now`;
|
|
10422
|
+
for (const ln of boxPanel(title, blastLines)) console.log(" " + ln);
|
|
10423
|
+
console.log("");
|
|
10424
|
+
}
|
|
10425
|
+
const shieldImpacts = rollupByShield(summary.sections);
|
|
10426
|
+
const exposed = Math.max(0, 100 - blast.score);
|
|
10427
|
+
const shieldLines = [];
|
|
10428
|
+
const ranked = [...shieldImpacts].sort((a, b) => {
|
|
10429
|
+
const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
|
|
10430
|
+
const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
|
|
10431
|
+
if (aDiscount !== bDiscount) return bDiscount - aDiscount;
|
|
10432
|
+
return b.totalCatches - a.totalCatches;
|
|
10433
|
+
});
|
|
10434
|
+
for (const impact of ranked) {
|
|
10435
|
+
if (impact.totalCatches === 0) continue;
|
|
10436
|
+
const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
|
|
10437
|
+
const bonus = Math.round(exposed * discount);
|
|
10438
|
+
const icon = discount > 0 ? "\u{1F6E1}\uFE0F " : "\u2610 ";
|
|
10439
|
+
const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
|
|
10440
|
+
const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
|
|
10441
|
+
shieldLines.push(
|
|
10442
|
+
mkLine(
|
|
10443
|
+
[icon, discount > 0 ? chalk5.cyan : chalk5.dim],
|
|
10444
|
+
[impact.shieldName.padEnd(14), chalk5.bold],
|
|
10445
|
+
[wouldCatch.padEnd(22), chalk5.dim],
|
|
10446
|
+
[deltaSuffix, bonus > 0 ? chalk5.green.bold : chalk5.dim]
|
|
10447
|
+
)
|
|
10448
|
+
);
|
|
10449
|
+
if (impact.topRuleLabels.length > 0) {
|
|
10450
|
+
const rules = impact.topRuleLabels.join(", ");
|
|
10451
|
+
shieldLines.push(mkLine([" ", chalk5.dim], [rules, chalk5.dim]));
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10454
|
+
const hitShieldSet = new Set(
|
|
10455
|
+
shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
|
|
10456
|
+
);
|
|
10457
|
+
const zeroHitBuiltins = Object.keys(SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
|
|
10458
|
+
if (zeroHitBuiltins.length > 0) {
|
|
10459
|
+
shieldLines.push(mkLine([""]));
|
|
10460
|
+
shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), chalk5.dim]));
|
|
10461
|
+
shieldLines.push(mkLine([" no hits in your history \u2014 install proactively", chalk5.dim]));
|
|
10462
|
+
}
|
|
10463
|
+
const topRec = ranked.find(
|
|
10464
|
+
(r) => r.totalCatches > 0 && (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0
|
|
10465
|
+
);
|
|
10466
|
+
if (topRec) {
|
|
10467
|
+
const bonus = Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0));
|
|
10468
|
+
const cta = `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${bonus} pts)`;
|
|
10469
|
+
shieldLines.push(mkLine([""]));
|
|
10470
|
+
shieldLines.push(mkLine([cta, chalk5.cyan]));
|
|
10471
|
+
}
|
|
10472
|
+
if (shieldLines.length > 0) {
|
|
10473
|
+
const title = "SHIELDS \xB7 install node9 + enable these to catch what we found";
|
|
10474
|
+
for (const ln of boxPanel(title, shieldLines)) console.log(" " + ln);
|
|
10475
|
+
console.log("");
|
|
10476
|
+
}
|
|
10477
|
+
}
|
|
10478
|
+
function originForRule(ruleName, sections) {
|
|
10479
|
+
for (const section of sections) {
|
|
10480
|
+
if (section.rules.some((r) => r.name === ruleName)) {
|
|
10481
|
+
if (section.sourceType === "default") return "default";
|
|
10482
|
+
if (section.sourceType === "shield") return `needs shield:${section.shieldKey ?? section.id}`;
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
return "";
|
|
10486
|
+
}
|
|
10014
10487
|
function registerScanCommand(program2) {
|
|
10015
10488
|
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").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
|
|
10016
10489
|
"--json",
|
|
@@ -10242,7 +10715,7 @@ function registerScanCommand(program2) {
|
|
|
10242
10715
|
" " + chalk5.dim("AI spend ") + chalk5.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk5.dim(" \xB7 wasted on loops ") + chalk5.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
|
|
10243
10716
|
);
|
|
10244
10717
|
}
|
|
10245
|
-
if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
|
|
10718
|
+
if (drillDown && scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
|
|
10246
10719
|
console.log(
|
|
10247
10720
|
" " + chalk5.dim(
|
|
10248
10721
|
`${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
|
|
@@ -10250,6 +10723,26 @@ function registerScanCommand(program2) {
|
|
|
10250
10723
|
);
|
|
10251
10724
|
}
|
|
10252
10725
|
console.log("");
|
|
10726
|
+
if (!drillDown) {
|
|
10727
|
+
renderPanelScorecard({
|
|
10728
|
+
scan,
|
|
10729
|
+
summary,
|
|
10730
|
+
blast,
|
|
10731
|
+
blastExposures,
|
|
10732
|
+
blockedCount,
|
|
10733
|
+
reviewCount
|
|
10734
|
+
});
|
|
10735
|
+
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
10736
|
+
console.log(" " + chalk5.green(cta));
|
|
10737
|
+
console.log(
|
|
10738
|
+
" " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 monitor") + chalk5.dim(" live dashboard")
|
|
10739
|
+
);
|
|
10740
|
+
console.log(
|
|
10741
|
+
" " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 scan --drill-down") + chalk5.dim(" full commands + session IDs")
|
|
10742
|
+
);
|
|
10743
|
+
console.log("");
|
|
10744
|
+
return;
|
|
10745
|
+
}
|
|
10253
10746
|
if (scan.dlpFindings.length > 0) {
|
|
10254
10747
|
console.log(" " + chalk5.dim("\u2500".repeat(70)));
|
|
10255
10748
|
console.log(
|
|
@@ -10438,7 +10931,7 @@ function registerScanCommand(program2) {
|
|
|
10438
10931
|
}
|
|
10439
10932
|
);
|
|
10440
10933
|
}
|
|
10441
|
-
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS,
|
|
10934
|
+
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
|
|
10442
10935
|
var init_scan = __esm({
|
|
10443
10936
|
"src/cli/commands/scan.ts"() {
|
|
10444
10937
|
"use strict";
|
|
@@ -10452,6 +10945,7 @@ var init_scan = __esm({
|
|
|
10452
10945
|
init_setup();
|
|
10453
10946
|
init_blast();
|
|
10454
10947
|
init_scan_derive();
|
|
10948
|
+
init_protection();
|
|
10455
10949
|
init_scan_json();
|
|
10456
10950
|
init_scan_history();
|
|
10457
10951
|
CLAUDE_PRICING = {
|
|
@@ -10534,9 +11028,6 @@ var init_scan = __esm({
|
|
|
10534
11028
|
STUCK_TOOLS_LIMIT = 3;
|
|
10535
11029
|
RECURRING_SESSION_THRESHOLD = 3;
|
|
10536
11030
|
STALE_AGE_DAYS = 30;
|
|
10537
|
-
DEFAULT_RULE_NAMES = new Set(
|
|
10538
|
-
DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
|
|
10539
|
-
);
|
|
10540
11031
|
classifyRuleSeverity2 = classifyRuleSeverity;
|
|
10541
11032
|
narrativeRuleLabel2 = narrativeRuleLabel;
|
|
10542
11033
|
}
|
|
@@ -13107,6 +13598,7 @@ var tail_exports = {};
|
|
|
13107
13598
|
__export(tail_exports, {
|
|
13108
13599
|
agentLabel: () => agentLabel,
|
|
13109
13600
|
sessionTag: () => sessionTag,
|
|
13601
|
+
shortenPathSummary: () => shortenPathSummary,
|
|
13110
13602
|
startTail: () => startTail
|
|
13111
13603
|
});
|
|
13112
13604
|
import http2 from "http";
|
|
@@ -13116,6 +13608,12 @@ import os41 from "os";
|
|
|
13116
13608
|
import path47 from "path";
|
|
13117
13609
|
import readline6 from "readline";
|
|
13118
13610
|
import { spawn as spawn9 } from "child_process";
|
|
13611
|
+
function shortenPathSummary(s) {
|
|
13612
|
+
if (!s || !s.startsWith("/")) return s;
|
|
13613
|
+
const parts = s.split("/").filter(Boolean);
|
|
13614
|
+
if (parts.length <= 2) return s;
|
|
13615
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
13616
|
+
}
|
|
13119
13617
|
function getIcon(tool) {
|
|
13120
13618
|
const t = tool.toLowerCase();
|
|
13121
13619
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -13869,7 +14367,8 @@ async function startTail(options = {}) {
|
|
|
13869
14367
|
if (event === "snapshot") {
|
|
13870
14368
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
13871
14369
|
const hash = data.hash ?? "";
|
|
13872
|
-
const
|
|
14370
|
+
const rawSummary = data.argsSummary ?? data.tool;
|
|
14371
|
+
const summary = shortenPathSummary(rawSummary);
|
|
13873
14372
|
const fileCount = data.fileCount ?? 0;
|
|
13874
14373
|
const files = fileCount > 0 ? chalk30.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
13875
14374
|
process.stdout.write(
|
|
@@ -16251,63 +16750,13 @@ function registerAuditCommand(program2) {
|
|
|
16251
16750
|
|
|
16252
16751
|
// src/cli/commands/report.ts
|
|
16253
16752
|
import chalk13 from "chalk";
|
|
16753
|
+
|
|
16754
|
+
// src/cli/aggregate/report-audit.ts
|
|
16755
|
+
init_costSync();
|
|
16756
|
+
init_litellm();
|
|
16254
16757
|
import fs35 from "fs";
|
|
16255
|
-
import path36 from "path";
|
|
16256
16758
|
import os31 from "os";
|
|
16257
|
-
|
|
16258
|
-
// src/cli/render/report-json.ts
|
|
16259
|
-
function buildReportJson(input) {
|
|
16260
|
-
const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
|
|
16261
|
-
const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
|
|
16262
|
-
const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
|
|
16263
|
-
return {
|
|
16264
|
-
schemaVersion: 1,
|
|
16265
|
-
generatedAt: input.generatedAt,
|
|
16266
|
-
period: input.period,
|
|
16267
|
-
range: { start: input.start.toISOString(), end: input.end.toISOString() },
|
|
16268
|
-
excludedTests: input.excludedTests,
|
|
16269
|
-
totals: {
|
|
16270
|
-
events: input.total,
|
|
16271
|
-
blocked: totalBlocked,
|
|
16272
|
-
blockRate,
|
|
16273
|
-
userApproved: input.userApproved,
|
|
16274
|
-
userDenied: input.userDenied,
|
|
16275
|
-
timedOut: input.timedOut,
|
|
16276
|
-
hardBlocked: input.hardBlocked,
|
|
16277
|
-
dlpBlocked: input.dlpBlocked,
|
|
16278
|
-
observeDlp: input.observeDlp,
|
|
16279
|
-
loopHits: input.loopHits,
|
|
16280
|
-
unackedDlp: input.unackedDlp
|
|
16281
|
-
},
|
|
16282
|
-
tests: {
|
|
16283
|
-
passes: input.testPasses,
|
|
16284
|
-
fails: input.testFails
|
|
16285
|
-
},
|
|
16286
|
-
cost: {
|
|
16287
|
-
totalUSD: input.cost.claudeUSD + input.cost.codexUSD,
|
|
16288
|
-
claudeUSD: input.cost.claudeUSD,
|
|
16289
|
-
codexUSD: input.cost.codexUSD,
|
|
16290
|
-
inputTokens: input.cost.inputTokens,
|
|
16291
|
-
outputTokens: input.cost.outputTokens,
|
|
16292
|
-
cacheWriteTokens: input.cost.cacheWriteTokens,
|
|
16293
|
-
cacheReadTokens: input.cost.cacheReadTokens,
|
|
16294
|
-
byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
|
|
16295
|
-
byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
|
|
16296
|
-
},
|
|
16297
|
-
byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
|
|
16298
|
-
byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
|
|
16299
|
-
byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
|
|
16300
|
-
byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
|
|
16301
|
-
byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
|
|
16302
|
-
byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
|
|
16303
|
-
trend: {
|
|
16304
|
-
priorBlockRate: input.priorBlockRate,
|
|
16305
|
-
deltaPct
|
|
16306
|
-
}
|
|
16307
|
-
};
|
|
16308
|
-
}
|
|
16309
|
-
|
|
16310
|
-
// src/cli/commands/report.ts
|
|
16759
|
+
import path36 from "path";
|
|
16311
16760
|
var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
16312
16761
|
function buildTestTimestamps(allEntries) {
|
|
16313
16762
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -16332,8 +16781,7 @@ function isTestEntry(entry, testTs) {
|
|
|
16332
16781
|
}
|
|
16333
16782
|
return false;
|
|
16334
16783
|
}
|
|
16335
|
-
function getDateRange(period) {
|
|
16336
|
-
const now = /* @__PURE__ */ new Date();
|
|
16784
|
+
function getDateRange(period, now) {
|
|
16337
16785
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
16338
16786
|
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
16339
16787
|
switch (period) {
|
|
@@ -16349,6 +16797,11 @@ function getDateRange(period) {
|
|
|
16349
16797
|
s.setDate(s.getDate() - 29);
|
|
16350
16798
|
return { start: s, end };
|
|
16351
16799
|
}
|
|
16800
|
+
case "90d": {
|
|
16801
|
+
const s = new Date(todayStart);
|
|
16802
|
+
s.setDate(s.getDate() - 89);
|
|
16803
|
+
return { start: s, end };
|
|
16804
|
+
}
|
|
16352
16805
|
case "month":
|
|
16353
16806
|
return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
|
|
16354
16807
|
}
|
|
@@ -16371,40 +16824,6 @@ function isAllow(decision) {
|
|
|
16371
16824
|
function isDlp(checkedBy) {
|
|
16372
16825
|
return !!checkedBy?.includes("dlp");
|
|
16373
16826
|
}
|
|
16374
|
-
var BLOCK_REASON_LABELS = {
|
|
16375
|
-
timeout: "Popup timeout",
|
|
16376
|
-
"smart-rule-block": "Smart rule",
|
|
16377
|
-
"observe-mode-dlp-would-block": "DLP (observe)",
|
|
16378
|
-
"persistent-deny": "Persistent deny",
|
|
16379
|
-
"local-decision": "User denied",
|
|
16380
|
-
"dlp-block": "DLP block",
|
|
16381
|
-
"loop-detected": "Loop detected"
|
|
16382
|
-
};
|
|
16383
|
-
function humanBlockReason(reason) {
|
|
16384
|
-
return BLOCK_REASON_LABELS[reason] ?? reason;
|
|
16385
|
-
}
|
|
16386
|
-
function barStr(value, max, width) {
|
|
16387
|
-
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
16388
|
-
const filled = Math.max(1, Math.round(value / max * width));
|
|
16389
|
-
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
16390
|
-
}
|
|
16391
|
-
function colorBar(value, max, width) {
|
|
16392
|
-
const s = barStr(value, max, width);
|
|
16393
|
-
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
16394
|
-
return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
|
|
16395
|
-
}
|
|
16396
|
-
function fmtDate(d) {
|
|
16397
|
-
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
16398
|
-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
16399
|
-
}
|
|
16400
|
-
function num2(n) {
|
|
16401
|
-
return n.toLocaleString();
|
|
16402
|
-
}
|
|
16403
|
-
function fmtCost2(usd) {
|
|
16404
|
-
if (usd < 1e-3) return "< $0.001";
|
|
16405
|
-
if (usd < 1) return "$" + usd.toFixed(4);
|
|
16406
|
-
return "$" + usd.toFixed(2);
|
|
16407
|
-
}
|
|
16408
16827
|
var CLAUDE_PRICING2 = {
|
|
16409
16828
|
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
16410
16829
|
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
@@ -16424,90 +16843,160 @@ function claudeModelPrice2(model) {
|
|
|
16424
16843
|
}
|
|
16425
16844
|
return null;
|
|
16426
16845
|
}
|
|
16427
|
-
function
|
|
16428
|
-
|
|
16846
|
+
function emptyClaudeCostAccumulator() {
|
|
16847
|
+
return {
|
|
16429
16848
|
total: 0,
|
|
16430
|
-
byDay: /* @__PURE__ */ new Map(),
|
|
16431
|
-
byModel: /* @__PURE__ */ new Map(),
|
|
16432
16849
|
inputTokens: 0,
|
|
16433
16850
|
outputTokens: 0,
|
|
16434
16851
|
cacheWriteTokens: 0,
|
|
16435
|
-
cacheReadTokens: 0
|
|
16852
|
+
cacheReadTokens: 0,
|
|
16853
|
+
byDay: /* @__PURE__ */ new Map(),
|
|
16854
|
+
byModel: /* @__PURE__ */ new Map(),
|
|
16855
|
+
byProject: /* @__PURE__ */ new Map()
|
|
16436
16856
|
};
|
|
16437
|
-
|
|
16438
|
-
|
|
16857
|
+
}
|
|
16858
|
+
function freezeClaudeCost(acc) {
|
|
16859
|
+
return {
|
|
16860
|
+
total: acc.total,
|
|
16861
|
+
byDay: acc.byDay,
|
|
16862
|
+
byModel: acc.byModel,
|
|
16863
|
+
byProject: acc.byProject,
|
|
16864
|
+
inputTokens: acc.inputTokens,
|
|
16865
|
+
outputTokens: acc.outputTokens,
|
|
16866
|
+
cacheWriteTokens: acc.cacheWriteTokens,
|
|
16867
|
+
cacheReadTokens: acc.cacheReadTokens
|
|
16868
|
+
};
|
|
16869
|
+
}
|
|
16870
|
+
function processClaudeCostProject(proj, projectsDir, start, end, acc) {
|
|
16871
|
+
const projPath = path36.join(projectsDir, proj);
|
|
16872
|
+
let files;
|
|
16873
|
+
try {
|
|
16874
|
+
const stat = fs35.statSync(projPath);
|
|
16875
|
+
if (!stat.isDirectory()) return;
|
|
16876
|
+
files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
16877
|
+
} catch {
|
|
16878
|
+
return;
|
|
16879
|
+
}
|
|
16880
|
+
const startMs = start.getTime();
|
|
16881
|
+
for (const file of files) {
|
|
16882
|
+
const filePath = path36.join(projPath, file);
|
|
16883
|
+
try {
|
|
16884
|
+
if (fs35.statSync(filePath).mtimeMs < startMs) continue;
|
|
16885
|
+
} catch {
|
|
16886
|
+
continue;
|
|
16887
|
+
}
|
|
16888
|
+
try {
|
|
16889
|
+
const raw = fs35.readFileSync(filePath, "utf-8");
|
|
16890
|
+
for (const line of raw.split("\n")) {
|
|
16891
|
+
if (!line.trim()) continue;
|
|
16892
|
+
let entry;
|
|
16893
|
+
try {
|
|
16894
|
+
entry = JSON.parse(line);
|
|
16895
|
+
} catch {
|
|
16896
|
+
continue;
|
|
16897
|
+
}
|
|
16898
|
+
if (entry.type !== "assistant") continue;
|
|
16899
|
+
if (!entry.timestamp) continue;
|
|
16900
|
+
const ts = new Date(entry.timestamp);
|
|
16901
|
+
if (ts < start || ts > end) continue;
|
|
16902
|
+
const usage = entry.message?.usage;
|
|
16903
|
+
const model = entry.message?.model;
|
|
16904
|
+
if (!usage || !model) continue;
|
|
16905
|
+
const p = claudeModelPrice2(model);
|
|
16906
|
+
if (!p) continue;
|
|
16907
|
+
const inp = usage.input_tokens ?? 0;
|
|
16908
|
+
const out = usage.output_tokens ?? 0;
|
|
16909
|
+
const cw = usage.cache_creation_input_tokens ?? 0;
|
|
16910
|
+
const cr = usage.cache_read_input_tokens ?? 0;
|
|
16911
|
+
const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
|
|
16912
|
+
acc.total += cost;
|
|
16913
|
+
acc.inputTokens += inp;
|
|
16914
|
+
acc.outputTokens += out;
|
|
16915
|
+
acc.cacheWriteTokens += cw;
|
|
16916
|
+
acc.cacheReadTokens += cr;
|
|
16917
|
+
const dateKey = entry.timestamp.slice(0, 10);
|
|
16918
|
+
acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
|
|
16919
|
+
const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
16920
|
+
acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
|
|
16921
|
+
const projectKey = decodeProjectDirName(proj);
|
|
16922
|
+
const projectRollup = acc.byProject.get(projectKey) ?? {
|
|
16923
|
+
cost: 0,
|
|
16924
|
+
inputTokens: 0,
|
|
16925
|
+
outputTokens: 0
|
|
16926
|
+
};
|
|
16927
|
+
projectRollup.cost += cost;
|
|
16928
|
+
projectRollup.inputTokens += inp;
|
|
16929
|
+
projectRollup.outputTokens += out;
|
|
16930
|
+
acc.byProject.set(projectKey, projectRollup);
|
|
16931
|
+
}
|
|
16932
|
+
} catch {
|
|
16933
|
+
continue;
|
|
16934
|
+
}
|
|
16935
|
+
}
|
|
16936
|
+
}
|
|
16937
|
+
function loadClaudeCost(start, end, projectsDir) {
|
|
16938
|
+
const acc = emptyClaudeCostAccumulator();
|
|
16939
|
+
if (!fs35.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
16439
16940
|
let dirs;
|
|
16440
16941
|
try {
|
|
16441
16942
|
dirs = fs35.readdirSync(projectsDir);
|
|
16442
16943
|
} catch {
|
|
16443
|
-
return
|
|
16944
|
+
return freezeClaudeCost(acc);
|
|
16444
16945
|
}
|
|
16445
|
-
let total = 0;
|
|
16446
|
-
let inputTokens = 0;
|
|
16447
|
-
let outputTokens = 0;
|
|
16448
|
-
let cacheWriteTokens = 0;
|
|
16449
|
-
let cacheReadTokens = 0;
|
|
16450
|
-
const byDay = /* @__PURE__ */ new Map();
|
|
16451
|
-
const byModel = /* @__PURE__ */ new Map();
|
|
16452
16946
|
for (const proj of dirs) {
|
|
16453
|
-
|
|
16454
|
-
|
|
16947
|
+
processClaudeCostProject(proj, projectsDir, start, end, acc);
|
|
16948
|
+
}
|
|
16949
|
+
return freezeClaudeCost(acc);
|
|
16950
|
+
}
|
|
16951
|
+
function processCodexCostFile(filePath, start, end, acc) {
|
|
16952
|
+
let lines;
|
|
16953
|
+
try {
|
|
16954
|
+
lines = fs35.readFileSync(filePath, "utf-8").split("\n");
|
|
16955
|
+
} catch {
|
|
16956
|
+
return;
|
|
16957
|
+
}
|
|
16958
|
+
let sessionStart2 = "";
|
|
16959
|
+
let lastTotalInput = 0;
|
|
16960
|
+
let lastTotalCached = 0;
|
|
16961
|
+
let lastTotalOutput = 0;
|
|
16962
|
+
let sessionToolCalls = 0;
|
|
16963
|
+
for (const line of lines) {
|
|
16964
|
+
if (!line.trim()) continue;
|
|
16965
|
+
let entry;
|
|
16455
16966
|
try {
|
|
16456
|
-
|
|
16457
|
-
if (!stat.isDirectory()) continue;
|
|
16458
|
-
files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
16967
|
+
entry = JSON.parse(line);
|
|
16459
16968
|
} catch {
|
|
16460
16969
|
continue;
|
|
16461
16970
|
}
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
if (ts < start || ts > end) continue;
|
|
16477
|
-
const usage = entry.message?.usage;
|
|
16478
|
-
const model = entry.message?.model;
|
|
16479
|
-
if (!usage || !model) continue;
|
|
16480
|
-
const p = claudeModelPrice2(model);
|
|
16481
|
-
if (!p) continue;
|
|
16482
|
-
const inp = usage.input_tokens ?? 0;
|
|
16483
|
-
const out = usage.output_tokens ?? 0;
|
|
16484
|
-
const cw = usage.cache_creation_input_tokens ?? 0;
|
|
16485
|
-
const cr = usage.cache_read_input_tokens ?? 0;
|
|
16486
|
-
const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
|
|
16487
|
-
total += cost;
|
|
16488
|
-
inputTokens += inp;
|
|
16489
|
-
outputTokens += out;
|
|
16490
|
-
cacheWriteTokens += cw;
|
|
16491
|
-
cacheReadTokens += cr;
|
|
16492
|
-
const dateKey = entry.timestamp.slice(0, 10);
|
|
16493
|
-
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
16494
|
-
const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
16495
|
-
byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
|
|
16496
|
-
}
|
|
16497
|
-
} catch {
|
|
16498
|
-
continue;
|
|
16499
|
-
}
|
|
16971
|
+
const p = entry.payload ?? {};
|
|
16972
|
+
if (entry.type === "session_meta") {
|
|
16973
|
+
sessionStart2 = String(p["timestamp"] ?? "");
|
|
16974
|
+
continue;
|
|
16975
|
+
}
|
|
16976
|
+
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
16977
|
+
const info = p["info"] ?? {};
|
|
16978
|
+
const usage = info["total_token_usage"] ?? {};
|
|
16979
|
+
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
16980
|
+
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
16981
|
+
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
16982
|
+
}
|
|
16983
|
+
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
16984
|
+
sessionToolCalls++;
|
|
16500
16985
|
}
|
|
16501
16986
|
}
|
|
16502
|
-
|
|
16987
|
+
if (!sessionStart2) return;
|
|
16988
|
+
const ts = new Date(sessionStart2);
|
|
16989
|
+
if (ts < start || ts > end) return;
|
|
16990
|
+
const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
|
|
16991
|
+
const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
|
|
16992
|
+
acc.total += cost;
|
|
16993
|
+
acc.toolCalls += sessionToolCalls;
|
|
16994
|
+
const dateKey = sessionStart2.slice(0, 10);
|
|
16995
|
+
acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
|
|
16503
16996
|
}
|
|
16504
|
-
function
|
|
16505
|
-
const sessionsBase = path36.join(os31.homedir(), ".codex", "sessions");
|
|
16506
|
-
const byDay = /* @__PURE__ */ new Map();
|
|
16507
|
-
let total = 0;
|
|
16508
|
-
let toolCalls = 0;
|
|
16509
|
-
if (!fs35.existsSync(sessionsBase)) return { total, byDay, toolCalls };
|
|
16997
|
+
function listCodexSessionFiles(sessionsBase) {
|
|
16510
16998
|
const jsonlFiles = [];
|
|
16999
|
+
if (!fs35.existsSync(sessionsBase)) return jsonlFiles;
|
|
16511
17000
|
try {
|
|
16512
17001
|
for (const year of fs35.readdirSync(sessionsBase)) {
|
|
16513
17002
|
const yearPath = path36.join(sessionsBase, year);
|
|
@@ -16537,495 +17026,742 @@ function loadCodexCost(start, end) {
|
|
|
16537
17026
|
}
|
|
16538
17027
|
}
|
|
16539
17028
|
} catch {
|
|
16540
|
-
return
|
|
17029
|
+
return [];
|
|
16541
17030
|
}
|
|
16542
|
-
|
|
16543
|
-
|
|
17031
|
+
return jsonlFiles;
|
|
17032
|
+
}
|
|
17033
|
+
function loadCodexCost(start, end, sessionsBase) {
|
|
17034
|
+
const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
|
|
17035
|
+
const files = listCodexSessionFiles(sessionsBase);
|
|
17036
|
+
for (const filePath of files) {
|
|
17037
|
+
processCodexCostFile(filePath, start, end, acc);
|
|
17038
|
+
}
|
|
17039
|
+
return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
|
|
17040
|
+
}
|
|
17041
|
+
var GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
|
|
17042
|
+
function geminiPriceFor(model) {
|
|
17043
|
+
let tuple = pricingFor(model);
|
|
17044
|
+
if (!tuple && /^gemini-/i.test(model)) {
|
|
17045
|
+
for (const proxy of GEMINI_FALLBACK_MODELS) {
|
|
17046
|
+
tuple = pricingFor(proxy);
|
|
17047
|
+
if (tuple) break;
|
|
17048
|
+
}
|
|
17049
|
+
}
|
|
17050
|
+
if (!tuple) return null;
|
|
17051
|
+
return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
|
|
17052
|
+
}
|
|
17053
|
+
function emptyGeminiAccumulator() {
|
|
17054
|
+
return {
|
|
17055
|
+
total: 0,
|
|
17056
|
+
inputTokens: 0,
|
|
17057
|
+
outputTokens: 0,
|
|
17058
|
+
cacheReadTokens: 0,
|
|
17059
|
+
byDay: /* @__PURE__ */ new Map(),
|
|
17060
|
+
byProject: /* @__PURE__ */ new Map()
|
|
17061
|
+
};
|
|
17062
|
+
}
|
|
17063
|
+
function freezeGeminiCost(acc) {
|
|
17064
|
+
return {
|
|
17065
|
+
total: acc.total,
|
|
17066
|
+
byDay: acc.byDay,
|
|
17067
|
+
byProject: acc.byProject,
|
|
17068
|
+
inputTokens: acc.inputTokens,
|
|
17069
|
+
outputTokens: acc.outputTokens,
|
|
17070
|
+
cacheReadTokens: acc.cacheReadTokens
|
|
17071
|
+
};
|
|
17072
|
+
}
|
|
17073
|
+
function processGeminiCostFile(filePath, projectKey, start, end, acc) {
|
|
17074
|
+
const startMs = start.getTime();
|
|
17075
|
+
try {
|
|
17076
|
+
if (fs35.statSync(filePath).mtimeMs < startMs) return;
|
|
17077
|
+
} catch {
|
|
17078
|
+
return;
|
|
17079
|
+
}
|
|
17080
|
+
let raw;
|
|
17081
|
+
try {
|
|
17082
|
+
raw = fs35.readFileSync(filePath, "utf-8");
|
|
17083
|
+
} catch {
|
|
17084
|
+
return;
|
|
17085
|
+
}
|
|
17086
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
17087
|
+
for (const line of raw.split("\n")) {
|
|
17088
|
+
if (!line.trim()) continue;
|
|
17089
|
+
let entry;
|
|
16544
17090
|
try {
|
|
16545
|
-
|
|
17091
|
+
entry = JSON.parse(line);
|
|
16546
17092
|
} catch {
|
|
16547
17093
|
continue;
|
|
16548
17094
|
}
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
for (const line of lines) {
|
|
16555
|
-
if (!line.trim()) continue;
|
|
16556
|
-
let entry;
|
|
16557
|
-
try {
|
|
16558
|
-
entry = JSON.parse(line);
|
|
16559
|
-
} catch {
|
|
16560
|
-
continue;
|
|
16561
|
-
}
|
|
16562
|
-
const p = entry.payload ?? {};
|
|
16563
|
-
if (entry.type === "session_meta") {
|
|
16564
|
-
sessionStart2 = String(p["timestamp"] ?? "");
|
|
16565
|
-
continue;
|
|
16566
|
-
}
|
|
16567
|
-
if (entry.type === "event_msg" && p["type"] === "token_count") {
|
|
16568
|
-
const info = p["info"] ?? {};
|
|
16569
|
-
const usage = info["total_token_usage"] ?? {};
|
|
16570
|
-
lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
|
|
16571
|
-
lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
|
|
16572
|
-
lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
|
|
16573
|
-
}
|
|
16574
|
-
if (entry.type === "response_item" && p["type"] === "function_call") {
|
|
16575
|
-
sessionToolCalls++;
|
|
16576
|
-
}
|
|
17095
|
+
if (entry.type !== "gemini") continue;
|
|
17096
|
+
if (!entry.tokens || !entry.model || !entry.timestamp) continue;
|
|
17097
|
+
if (entry.id) {
|
|
17098
|
+
if (seenIds.has(entry.id)) continue;
|
|
17099
|
+
seenIds.add(entry.id);
|
|
16577
17100
|
}
|
|
16578
|
-
|
|
16579
|
-
const ts = new Date(sessionStart2);
|
|
17101
|
+
const ts = new Date(entry.timestamp);
|
|
16580
17102
|
if (ts < start || ts > end) continue;
|
|
16581
|
-
const
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
const
|
|
16586
|
-
|
|
17103
|
+
const price = geminiPriceFor(entry.model);
|
|
17104
|
+
if (!price) continue;
|
|
17105
|
+
const inp = entry.tokens.input ?? 0;
|
|
17106
|
+
const out = entry.tokens.output ?? 0;
|
|
17107
|
+
const cached = Math.min(entry.tokens.cached ?? 0, inp);
|
|
17108
|
+
const fresh = Math.max(0, inp - cached);
|
|
17109
|
+
const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
|
|
17110
|
+
acc.total += cost;
|
|
17111
|
+
acc.inputTokens += inp;
|
|
17112
|
+
acc.outputTokens += out;
|
|
17113
|
+
acc.cacheReadTokens += cached;
|
|
17114
|
+
const dateKey = entry.timestamp.slice(0, 10);
|
|
17115
|
+
acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
|
|
17116
|
+
const rollup = acc.byProject.get(projectKey) ?? {
|
|
17117
|
+
cost: 0,
|
|
17118
|
+
inputTokens: 0,
|
|
17119
|
+
outputTokens: 0
|
|
17120
|
+
};
|
|
17121
|
+
rollup.cost += cost;
|
|
17122
|
+
rollup.inputTokens += inp;
|
|
17123
|
+
rollup.outputTokens += out;
|
|
17124
|
+
acc.byProject.set(projectKey, rollup);
|
|
17125
|
+
}
|
|
17126
|
+
}
|
|
17127
|
+
function listGeminiSessionFiles(geminiTmpDir) {
|
|
17128
|
+
const out = [];
|
|
17129
|
+
let dirs;
|
|
17130
|
+
try {
|
|
17131
|
+
if (!fs35.statSync(geminiTmpDir).isDirectory()) return out;
|
|
17132
|
+
dirs = fs35.readdirSync(geminiTmpDir);
|
|
17133
|
+
} catch {
|
|
17134
|
+
return out;
|
|
17135
|
+
}
|
|
17136
|
+
for (const proj of dirs) {
|
|
17137
|
+
const chatsDir = path36.join(geminiTmpDir, proj, "chats");
|
|
17138
|
+
let files;
|
|
17139
|
+
try {
|
|
17140
|
+
if (!fs35.statSync(chatsDir).isDirectory()) continue;
|
|
17141
|
+
files = fs35.readdirSync(chatsDir);
|
|
17142
|
+
} catch {
|
|
17143
|
+
continue;
|
|
17144
|
+
}
|
|
17145
|
+
for (const f of files) {
|
|
17146
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
17147
|
+
out.push({ projectKey: proj, file: path36.join(chatsDir, f) });
|
|
17148
|
+
}
|
|
16587
17149
|
}
|
|
16588
|
-
return
|
|
17150
|
+
return out;
|
|
17151
|
+
}
|
|
17152
|
+
function loadGeminiCost(start, end, geminiTmpDir) {
|
|
17153
|
+
const acc = emptyGeminiAccumulator();
|
|
17154
|
+
if (!fs35.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
|
|
17155
|
+
for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
|
|
17156
|
+
processGeminiCostFile(file, projectKey, start, end, acc);
|
|
17157
|
+
}
|
|
17158
|
+
return freezeGeminiCost(acc);
|
|
17159
|
+
}
|
|
17160
|
+
function aggregateReportFromAudit(period, opts = {}) {
|
|
17161
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
17162
|
+
const auditLogPath = opts.auditLogPath ?? path36.join(os31.homedir(), ".node9", "audit.log");
|
|
17163
|
+
const claudeProjectsDir = opts.claudeProjectsDir ?? path36.join(os31.homedir(), ".claude", "projects");
|
|
17164
|
+
const codexSessionsDir = opts.codexSessionsDir ?? path36.join(os31.homedir(), ".codex", "sessions");
|
|
17165
|
+
const geminiTmpDir = opts.geminiTmpDir ?? path36.join(os31.homedir(), ".gemini", "tmp");
|
|
17166
|
+
const hasAuditFile = fs35.existsSync(auditLogPath);
|
|
17167
|
+
const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
|
|
17168
|
+
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
17169
|
+
const { start, end } = getDateRange(period, now);
|
|
17170
|
+
const responseDlpEntries = allEntries.filter((e) => {
|
|
17171
|
+
if (e.source !== "response-dlp") return false;
|
|
17172
|
+
const ts = new Date(e.ts);
|
|
17173
|
+
return ts >= start && ts <= end;
|
|
17174
|
+
}).map((e) => {
|
|
17175
|
+
const raw = e;
|
|
17176
|
+
return {
|
|
17177
|
+
ts: e.ts,
|
|
17178
|
+
dlpPattern: typeof raw.dlpPattern === "string" ? raw.dlpPattern : void 0,
|
|
17179
|
+
dlpSample: typeof raw.dlpSample === "string" ? raw.dlpSample : void 0
|
|
17180
|
+
};
|
|
17181
|
+
});
|
|
17182
|
+
const claudeCost = opts.preloadedClaudeCost ?? loadClaudeCost(start, end, claudeProjectsDir);
|
|
17183
|
+
const codexCost = opts.preloadedCodexCost ?? loadCodexCost(start, end, codexSessionsDir);
|
|
17184
|
+
const geminiCost = opts.preloadedGeminiCost ?? loadGeminiCost(start, end, geminiTmpDir);
|
|
17185
|
+
for (const [day, c] of codexCost.byDay) {
|
|
17186
|
+
claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
|
|
17187
|
+
}
|
|
17188
|
+
for (const [day, c] of geminiCost.byDay) {
|
|
17189
|
+
claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
|
|
17190
|
+
}
|
|
17191
|
+
for (const [geminiKey, gRollup] of geminiCost.byProject) {
|
|
17192
|
+
let mergedInto = null;
|
|
17193
|
+
for (const claudeKey of claudeCost.byProject.keys()) {
|
|
17194
|
+
const claudeBase = claudeKey.match(/[^/\\]+$/)?.[0] ?? claudeKey;
|
|
17195
|
+
if (claudeBase === geminiKey) {
|
|
17196
|
+
mergedInto = claudeKey;
|
|
17197
|
+
break;
|
|
17198
|
+
}
|
|
17199
|
+
}
|
|
17200
|
+
const targetKey = mergedInto ?? geminiKey;
|
|
17201
|
+
const existing = claudeCost.byProject.get(targetKey) ?? {
|
|
17202
|
+
cost: 0,
|
|
17203
|
+
inputTokens: 0,
|
|
17204
|
+
outputTokens: 0
|
|
17205
|
+
};
|
|
17206
|
+
existing.cost += gRollup.cost;
|
|
17207
|
+
existing.inputTokens += gRollup.inputTokens;
|
|
17208
|
+
existing.outputTokens += gRollup.outputTokens;
|
|
17209
|
+
claudeCost.byProject.set(targetKey, existing);
|
|
17210
|
+
}
|
|
17211
|
+
const periodMs = end.getTime() - start.getTime();
|
|
17212
|
+
const priorEnd = new Date(start.getTime() - 1);
|
|
17213
|
+
const priorStart = new Date(start.getTime() - periodMs);
|
|
17214
|
+
const priorEntries = allEntries.filter((e) => {
|
|
17215
|
+
if (e.source === "post-hook") return false;
|
|
17216
|
+
const ts = new Date(e.ts);
|
|
17217
|
+
return ts >= priorStart && ts <= priorEnd;
|
|
17218
|
+
});
|
|
17219
|
+
const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
|
|
17220
|
+
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
17221
|
+
const excludeTests = opts.excludeTests === true;
|
|
17222
|
+
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
17223
|
+
let excludedTests = 0;
|
|
17224
|
+
const entries = allEntries.filter((e) => {
|
|
17225
|
+
if (e.source === "post-hook") return false;
|
|
17226
|
+
if (e.source === "response-dlp") return false;
|
|
17227
|
+
const ts = new Date(e.ts);
|
|
17228
|
+
if (ts < start || ts > end) return false;
|
|
17229
|
+
if (excludeTests && isTestEntry(e, testTs)) {
|
|
17230
|
+
excludedTests++;
|
|
17231
|
+
return false;
|
|
17232
|
+
}
|
|
17233
|
+
return true;
|
|
17234
|
+
});
|
|
17235
|
+
let userApproved = 0;
|
|
17236
|
+
let userDenied = 0;
|
|
17237
|
+
let timedOut = 0;
|
|
17238
|
+
let hardBlocked = 0;
|
|
17239
|
+
let dlpBlocked = 0;
|
|
17240
|
+
let observeDlp = 0;
|
|
17241
|
+
let loopHits = 0;
|
|
17242
|
+
let testPasses = 0;
|
|
17243
|
+
let testFails = 0;
|
|
17244
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
17245
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
17246
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
17247
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
17248
|
+
const mcpMap = /* @__PURE__ */ new Map();
|
|
17249
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
17250
|
+
const hourMap = /* @__PURE__ */ new Map();
|
|
17251
|
+
for (const e of entries) {
|
|
17252
|
+
const allow = isAllow(e.decision);
|
|
17253
|
+
const dateKey = e.ts.slice(0, 10);
|
|
17254
|
+
const userInteracted = e.source === "daemon";
|
|
17255
|
+
if (userInteracted) {
|
|
17256
|
+
if (allow) userApproved++;
|
|
17257
|
+
else userDenied++;
|
|
17258
|
+
} else if (!allow) {
|
|
17259
|
+
if (e.checkedBy === "timeout") timedOut++;
|
|
17260
|
+
else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
17261
|
+
else if (isDlp(e.checkedBy)) dlpBlocked++;
|
|
17262
|
+
else if (e.checkedBy !== "loop-detected") hardBlocked++;
|
|
17263
|
+
}
|
|
17264
|
+
if (e.checkedBy === "loop-detected") loopHits++;
|
|
17265
|
+
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
17266
|
+
t.calls++;
|
|
17267
|
+
if (!allow) t.blocked++;
|
|
17268
|
+
toolMap.set(e.tool, t);
|
|
17269
|
+
if (!allow && e.checkedBy) {
|
|
17270
|
+
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
17271
|
+
}
|
|
17272
|
+
if (!allow && e.ruleName) {
|
|
17273
|
+
ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
|
|
17274
|
+
}
|
|
17275
|
+
if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
|
|
17276
|
+
if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
|
|
17277
|
+
const hour = new Date(e.ts).getHours();
|
|
17278
|
+
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
17279
|
+
const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
|
|
17280
|
+
d.calls++;
|
|
17281
|
+
if (!allow) d.blocked++;
|
|
17282
|
+
dailyMap.set(dateKey, d);
|
|
17283
|
+
}
|
|
17284
|
+
for (const e of allEntries) {
|
|
17285
|
+
if (e.source !== "test-result") continue;
|
|
17286
|
+
const ts = new Date(e.ts);
|
|
17287
|
+
if (ts < start || ts > end) continue;
|
|
17288
|
+
if (e.testResult === "pass") testPasses++;
|
|
17289
|
+
else if (e.testResult === "fail") testFails++;
|
|
17290
|
+
}
|
|
17291
|
+
if (codexCost.toolCalls > 0) {
|
|
17292
|
+
agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexCost.toolCalls);
|
|
17293
|
+
}
|
|
17294
|
+
const data = {
|
|
17295
|
+
period,
|
|
17296
|
+
start,
|
|
17297
|
+
end,
|
|
17298
|
+
excludedTests,
|
|
17299
|
+
total: entries.length,
|
|
17300
|
+
userApproved,
|
|
17301
|
+
userDenied,
|
|
17302
|
+
timedOut,
|
|
17303
|
+
hardBlocked,
|
|
17304
|
+
dlpBlocked,
|
|
17305
|
+
observeDlp,
|
|
17306
|
+
loopHits,
|
|
17307
|
+
testPasses,
|
|
17308
|
+
testFails,
|
|
17309
|
+
unackedDlp: unackedDlp.length,
|
|
17310
|
+
priorBlockRate,
|
|
17311
|
+
cost: {
|
|
17312
|
+
claudeUSD: claudeCost.total,
|
|
17313
|
+
codexUSD: codexCost.total,
|
|
17314
|
+
geminiUSD: geminiCost.total,
|
|
17315
|
+
inputTokens: claudeCost.inputTokens + geminiCost.inputTokens,
|
|
17316
|
+
outputTokens: claudeCost.outputTokens + geminiCost.outputTokens,
|
|
17317
|
+
cacheWriteTokens: claudeCost.cacheWriteTokens,
|
|
17318
|
+
cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
|
|
17319
|
+
byDay: claudeCost.byDay,
|
|
17320
|
+
byModel: claudeCost.byModel,
|
|
17321
|
+
byProject: claudeCost.byProject
|
|
17322
|
+
},
|
|
17323
|
+
toolMap,
|
|
17324
|
+
blockMap,
|
|
17325
|
+
ruleMap,
|
|
17326
|
+
agentMap,
|
|
17327
|
+
mcpMap,
|
|
17328
|
+
dailyMap,
|
|
17329
|
+
hourMap,
|
|
17330
|
+
generatedAt: now.toISOString()
|
|
17331
|
+
};
|
|
17332
|
+
return { data, hasAuditFile, responseDlpEntries };
|
|
17333
|
+
}
|
|
17334
|
+
|
|
17335
|
+
// src/cli/render/report-json.ts
|
|
17336
|
+
function buildReportJson(input) {
|
|
17337
|
+
const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
|
|
17338
|
+
const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
|
|
17339
|
+
const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
|
|
17340
|
+
return {
|
|
17341
|
+
schemaVersion: 1,
|
|
17342
|
+
generatedAt: input.generatedAt,
|
|
17343
|
+
period: input.period,
|
|
17344
|
+
range: { start: input.start.toISOString(), end: input.end.toISOString() },
|
|
17345
|
+
excludedTests: input.excludedTests,
|
|
17346
|
+
totals: {
|
|
17347
|
+
events: input.total,
|
|
17348
|
+
blocked: totalBlocked,
|
|
17349
|
+
blockRate,
|
|
17350
|
+
userApproved: input.userApproved,
|
|
17351
|
+
userDenied: input.userDenied,
|
|
17352
|
+
timedOut: input.timedOut,
|
|
17353
|
+
hardBlocked: input.hardBlocked,
|
|
17354
|
+
dlpBlocked: input.dlpBlocked,
|
|
17355
|
+
observeDlp: input.observeDlp,
|
|
17356
|
+
loopHits: input.loopHits,
|
|
17357
|
+
unackedDlp: input.unackedDlp
|
|
17358
|
+
},
|
|
17359
|
+
tests: {
|
|
17360
|
+
passes: input.testPasses,
|
|
17361
|
+
fails: input.testFails
|
|
17362
|
+
},
|
|
17363
|
+
cost: {
|
|
17364
|
+
totalUSD: input.cost.claudeUSD + input.cost.codexUSD + input.cost.geminiUSD,
|
|
17365
|
+
claudeUSD: input.cost.claudeUSD,
|
|
17366
|
+
codexUSD: input.cost.codexUSD,
|
|
17367
|
+
geminiUSD: input.cost.geminiUSD,
|
|
17368
|
+
inputTokens: input.cost.inputTokens,
|
|
17369
|
+
outputTokens: input.cost.outputTokens,
|
|
17370
|
+
cacheWriteTokens: input.cost.cacheWriteTokens,
|
|
17371
|
+
cacheReadTokens: input.cost.cacheReadTokens,
|
|
17372
|
+
byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
|
|
17373
|
+
byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
|
|
17374
|
+
},
|
|
17375
|
+
byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
|
|
17376
|
+
byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
|
|
17377
|
+
byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
|
|
17378
|
+
byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
|
|
17379
|
+
byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
|
|
17380
|
+
byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
|
|
17381
|
+
trend: {
|
|
17382
|
+
priorBlockRate: input.priorBlockRate,
|
|
17383
|
+
deltaPct
|
|
17384
|
+
}
|
|
17385
|
+
};
|
|
17386
|
+
}
|
|
17387
|
+
|
|
17388
|
+
// src/cli/commands/report.ts
|
|
17389
|
+
var BLOCK_REASON_LABELS = {
|
|
17390
|
+
timeout: "Approval timeout",
|
|
17391
|
+
"smart-rule-block": "Smart rule",
|
|
17392
|
+
"observe-mode-dlp-would-block": "DLP (observe)",
|
|
17393
|
+
"persistent-deny": "Persistent deny",
|
|
17394
|
+
"local-decision": "User denied",
|
|
17395
|
+
"dlp-block": "DLP block",
|
|
17396
|
+
"loop-detected": "Loop detected"
|
|
17397
|
+
};
|
|
17398
|
+
function humanBlockReason(reason) {
|
|
17399
|
+
return BLOCK_REASON_LABELS[reason] ?? reason;
|
|
17400
|
+
}
|
|
17401
|
+
function barStr(value, max, width) {
|
|
17402
|
+
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
17403
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
17404
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
17405
|
+
}
|
|
17406
|
+
function colorBar(value, max, width) {
|
|
17407
|
+
const s = barStr(value, max, width);
|
|
17408
|
+
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
17409
|
+
return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
|
|
17410
|
+
}
|
|
17411
|
+
function fmtDate(d) {
|
|
17412
|
+
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
17413
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
17414
|
+
}
|
|
17415
|
+
function num2(n) {
|
|
17416
|
+
return n.toLocaleString();
|
|
17417
|
+
}
|
|
17418
|
+
function fmtCost2(usd) {
|
|
17419
|
+
if (usd < 1e-3) return "< $0.001";
|
|
17420
|
+
if (usd < 1) return "$" + usd.toFixed(4);
|
|
17421
|
+
return "$" + usd.toFixed(2);
|
|
16589
17422
|
}
|
|
16590
17423
|
function registerReportCommand(program2) {
|
|
16591
17424
|
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").option("--json", "Emit machine-readable JSON to stdout (suppresses renderer)").action((options) => {
|
|
16592
|
-
const period = ["today", "7d", "30d", "month"].includes(
|
|
17425
|
+
const period = ["today", "7d", "30d", "90d", "month"].includes(
|
|
16593
17426
|
options.period
|
|
16594
17427
|
) ? options.period : "7d";
|
|
16595
|
-
const
|
|
16596
|
-
const
|
|
16597
|
-
|
|
16598
|
-
|
|
17428
|
+
const excludeTests = options.tests === false;
|
|
17429
|
+
const { data, hasAuditFile, responseDlpEntries } = aggregateReportFromAudit(period, {
|
|
17430
|
+
excludeTests
|
|
17431
|
+
});
|
|
17432
|
+
if (data.unackedDlp > 0 && !options.json) {
|
|
16599
17433
|
console.log("");
|
|
16600
17434
|
console.log(
|
|
16601
17435
|
chalk13.bgRed.white.bold(
|
|
16602
|
-
` \u26A0\uFE0F DLP ALERT: ${unackedDlp
|
|
17436
|
+
` \u26A0\uFE0F DLP ALERT: ${data.unackedDlp} secret${data.unackedDlp !== 1 ? "s" : ""} found in Claude response text `
|
|
16603
17437
|
) + " " + chalk13.yellow("\u2192 run: node9 dlp")
|
|
16604
17438
|
);
|
|
16605
17439
|
}
|
|
16606
|
-
if (
|
|
17440
|
+
if (!hasAuditFile && !options.json) {
|
|
16607
17441
|
console.log(
|
|
16608
17442
|
chalk13.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
|
|
16609
17443
|
);
|
|
16610
17444
|
return;
|
|
16611
17445
|
}
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
outputTokens: costOutputTokens,
|
|
16619
|
-
cacheWriteTokens: costCacheWrite,
|
|
16620
|
-
cacheReadTokens: costCacheRead
|
|
16621
|
-
} = loadClaudeCost(start, end);
|
|
16622
|
-
const {
|
|
16623
|
-
total: codexCostUSD,
|
|
16624
|
-
byDay: codexCostByDay,
|
|
16625
|
-
toolCalls: codexToolCalls
|
|
16626
|
-
} = loadCodexCost(start, end);
|
|
16627
|
-
const costUSD = claudeCostUSD + codexCostUSD;
|
|
16628
|
-
for (const [day, c] of codexCostByDay) {
|
|
16629
|
-
costByDay.set(day, (costByDay.get(day) ?? 0) + c);
|
|
16630
|
-
}
|
|
16631
|
-
const periodMs = end.getTime() - start.getTime();
|
|
16632
|
-
const priorEnd = new Date(start.getTime() - 1);
|
|
16633
|
-
const priorStart = new Date(start.getTime() - periodMs);
|
|
16634
|
-
const priorEntries = allEntries.filter((e) => {
|
|
16635
|
-
if (e.source === "post-hook") return false;
|
|
16636
|
-
const ts = new Date(e.ts);
|
|
16637
|
-
return ts >= priorStart && ts <= priorEnd;
|
|
16638
|
-
});
|
|
16639
|
-
const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
|
|
16640
|
-
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
16641
|
-
const excludeTests = options.tests === false;
|
|
16642
|
-
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
16643
|
-
let filteredTestCount = 0;
|
|
16644
|
-
const entries = allEntries.filter((e) => {
|
|
16645
|
-
if (e.source === "post-hook") return false;
|
|
16646
|
-
if (e.source === "response-dlp") return false;
|
|
16647
|
-
const ts = new Date(e.ts);
|
|
16648
|
-
if (ts < start || ts > end) return false;
|
|
16649
|
-
if (excludeTests && isTestEntry(e, testTs)) {
|
|
16650
|
-
filteredTestCount++;
|
|
16651
|
-
return false;
|
|
16652
|
-
}
|
|
16653
|
-
return true;
|
|
16654
|
-
});
|
|
16655
|
-
if (entries.length === 0 && !options.json) {
|
|
17446
|
+
if (options.json) {
|
|
17447
|
+
const envelope = buildReportJson(data);
|
|
17448
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
17449
|
+
return;
|
|
17450
|
+
}
|
|
17451
|
+
if (data.total === 0) {
|
|
16656
17452
|
console.log(chalk13.yellow(`
|
|
16657
17453
|
No activity for period "${period}".
|
|
16658
17454
|
`));
|
|
16659
17455
|
return;
|
|
16660
17456
|
}
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
16708
|
-
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
}
|
|
17457
|
+
renderTerminalReport(data, responseDlpEntries, excludeTests);
|
|
17458
|
+
});
|
|
17459
|
+
}
|
|
17460
|
+
function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
17461
|
+
const {
|
|
17462
|
+
period,
|
|
17463
|
+
start,
|
|
17464
|
+
end,
|
|
17465
|
+
total,
|
|
17466
|
+
excludedTests,
|
|
17467
|
+
userApproved,
|
|
17468
|
+
userDenied,
|
|
17469
|
+
timedOut,
|
|
17470
|
+
hardBlocked,
|
|
17471
|
+
dlpBlocked,
|
|
17472
|
+
observeDlp,
|
|
17473
|
+
loopHits,
|
|
17474
|
+
testPasses,
|
|
17475
|
+
testFails,
|
|
17476
|
+
priorBlockRate,
|
|
17477
|
+
cost: {
|
|
17478
|
+
claudeUSD,
|
|
17479
|
+
codexUSD,
|
|
17480
|
+
geminiUSD,
|
|
17481
|
+
inputTokens: costInputTokens,
|
|
17482
|
+
outputTokens: costOutputTokens,
|
|
17483
|
+
cacheWriteTokens: costCacheWrite,
|
|
17484
|
+
cacheReadTokens: costCacheRead,
|
|
17485
|
+
byDay: costByDay,
|
|
17486
|
+
byModel: costByModel
|
|
17487
|
+
},
|
|
17488
|
+
toolMap,
|
|
17489
|
+
blockMap,
|
|
17490
|
+
agentMap,
|
|
17491
|
+
mcpMap,
|
|
17492
|
+
dailyMap,
|
|
17493
|
+
hourMap
|
|
17494
|
+
} = data;
|
|
17495
|
+
const costUSD = claudeUSD + codexUSD + geminiUSD;
|
|
17496
|
+
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
17497
|
+
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
17498
|
+
const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
|
|
17499
|
+
const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
|
|
17500
|
+
const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
|
|
17501
|
+
const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
|
|
17502
|
+
const W = Math.min(process.stdout.columns || 80, 100);
|
|
17503
|
+
const INNER = W - 4;
|
|
17504
|
+
const COL = Math.floor(INNER / 2) - 1;
|
|
17505
|
+
const LABEL = 24;
|
|
17506
|
+
const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
|
|
17507
|
+
const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
|
|
17508
|
+
const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
|
|
17509
|
+
const line = chalk13.dim("\u2500".repeat(W - 2));
|
|
17510
|
+
const periodLabel = {
|
|
17511
|
+
today: "Today",
|
|
17512
|
+
"7d": "Last 7 Days",
|
|
17513
|
+
"30d": "Last 30 Days",
|
|
17514
|
+
"90d": "Last 90 Days",
|
|
17515
|
+
month: "This Month"
|
|
17516
|
+
};
|
|
17517
|
+
console.log("");
|
|
17518
|
+
console.log(
|
|
17519
|
+
" " + chalk13.bold.cyan("\u{1F6E1} node9 Report") + chalk13.dim(" \xB7 ") + chalk13.white(periodLabel[period]) + chalk13.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk13.dim(` ${num2(total)} events`) + (excludeTests ? chalk13.dim(` \u2013tests (\u2013${excludedTests})`) : "")
|
|
17520
|
+
);
|
|
17521
|
+
console.log(" " + line);
|
|
17522
|
+
const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
|
|
17523
|
+
const currentRate = total > 0 ? totalBlocked / total : 0;
|
|
17524
|
+
const trendLabel = (() => {
|
|
17525
|
+
if (priorBlockRate === null) return "";
|
|
17526
|
+
const delta = Math.round((currentRate - priorBlockRate) * 100);
|
|
17527
|
+
if (delta === 0) return "";
|
|
17528
|
+
return " " + (delta > 0 ? chalk13.red(`\u25B2${delta}%`) : chalk13.green(`\u25BC${Math.abs(delta)}%`)) + chalk13.dim(" vs prior");
|
|
17529
|
+
})();
|
|
17530
|
+
const reads = toolMap.get("Read")?.calls ?? 0;
|
|
17531
|
+
const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
|
|
17532
|
+
const ratioLabel = reads > 0 ? chalk13.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk13.dim("edit/read \u2013");
|
|
17533
|
+
const testLabel = testPasses + testFails > 0 ? chalk13.dim("tests ") + chalk13.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk13.red(`${testFails}\u2717`) : "") : chalk13.dim("tests \u2013");
|
|
17534
|
+
console.log("");
|
|
17535
|
+
console.log(" " + chalk13.bold("Protection Summary"));
|
|
17536
|
+
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
17537
|
+
console.log(
|
|
17538
|
+
" " + chalk13.dim("Intercepted") + " " + chalk13.white(num2(total)) + chalk13.dim(" tool calls")
|
|
17539
|
+
);
|
|
17540
|
+
console.log("");
|
|
17541
|
+
const COL1 = 18;
|
|
17542
|
+
const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
|
|
17543
|
+
const countStr = colorFn(num2(count));
|
|
17544
|
+
const noteStr = note ? chalk13.dim(" " + note) : "";
|
|
17545
|
+
console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
|
|
17546
|
+
};
|
|
17547
|
+
summaryRow(
|
|
17548
|
+
userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
|
|
17549
|
+
"User approved",
|
|
17550
|
+
userApproved,
|
|
17551
|
+
userApproved === 0 ? "no popups this period" : void 0,
|
|
17552
|
+
userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
|
|
17553
|
+
);
|
|
17554
|
+
summaryRow(
|
|
17555
|
+
userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
|
|
17556
|
+
"User denied",
|
|
17557
|
+
userDenied,
|
|
17558
|
+
void 0,
|
|
17559
|
+
userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
|
|
17560
|
+
);
|
|
17561
|
+
summaryRow(
|
|
17562
|
+
timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
|
|
17563
|
+
"Timed out",
|
|
17564
|
+
timedOut,
|
|
17565
|
+
timedOut > 0 ? "no approval response" : void 0,
|
|
17566
|
+
timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
17567
|
+
);
|
|
17568
|
+
summaryRow(
|
|
17569
|
+
hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
|
|
17570
|
+
"Auto-blocked",
|
|
17571
|
+
hardBlocked,
|
|
17572
|
+
void 0,
|
|
17573
|
+
hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
|
|
17574
|
+
);
|
|
17575
|
+
summaryRow(
|
|
17576
|
+
dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
|
|
17577
|
+
"DLP blocked",
|
|
17578
|
+
dlpBlocked,
|
|
17579
|
+
void 0,
|
|
17580
|
+
dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
17581
|
+
);
|
|
17582
|
+
summaryRow(
|
|
17583
|
+
observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
|
|
17584
|
+
"DLP (observe)",
|
|
17585
|
+
observeDlp,
|
|
17586
|
+
observeDlp > 0 ? "would-block in strict mode" : void 0,
|
|
17587
|
+
observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
|
|
17588
|
+
);
|
|
17589
|
+
summaryRow(
|
|
17590
|
+
loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
|
|
17591
|
+
"Loops detected",
|
|
17592
|
+
loopHits,
|
|
17593
|
+
void 0,
|
|
17594
|
+
loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
17595
|
+
);
|
|
17596
|
+
if (trendLabel || ratioLabel || testPasses + testFails > 0) {
|
|
16774
17597
|
console.log("");
|
|
16775
|
-
console.log(
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
17598
|
+
console.log(" " + ratioLabel + " " + testLabel + trendLabel);
|
|
17599
|
+
}
|
|
17600
|
+
console.log("");
|
|
17601
|
+
const toolHeaderRaw = "Top Tools";
|
|
17602
|
+
const blockHeaderRaw = "Top Blocks";
|
|
17603
|
+
console.log(
|
|
17604
|
+
" " + chalk13.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk13.bold(blockHeaderRaw)
|
|
17605
|
+
);
|
|
17606
|
+
console.log(" " + chalk13.dim("\u2500".repeat(COL)) + " " + chalk13.dim("\u2500".repeat(COL)));
|
|
17607
|
+
const rows = Math.max(topTools.length, topBlocks.length, 1);
|
|
17608
|
+
for (let i = 0; i < rows; i++) {
|
|
17609
|
+
let leftStyled = " ".repeat(COL);
|
|
17610
|
+
if (i < topTools.length) {
|
|
17611
|
+
const [tool, { calls }] = topTools[i];
|
|
17612
|
+
const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
|
|
17613
|
+
const countStr = num2(calls).padStart(TOOL_COUNT_W);
|
|
17614
|
+
const b = colorBar(calls, maxTool, BAR);
|
|
17615
|
+
const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
|
|
17616
|
+
const pad = Math.max(0, COL - rawLen);
|
|
17617
|
+
leftStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
|
|
17618
|
+
}
|
|
17619
|
+
let rightStyled = "";
|
|
17620
|
+
if (i < topBlocks.length) {
|
|
17621
|
+
const [reason, count] = topBlocks[i];
|
|
17622
|
+
const readable = humanBlockReason(reason);
|
|
17623
|
+
const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
17624
|
+
const countStr = num2(count).padStart(BLOCK_COUNT_W);
|
|
17625
|
+
const b = colorBar(count, maxBlock, BAR);
|
|
17626
|
+
rightStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
|
|
17627
|
+
}
|
|
17628
|
+
console.log(" " + leftStyled + " " + rightStyled);
|
|
17629
|
+
}
|
|
17630
|
+
if (topBlocks.length === 0) {
|
|
17631
|
+
console.log(" " + " ".repeat(COL) + " " + chalk13.dim("nothing blocked \u2713"));
|
|
17632
|
+
}
|
|
17633
|
+
if (agentMap.size >= 1) {
|
|
16791
17634
|
console.log("");
|
|
16792
|
-
console.log(" " + chalk13.bold("
|
|
17635
|
+
console.log(" " + chalk13.bold("Agents"));
|
|
16793
17636
|
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
|
|
16800
|
-
const countStr = colorFn(num2(count));
|
|
16801
|
-
const noteStr = note ? chalk13.dim(" " + note) : "";
|
|
16802
|
-
console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
|
|
16803
|
-
};
|
|
16804
|
-
summaryRow(
|
|
16805
|
-
userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
|
|
16806
|
-
"User approved",
|
|
16807
|
-
userApproved,
|
|
16808
|
-
userApproved === 0 ? "no popups this period" : void 0,
|
|
16809
|
-
userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
|
|
16810
|
-
);
|
|
16811
|
-
summaryRow(
|
|
16812
|
-
userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
|
|
16813
|
-
"User denied",
|
|
16814
|
-
userDenied,
|
|
16815
|
-
void 0,
|
|
16816
|
-
userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
|
|
16817
|
-
);
|
|
16818
|
-
summaryRow(
|
|
16819
|
-
timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
|
|
16820
|
-
"Timed out",
|
|
16821
|
-
timedOut,
|
|
16822
|
-
timedOut > 0 ? "no approval response" : void 0,
|
|
16823
|
-
timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
16824
|
-
);
|
|
16825
|
-
summaryRow(
|
|
16826
|
-
hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
|
|
16827
|
-
"Auto-blocked",
|
|
16828
|
-
hardBlocked,
|
|
16829
|
-
void 0,
|
|
16830
|
-
hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
|
|
16831
|
-
);
|
|
16832
|
-
summaryRow(
|
|
16833
|
-
dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
|
|
16834
|
-
"DLP blocked",
|
|
16835
|
-
dlpBlocked,
|
|
16836
|
-
void 0,
|
|
16837
|
-
dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
16838
|
-
);
|
|
16839
|
-
summaryRow(
|
|
16840
|
-
observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
|
|
16841
|
-
"DLP (observe)",
|
|
16842
|
-
observeDlp,
|
|
16843
|
-
observeDlp > 0 ? "would-block in strict mode" : void 0,
|
|
16844
|
-
observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
|
|
16845
|
-
);
|
|
16846
|
-
summaryRow(
|
|
16847
|
-
loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
|
|
16848
|
-
"Loops detected",
|
|
16849
|
-
loopHits,
|
|
16850
|
-
void 0,
|
|
16851
|
-
loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
|
|
16852
|
-
);
|
|
16853
|
-
if (trendLabel || ratioLabel || testPasses + testFails > 0) {
|
|
16854
|
-
console.log("");
|
|
16855
|
-
console.log(" " + ratioLabel + " " + testLabel + trendLabel);
|
|
17637
|
+
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
17638
|
+
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
17639
|
+
const label = agent.slice(0, LABEL - 1);
|
|
17640
|
+
const b = colorBar(count, maxAgent, BAR);
|
|
17641
|
+
console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
|
|
16856
17642
|
}
|
|
17643
|
+
}
|
|
17644
|
+
if (mcpMap.size > 0) {
|
|
16857
17645
|
console.log("");
|
|
16858
|
-
|
|
16859
|
-
|
|
16860
|
-
|
|
16861
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
16894
|
-
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
16895
|
-
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
16896
|
-
const label = agent.slice(0, LABEL - 1);
|
|
16897
|
-
const b = colorBar(count, maxAgent, BAR);
|
|
16898
|
-
console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
|
|
16899
|
-
}
|
|
16900
|
-
}
|
|
16901
|
-
if (mcpMap.size > 0) {
|
|
16902
|
-
console.log("");
|
|
16903
|
-
console.log(" " + chalk13.bold("MCP Servers"));
|
|
16904
|
-
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
16905
|
-
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
16906
|
-
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
16907
|
-
const label = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
16908
|
-
const b = colorBar(count, maxMcp, BAR);
|
|
16909
|
-
console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
|
|
16910
|
-
}
|
|
16911
|
-
}
|
|
16912
|
-
if (hourMap.size > 0) {
|
|
16913
|
-
const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
16914
|
-
const maxHour = Math.max(...hourMap.values(), 1);
|
|
16915
|
-
const bar = Array.from({ length: 24 }, (_, h) => {
|
|
16916
|
-
const v = hourMap.get(h) ?? 0;
|
|
16917
|
-
return BLOCKS[Math.round(v / maxHour * 8)];
|
|
16918
|
-
}).join("");
|
|
16919
|
-
console.log("");
|
|
16920
|
-
console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
|
|
16921
|
-
console.log(" " + chalk13.cyan(bar));
|
|
16922
|
-
console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
|
|
16923
|
-
}
|
|
16924
|
-
if (dailyList.length > 1) {
|
|
16925
|
-
console.log("");
|
|
16926
|
-
console.log(" " + chalk13.bold("Daily Activity"));
|
|
16927
|
-
console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
|
|
16928
|
-
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
16929
|
-
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
16930
|
-
const label = fmtDate(dateKey).padEnd(10);
|
|
16931
|
-
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
16932
|
-
const dayCost = costByDay.get(dateKey);
|
|
16933
|
-
const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
|
|
16934
|
-
const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
|
|
16935
|
-
console.log(
|
|
16936
|
-
" " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
|
|
16937
|
-
);
|
|
16938
|
-
}
|
|
16939
|
-
}
|
|
16940
|
-
const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
|
|
16941
|
-
if (totalTokens > 0) {
|
|
16942
|
-
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
16943
|
-
console.log("");
|
|
16944
|
-
console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
|
|
16945
|
-
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
16946
|
-
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
16947
|
-
const TOK_LABEL = 14;
|
|
16948
|
-
const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
|
|
16949
|
-
const nonCacheRows = [
|
|
16950
|
-
["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
|
|
16951
|
-
["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
|
|
16952
|
-
["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
|
|
16953
|
-
];
|
|
16954
|
-
for (const [label, count, colored] of nonCacheRows) {
|
|
16955
|
-
if (count === 0) continue;
|
|
16956
|
-
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
16957
|
-
console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
16958
|
-
}
|
|
16959
|
-
if (costCacheRead > 0) {
|
|
16960
|
-
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
16961
|
-
const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
|
|
16962
|
-
console.log(
|
|
16963
|
-
" " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
|
|
16964
|
-
);
|
|
16965
|
-
}
|
|
16966
|
-
}
|
|
16967
|
-
if (costUSD > 0) {
|
|
16968
|
-
const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
|
|
16969
|
-
const avgPerDay = costUSD / periodDays;
|
|
16970
|
-
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
16971
|
-
const costHeaderRight = [
|
|
16972
|
-
chalk13.yellow(fmtCost2(costUSD)),
|
|
16973
|
-
chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
|
|
16974
|
-
cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
|
|
16975
|
-
].filter(Boolean).join(chalk13.dim(" \xB7 "));
|
|
16976
|
-
console.log("");
|
|
16977
|
-
console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
|
|
16978
|
-
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
16979
|
-
if (codexCostUSD > 0)
|
|
16980
|
-
costByModel.set(
|
|
16981
|
-
"codex (openai)",
|
|
16982
|
-
(costByModel.get("codex (openai)") ?? 0) + codexCostUSD
|
|
16983
|
-
);
|
|
16984
|
-
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
16985
|
-
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
16986
|
-
const MODEL_LABEL = 22;
|
|
16987
|
-
const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
|
|
16988
|
-
for (const [model, cost] of modelList) {
|
|
16989
|
-
const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
|
|
16990
|
-
const b = colorBar(cost, maxModelCost, MODEL_BAR);
|
|
16991
|
-
console.log(
|
|
16992
|
-
" " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
|
|
16993
|
-
);
|
|
16994
|
-
}
|
|
17646
|
+
console.log(" " + chalk13.bold("MCP Servers"));
|
|
17647
|
+
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
17648
|
+
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
17649
|
+
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
17650
|
+
const label = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
17651
|
+
const b = colorBar(count, maxMcp, BAR);
|
|
17652
|
+
console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
if (hourMap.size > 0) {
|
|
17656
|
+
const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
17657
|
+
const maxHour = Math.max(...hourMap.values(), 1);
|
|
17658
|
+
const bar = Array.from({ length: 24 }, (_, h) => {
|
|
17659
|
+
const v = hourMap.get(h) ?? 0;
|
|
17660
|
+
return BLOCKS[Math.round(v / maxHour * 8)];
|
|
17661
|
+
}).join("");
|
|
17662
|
+
console.log("");
|
|
17663
|
+
console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
|
|
17664
|
+
console.log(" " + chalk13.cyan(bar));
|
|
17665
|
+
console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
|
|
17666
|
+
}
|
|
17667
|
+
if (dailyList.length > 1) {
|
|
17668
|
+
console.log("");
|
|
17669
|
+
console.log(" " + chalk13.bold("Daily Activity"));
|
|
17670
|
+
console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
|
|
17671
|
+
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
17672
|
+
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
17673
|
+
const label = fmtDate(dateKey).padEnd(10);
|
|
17674
|
+
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
17675
|
+
const dayCost = costByDay.get(dateKey);
|
|
17676
|
+
const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
|
|
17677
|
+
const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
|
|
17678
|
+
console.log(
|
|
17679
|
+
" " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
|
|
17680
|
+
);
|
|
16995
17681
|
}
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17682
|
+
}
|
|
17683
|
+
const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
|
|
17684
|
+
if (totalTokens > 0) {
|
|
17685
|
+
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
17686
|
+
console.log("");
|
|
17687
|
+
console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
|
|
17688
|
+
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
17689
|
+
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
17690
|
+
const TOK_LABEL = 14;
|
|
17691
|
+
const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
|
|
17692
|
+
const nonCacheRows = [
|
|
17693
|
+
["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
|
|
17694
|
+
["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
|
|
17695
|
+
["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
|
|
17696
|
+
];
|
|
17697
|
+
for (const [label, count, colored] of nonCacheRows) {
|
|
17698
|
+
if (count === 0) continue;
|
|
17699
|
+
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
17700
|
+
console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
17701
|
+
}
|
|
17702
|
+
if (costCacheRead > 0) {
|
|
17703
|
+
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
17704
|
+
const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
|
|
17003
17705
|
console.log(
|
|
17004
|
-
" " + chalk13.
|
|
17005
|
-
`${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
|
|
17006
|
-
)
|
|
17706
|
+
" " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
|
|
17007
17707
|
);
|
|
17008
|
-
|
|
17708
|
+
}
|
|
17709
|
+
}
|
|
17710
|
+
if (costUSD > 0) {
|
|
17711
|
+
const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
|
|
17712
|
+
const avgPerDay = costUSD / periodDays;
|
|
17713
|
+
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
17714
|
+
const costHeaderRight = [
|
|
17715
|
+
chalk13.yellow(fmtCost2(costUSD)),
|
|
17716
|
+
chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
|
|
17717
|
+
cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
|
|
17718
|
+
].filter(Boolean).join(chalk13.dim(" \xB7 "));
|
|
17719
|
+
console.log("");
|
|
17720
|
+
console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
|
|
17721
|
+
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
17722
|
+
if (codexUSD > 0)
|
|
17723
|
+
costByModel.set("codex (openai)", (costByModel.get("codex (openai)") ?? 0) + codexUSD);
|
|
17724
|
+
if (geminiUSD > 0)
|
|
17725
|
+
costByModel.set("gemini (google)", (costByModel.get("gemini (google)") ?? 0) + geminiUSD);
|
|
17726
|
+
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
17727
|
+
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
17728
|
+
const MODEL_LABEL = 22;
|
|
17729
|
+
const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
|
|
17730
|
+
for (const [model, cost] of modelList) {
|
|
17731
|
+
const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
|
|
17732
|
+
const b = colorBar(cost, maxModelCost, MODEL_BAR);
|
|
17009
17733
|
console.log(
|
|
17010
|
-
" " + chalk13.
|
|
17734
|
+
" " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
|
|
17011
17735
|
);
|
|
17012
|
-
console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
|
|
17013
|
-
for (const e of responseDlpEntries.slice(0, 5)) {
|
|
17014
|
-
const ts = chalk13.dim(fmtDate(e.ts) + " ");
|
|
17015
|
-
const pattern = chalk13.red(e.dlpPattern ?? "DLP");
|
|
17016
|
-
const sample = chalk13.gray(e.dlpSample ?? "");
|
|
17017
|
-
console.log(` ${ts}${pattern} ${sample}`);
|
|
17018
|
-
}
|
|
17019
|
-
if (responseDlpEntries.length > 5) {
|
|
17020
|
-
console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
|
|
17021
|
-
}
|
|
17022
17736
|
}
|
|
17737
|
+
}
|
|
17738
|
+
if (responseDlpEntries.length > 0) {
|
|
17023
17739
|
console.log("");
|
|
17024
17740
|
console.log(
|
|
17025
|
-
" " + chalk13.
|
|
17741
|
+
" " + chalk13.red.bold("\u26A0\uFE0F Response DLP") + chalk13.dim(" \xB7 ") + chalk13.red(
|
|
17742
|
+
`${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
|
|
17743
|
+
)
|
|
17026
17744
|
);
|
|
17027
|
-
console.log("");
|
|
17028
|
-
|
|
17745
|
+
console.log(" " + chalk13.dim("\u2500".repeat(Math.min(60, W - 4))));
|
|
17746
|
+
console.log(
|
|
17747
|
+
" " + chalk13.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
|
|
17748
|
+
);
|
|
17749
|
+
console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
|
|
17750
|
+
for (const e of responseDlpEntries.slice(0, 5)) {
|
|
17751
|
+
const ts = chalk13.dim(fmtDate(e.ts) + " ");
|
|
17752
|
+
const pattern = chalk13.red(e.dlpPattern ?? "DLP");
|
|
17753
|
+
const sample = chalk13.gray(e.dlpSample ?? "");
|
|
17754
|
+
console.log(` ${ts}${pattern} ${sample}`);
|
|
17755
|
+
}
|
|
17756
|
+
if (responseDlpEntries.length > 5) {
|
|
17757
|
+
console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
|
|
17758
|
+
}
|
|
17759
|
+
}
|
|
17760
|
+
console.log("");
|
|
17761
|
+
console.log(
|
|
17762
|
+
" " + chalk13.dim("node9 audit --deny") + chalk13.dim(" \xB7 ") + chalk13.dim("node9 report --period today|7d|30d|month --no-tests")
|
|
17763
|
+
);
|
|
17764
|
+
console.log("");
|
|
17029
17765
|
}
|
|
17030
17766
|
|
|
17031
17767
|
// src/cli/commands/daemon-cmd.ts
|