@node9/proxy 2.6.1 → 2.7.0

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/LICENSE CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  Apache License
2
3
  Version 2.0, January 2004
3
4
  http://www.apache.org/licenses/
@@ -32,33 +33,36 @@
32
33
  not limited to compiled object code, generated documentation,
33
34
  and conversions to other media types.
34
35
 
35
- "Work" shall mean the work of authorship made available under
36
- the License, as indicated by a copyright notice that is included in
37
- or attached to the work (an example is provided in the Appendix below).
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
38
40
 
39
41
  "Derivative Works" shall mean any work, whether in Source or Object
40
42
  form, that is based on (or derived from) the Work and for which the
41
- editorial revisions, annotations, elaborations, or other transformations
43
+ editorial revisions, annotations, elaborations, or other modifications
42
44
  represent, as a whole, an original work of authorship. For the purposes
43
45
  of this License, Derivative Works shall not include works that remain
44
46
  separable from, or merely link (or bind by name) to the interfaces of,
45
- the Work and the Derivative Works thereof.
46
-
47
- "Contribution" shall mean, as submitted to the Licensor for inclusion
48
- in the Work by the copyright owner or by an individual or Legal Entity
49
- authorized to submit on behalf of the copyright owner. For the purposes
50
- of this definition, "submitted" means any form of electronic, verbal,
51
- or written communication sent to the Licensor or its representations,
52
- including but not limited to communication on electronic mailing lists,
53
- source code control systems, and issue tracking systems that are managed
54
- by, or on behalf of, the Licensor for the purpose of discussing and
55
- improving the Work, but excluding communication that is conspicuously
56
- marked or designated in writing by the copyright owner as "Not a
57
- Contribution."
58
-
59
- "Contributor" shall mean Licensor and any Legal Entity on behalf of
60
- whom a Contribution has been received by the Licensor and included
61
- within the Work.
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
62
66
 
63
67
  2. Grant of Copyright License. Subject to the terms and conditions of
64
68
  this License, each Contributor hereby grants to You a perpetual,
@@ -74,21 +78,22 @@
74
78
  use, offer to sell, sell, import, and otherwise transfer the Work,
75
79
  where such license applies only to those patent claims licensable
76
80
  by such Contributor that are necessarily infringed by their
77
- Contribution(s) alone or by the combination of their Contribution(s)
81
+ Contribution(s) alone or by combination of their Contribution(s)
78
82
  with the Work to which such Contribution(s) was submitted. If You
79
- institute patent litigation against any entity (including a cross-claim
80
- or counterclaim in a lawsuit) alleging that the Work or any Contribution
81
- embodied within the Work constitutes direct or contributory patent
82
- infringement, then any patent licenses granted to You under this License
83
- for that Work shall terminate as of the date such litigation is filed.
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
84
89
 
85
90
  4. Redistribution. You may reproduce and distribute copies of the
86
91
  Work or Derivative Works thereof in any medium, with or without
87
92
  modifications, and in Source or Object form, provided that You
88
93
  meet the following conditions:
89
94
 
90
- (a) You must give any other recipients of the Work or Derivative
91
- Works a copy of this License; and
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
92
97
 
93
98
  (b) You must cause any modified files to carry prominent notices
94
99
  stating that You changed the files; and
@@ -100,25 +105,28 @@
100
105
  the Derivative Works; and
101
106
 
102
107
  (d) If the Work includes a "NOTICE" text file as part of its
103
- distribution, You must include a readable copy of the
104
- attribution notices contained within such NOTICE file, in
105
- at least one of the following places: within a NOTICE text
106
- file distributed as part of the Derivative Works; within
107
- the Source form or documentation, if provided along with the
108
- Derivative Works; or, within a display generated by the
109
- Derivative Works, if and wherever such third-party notices
110
- normally appear. The contents of the NOTICE file are for
111
- informational purposes only and do not modify the License.
112
- You may add Your own attribution notices within Derivative
113
- Works that You distribute, alongside or in addition to the
114
- NOTICE text from the Work, provided that such additional
115
- attribution notices cannot be construed as modifying the License.
116
-
117
- You may add Your own license statement for Your modifications and
118
- may provide additional grant of rights to use, reproduce, modify,
119
- prepare Derivative Works of, convert to other formats, and distribute
120
- the contributions, provided such additional grant of rights does not
121
- conflict with the terms and conditions of this License.
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
122
130
 
123
131
  5. Submission of Contributions. Unless You explicitly state otherwise,
124
132
  any Contribution intentionally submitted for inclusion in the Work
@@ -140,7 +148,7 @@
140
148
  implied, including, without limitation, any warranties or conditions
141
149
  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
142
150
  PARTICULAR PURPOSE. You are solely responsible for determining the
143
- appropriateness of using or reproducing the Work and assume any
151
+ appropriateness of using or redistributing the Work and assume any
144
152
  risks associated with Your exercise of permissions under this License.
145
153
 
146
154
  8. Limitation of Liability. In no event and under no legal theory,
@@ -148,26 +156,37 @@
148
156
  unless required by applicable law (such as deliberate and grossly
149
157
  negligent acts) or agreed to in writing, shall any Contributor be
150
158
  liable to You for damages, including any direct, indirect, special,
151
- incidental, or exemplary damages of any character arising as a
159
+ incidental, or consequential damages of any character arising as a
152
160
  result of this License or out of the use or inability to use the
153
161
  Work (including but not limited to damages for loss of goodwill,
154
- work stoppage, computer failure or malfunction, or all other
155
- commercial damages or losses), even if such Contributor has been
156
- advised of the possibility of such damages.
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
157
165
 
158
166
  9. Accepting Warranty or Additional Liability. While redistributing
159
167
  the Work or Derivative Works thereof, You may choose to offer,
160
168
  and charge a fee for, acceptance of support, warranty, indemnity,
161
169
  or other liability obligations and/or rights consistent with this
162
- License. However, in accepting such obligations, You may offer such
163
- obligations only on Your own behalf and on Your sole responsibility,
164
- not on behalf of any other Contributor, and only if You agree to
165
- indemnify, defend, and hold each Contributor harmless for any
166
- liability incurred by, or claims asserted against, such Contributor
167
- by reason of your accepting any such warranty or additional liability.
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
168
176
 
169
177
  END OF TERMS AND CONDITIONS
170
178
 
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
171
190
  Copyright 2026 Node9 AI
172
191
 
173
192
  Licensed under the Apache License, Version 2.0 (the "License");
package/dist/cli.js CHANGED
@@ -2561,8 +2561,7 @@ function extractSessionLevelFindings(calls, ctx) {
2561
2561
  occurrenceCount: 1,
2562
2562
  loopCount: verdict.count,
2563
2563
  loopKind: "loop",
2564
- commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
2565
- costUsd: verdict.count * COST_PER_LOOP_ITER_USD
2564
+ commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN)
2566
2565
  });
2567
2566
  }
2568
2567
  return out;
@@ -2632,7 +2631,7 @@ function* stringValues(obj, depth = 0) {
2632
2631
  }
2633
2632
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2634
2633
  }
2635
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2634
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2636
2635
  var init_dist = __esm({
2637
2636
  "packages/policy-engine/dist/index.mjs"() {
2638
2637
  "use strict";
@@ -4439,7 +4438,6 @@ var init_dist = __esm({
4439
4438
  longOutputRedactions: 1
4440
4439
  };
4441
4440
  LOOP_THRESHOLD_FOR_WASTE = 3;
4442
- COST_PER_LOOP_ITER_USD = 6e-3;
4443
4441
  DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
4444
4442
  SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
4445
4443
  FILE_TOOLS = /* @__PURE__ */ new Set([
@@ -5650,7 +5648,14 @@ var init_config = __esm({
5650
5648
  settings: {
5651
5649
  mode: "standard",
5652
5650
  autoStartDaemon: true,
5653
- enableHookLogDebug: true,
5651
+ // Off by default: this is a DEBUG facility, not part of the product's job.
5652
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
5653
+ // has no production reader and no size ceiling. Turn it on deliberately
5654
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
5655
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
5656
+ // keep working when it is off — see CLAUDE.md "Always write to
5657
+ // hook-debug.log in catch blocks that guard audit trail".
5658
+ enableHookLogDebug: false,
5654
5659
  approvalTimeoutMs: 12e4,
5655
5660
  // 120-second auto-deny timeout
5656
5661
  flightRecorder: true,
@@ -8136,6 +8141,7 @@ ${appPermReview}`
8136
8141
  // not an approval of an exfiltration risk. Measured against the live BE:
8137
8142
  // {approved:true} without this flag, {pending:true} with it.
8138
8143
  !!taintWarning || void 0;
8144
+ const cloudMayVote = !hardBlockDowngraded;
8139
8145
  if (cloudEnforced) {
8140
8146
  try {
8141
8147
  const initResult = await initNode9SaaS(
@@ -8151,7 +8157,7 @@ ${appPermReview}`
8151
8157
  if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
8152
8158
  return { approved: true, checkedBy: "cloud" };
8153
8159
  }
8154
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
8160
+ if (!forceReview) {
8155
8161
  return {
8156
8162
  approved: !!initResult.approved,
8157
8163
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -8232,7 +8238,7 @@ ${appPermReview}`
8232
8238
  }
8233
8239
  }
8234
8240
  }
8235
- if (cloudEnforced && cloudRequestId) {
8241
+ if (cloudEnforced && cloudRequestId && cloudMayVote) {
8236
8242
  racePromises.push(
8237
8243
  (async () => {
8238
8244
  try {
@@ -11954,6 +11960,31 @@ function agentColorName(agent) {
11954
11960
  return "cyan";
11955
11961
  }
11956
11962
  }
11963
+ function computeLoopWaste(loops, perSession) {
11964
+ const rate = /* @__PURE__ */ new Map();
11965
+ for (const s of perSession) {
11966
+ if (!(s.toolCalls > 0) || !(s.costUSD > 0)) continue;
11967
+ const r = s.costUSD / s.toolCalls;
11968
+ if (!Number.isFinite(r) || r <= 0 || r > MAX_PLAUSIBLE_RATE_USD) continue;
11969
+ rate.set(s.sessionId, r);
11970
+ }
11971
+ let usd = 0;
11972
+ let pricedIterations = 0;
11973
+ let unpricedIterations = 0;
11974
+ for (const l of loops) {
11975
+ if (l.kind === "long-iteration") continue;
11976
+ const wasted = Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE);
11977
+ if (!Number.isFinite(wasted) || wasted === 0) continue;
11978
+ const r = rate.get(l.sessionId);
11979
+ if (r === void 0) {
11980
+ unpricedIterations += wasted;
11981
+ continue;
11982
+ }
11983
+ usd += wasted * r;
11984
+ pricedIterations += wasted;
11985
+ }
11986
+ return { usd, pricedIterations, unpricedIterations };
11987
+ }
11957
11988
  function buildScanSummary(agents) {
11958
11989
  const stats = {
11959
11990
  sessions: 0,
@@ -12014,8 +12045,11 @@ function buildScanSummary(agents) {
12014
12045
  costUSD: a.scan.totalCostUSD
12015
12046
  })).filter((s) => s.sessions > 0 || s.findings > 0);
12016
12047
  const sections = buildSections(allFindings);
12017
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
12018
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
12048
+ const loopWaste = computeLoopWaste(
12049
+ allLoops,
12050
+ agents.flatMap((a) => a.scan.perSession)
12051
+ );
12052
+ const loopWastedUSD = loopWaste.usd;
12019
12053
  return {
12020
12054
  stats,
12021
12055
  byVerdict,
@@ -12023,7 +12057,8 @@ function buildScanSummary(agents) {
12023
12057
  sections,
12024
12058
  leaks: allLeaks,
12025
12059
  loops: allLoops,
12026
- loopWastedUSD
12060
+ loopWastedUSD,
12061
+ loopWaste
12027
12062
  };
12028
12063
  }
12029
12064
  function buildSections(findings) {
@@ -12130,7 +12165,7 @@ function fullCommandOf(input) {
12130
12165
  const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
12131
12166
  return String(raw).replace(/\s+/g, " ").trim();
12132
12167
  }
12133
- var AGENT_SHORT, AGENT_LONG;
12168
+ var AGENT_SHORT, AGENT_LONG, MAX_PLAUSIBLE_RATE_USD;
12134
12169
  var init_scan_summary = __esm({
12135
12170
  "src/scan-summary.ts"() {
12136
12171
  "use strict";
@@ -12153,6 +12188,7 @@ var init_scan_summary = __esm({
12153
12188
  copilot: "GitHub Copilot",
12154
12189
  shell: "Shell"
12155
12190
  };
12191
+ MAX_PLAUSIBLE_RATE_USD = 50;
12156
12192
  }
12157
12193
  });
12158
12194
 
@@ -12360,7 +12396,7 @@ function topRulesByVerdict(sections, verdict, n) {
12360
12396
  }
12361
12397
  return matched.sort((a, b) => b.count - a.count).slice(0, n);
12362
12398
  }
12363
- function computeLoopWaste(loops, totalToolCalls) {
12399
+ function computeLoopWaste2(loops, totalToolCalls) {
12364
12400
  const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
12365
12401
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
12366
12402
  return { wastedCalls, wastePct };
@@ -13946,6 +13982,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
13946
13982
  result.sessions++;
13947
13983
  onProgress?.(result.filesScanned);
13948
13984
  const sessionId = file.replace(/\.jsonl$/, "");
13985
+ const session = { sessionId, costUSD: 0, toolCalls: 0 };
13949
13986
  let raw;
13950
13987
  try {
13951
13988
  raw = import_fs26.default.readFileSync(import_path28.default.join(projPath, file), "utf-8");
@@ -14033,7 +14070,9 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14033
14070
  if (usage && model) {
14034
14071
  const p = claudeModelPrice(model);
14035
14072
  if (p) {
14036
- 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;
14073
+ const rowCost = (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;
14074
+ result.totalCostUSD += rowCost;
14075
+ session.costUSD += rowCost;
14037
14076
  }
14038
14077
  }
14039
14078
  const content = entry.message?.content;
@@ -14041,6 +14080,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14041
14080
  for (const block of content) {
14042
14081
  if (block.type !== "tool_use") continue;
14043
14082
  result.totalToolCalls++;
14083
+ session.toolCalls++;
14044
14084
  const toolName = block.name ?? "";
14045
14085
  const toolNameLower = toolName.toLowerCase();
14046
14086
  const input = block.input ?? {};
@@ -14152,6 +14192,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14152
14192
  if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
14153
14193
  result.sessionsWithEarlySecrets++;
14154
14194
  }
14195
+ result.perSession.push(session);
14155
14196
  }
14156
14197
  function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
14157
14198
  const projPath = import_path28.default.join(projectsDir, proj);
@@ -14196,7 +14237,8 @@ function emptyClaudeScan() {
14196
14237
  totalCostUSD: 0,
14197
14238
  firstDate: null,
14198
14239
  lastDate: null,
14199
- sessionsWithEarlySecrets: 0
14240
+ sessionsWithEarlySecrets: 0,
14241
+ perSession: []
14200
14242
  };
14201
14243
  }
14202
14244
  function scanClaudeHistory(startDate, onProgress, onLine) {
@@ -14238,7 +14280,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
14238
14280
  totalCostUSD: 0,
14239
14281
  firstDate: null,
14240
14282
  lastDate: null,
14241
- sessionsWithEarlySecrets: 0
14283
+ sessionsWithEarlySecrets: 0,
14284
+ perSession: []
14242
14285
  };
14243
14286
  const dedup = emptyScanDedup();
14244
14287
  if (!import_fs26.default.existsSync(tmpDir)) return result;
@@ -14476,7 +14519,8 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
14476
14519
  // transcripts carry no token/model data
14477
14520
  firstDate: null,
14478
14521
  lastDate: null,
14479
- sessionsWithEarlySecrets: 0
14522
+ sessionsWithEarlySecrets: 0,
14523
+ perSession: []
14480
14524
  };
14481
14525
  const dedup = emptyScanDedup();
14482
14526
  const brainDirs = antigravityBrainDirs();
@@ -14672,7 +14716,8 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
14672
14716
  // event logs carry no token/cost rollup
14673
14717
  firstDate: null,
14674
14718
  lastDate: null,
14675
- sessionsWithEarlySecrets: 0
14719
+ sessionsWithEarlySecrets: 0,
14720
+ perSession: []
14676
14721
  };
14677
14722
  const dedup = emptyScanDedup();
14678
14723
  if (!import_fs26.default.existsSync(sessionDir)) return result;
@@ -14854,7 +14899,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
14854
14899
  totalCostUSD: 0,
14855
14900
  firstDate: null,
14856
14901
  lastDate: null,
14857
- sessionsWithEarlySecrets: 0
14902
+ sessionsWithEarlySecrets: 0,
14903
+ perSession: []
14858
14904
  };
14859
14905
  const dedup = emptyScanDedup();
14860
14906
  if (!import_fs26.default.existsSync(sessionsBase)) return result;
@@ -15133,7 +15179,8 @@ function mergeScans(a, b) {
15133
15179
  totalCostUSD: a.totalCostUSD + b.totalCostUSD,
15134
15180
  firstDate: dates.length ? dates.sort()[0] : null,
15135
15181
  lastDate: lastDates.length ? lastDates.sort().at(-1) : null,
15136
- sessionsWithEarlySecrets: a.sessionsWithEarlySecrets + b.sessionsWithEarlySecrets
15182
+ sessionsWithEarlySecrets: a.sessionsWithEarlySecrets + b.sessionsWithEarlySecrets,
15183
+ perSession: [...a.perSession, ...b.perSession]
15137
15184
  };
15138
15185
  }
15139
15186
  function verdictIcon(verdict) {
@@ -15217,9 +15264,9 @@ function renderCompactScorecard(input) {
15217
15264
  const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
15218
15265
  const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
15219
15266
  if (realLoops.length > 0) {
15220
- const { wastePct, wastedCalls } = computeLoopWaste(realLoops, scan.totalToolCalls);
15267
+ const { wastePct, wastedCalls } = computeLoopWaste2(realLoops, scan.totalToolCalls);
15221
15268
  const wasteParts = [];
15222
- if (wastedCalls > 0) wasteParts.push(`${wastedCalls} wasted calls (${wastePct}% of all calls)`);
15269
+ if (wastedCalls > 0) wasteParts.push(`${wastedCalls} repeat calls (${wastePct}% of all calls)`);
15223
15270
  if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
15224
15271
  const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
15225
15272
  console.log(
@@ -15297,10 +15344,10 @@ function renderNarrativeScorecard(input) {
15297
15344
  });
15298
15345
  }
15299
15346
  if (scan.loopFindings.length > 0) {
15300
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15347
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15301
15348
  const cost = summary.loopWastedUSD > 0 ? `, ~${fmtCost(summary.loopWastedUSD)} wasted` : "";
15302
15349
  medium.push({
15303
- label: `${scan.loopFindings.length} agent loops (${wastedCalls} wasted calls, ${wastePct}% of calls${cost})`,
15350
+ label: `${scan.loopFindings.length} agent loops (${wastedCalls} repeat calls, ${wastePct}% of calls${cost})`,
15304
15351
  count: scan.loopFindings.length
15305
15352
  });
15306
15353
  }
@@ -15412,13 +15459,13 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15412
15459
  );
15413
15460
  }
15414
15461
  if (scan.loopFindings.length > 0) {
15415
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15462
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15416
15463
  const byTool = /* @__PURE__ */ new Map();
15417
15464
  for (const f of scan.loopFindings) {
15418
15465
  byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
15419
15466
  }
15420
15467
  const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
15421
- const wasteSuffix = wastedCalls > 0 ? `, ${wastedCalls} wasted calls \xB7 ${wastePct}% of calls` : "";
15468
+ const wasteSuffix = wastedCalls > 0 ? `, ${wastedCalls} repeat calls \xB7 ${wastePct}% of calls` : "";
15422
15469
  const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
15423
15470
  topLines.push(
15424
15471
  mkLine(
@@ -15510,7 +15557,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15510
15557
  console.log("");
15511
15558
  }
15512
15559
  if (scan.loopFindings.length > 0) {
15513
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15560
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15514
15561
  const byTool = /* @__PURE__ */ new Map();
15515
15562
  let totalRepeats = 0;
15516
15563
  for (const f of scan.loopFindings) {
@@ -15540,7 +15587,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15540
15587
  loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, import_chalk6.default.bold], [target, import_chalk6.default.dim]));
15541
15588
  }
15542
15589
  }
15543
- const wasteSuffix = wastedCalls > 0 ? ` \xB7 ${wastedCalls} wasted calls (${wastePct}% of all calls)` : "";
15590
+ const wasteSuffix = wastedCalls > 0 ? ` \xB7 ${wastedCalls} repeat calls (${wastePct}% of all calls)` : "";
15544
15591
  const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} pattern${scan.loopFindings.length !== 1 ? "s" : ""}${wasteSuffix}`;
15545
15592
  for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
15546
15593
  console.log("");
@@ -15875,7 +15922,7 @@ function registerScanCommand(program2) {
15875
15922
  );
15876
15923
  }
15877
15924
  if (scan.loopFindings.length > 0) {
15878
- const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15925
+ const { wastePct } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15879
15926
  const wasteSuffix = wastePct > 0 ? import_chalk6.default.dim(` (${wastePct}% of calls)`) : "";
15880
15927
  cardParts.push(
15881
15928
  import_chalk6.default.yellow("\u{1F501} ") + import_chalk6.default.yellow.bold(String(scan.loopFindings.length)) + import_chalk6.default.dim(" loops") + wasteSuffix
@@ -21048,7 +21095,8 @@ data: ${JSON.stringify(item.data)}
21048
21095
  totalCostUSD: 0,
21049
21096
  firstDate: null,
21050
21097
  lastDate: null,
21051
- sessionsWithEarlySecrets: 0
21098
+ sessionsWithEarlySecrets: 0,
21099
+ perSession: []
21052
21100
  };
21053
21101
  let claude = EMPTY_SCAN;
21054
21102
  let gemini = EMPTY_SCAN;
package/dist/cli.mjs CHANGED
@@ -2571,8 +2571,7 @@ function extractSessionLevelFindings(calls, ctx) {
2571
2571
  occurrenceCount: 1,
2572
2572
  loopCount: verdict.count,
2573
2573
  loopKind: "loop",
2574
- commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN),
2575
- costUsd: verdict.count * COST_PER_LOOP_ITER_USD
2574
+ commandPreview: previewArgs(call.args, DEDUPE_PREVIEW_LEN)
2576
2575
  });
2577
2576
  }
2578
2577
  return out;
@@ -2642,7 +2641,7 @@ function* stringValues(obj, depth = 0) {
2642
2641
  }
2643
2642
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2644
2643
  }
2645
- var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2644
+ var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2646
2645
  var init_dist = __esm({
2647
2646
  "packages/policy-engine/dist/index.mjs"() {
2648
2647
  "use strict";
@@ -4443,7 +4442,6 @@ var init_dist = __esm({
4443
4442
  longOutputRedactions: 1
4444
4443
  };
4445
4444
  LOOP_THRESHOLD_FOR_WASTE = 3;
4446
- COST_PER_LOOP_ITER_USD = 6e-3;
4447
4445
  DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
4448
4446
  SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
4449
4447
  FILE_TOOLS = /* @__PURE__ */ new Set([
@@ -5654,7 +5652,14 @@ var init_config = __esm({
5654
5652
  settings: {
5655
5653
  mode: "standard",
5656
5654
  autoStartDaemon: true,
5657
- enableHookLogDebug: true,
5655
+ // Off by default: this is a DEBUG facility, not part of the product's job.
5656
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
5657
+ // has no production reader and no size ceiling. Turn it on deliberately
5658
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
5659
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
5660
+ // keep working when it is off — see CLAUDE.md "Always write to
5661
+ // hook-debug.log in catch blocks that guard audit trail".
5662
+ enableHookLogDebug: false,
5658
5663
  approvalTimeoutMs: 12e4,
5659
5664
  // 120-second auto-deny timeout
5660
5665
  flightRecorder: true,
@@ -8139,6 +8144,7 @@ ${appPermReview}`
8139
8144
  // not an approval of an exfiltration risk. Measured against the live BE:
8140
8145
  // {approved:true} without this flag, {pending:true} with it.
8141
8146
  !!taintWarning || void 0;
8147
+ const cloudMayVote = !hardBlockDowngraded;
8142
8148
  if (cloudEnforced) {
8143
8149
  try {
8144
8150
  const initResult = await initNode9SaaS(
@@ -8154,7 +8160,7 @@ ${appPermReview}`
8154
8160
  if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
8155
8161
  return { approved: true, checkedBy: "cloud" };
8156
8162
  }
8157
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
8163
+ if (!forceReview) {
8158
8164
  return {
8159
8165
  approved: !!initResult.approved,
8160
8166
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -8235,7 +8241,7 @@ ${appPermReview}`
8235
8241
  }
8236
8242
  }
8237
8243
  }
8238
- if (cloudEnforced && cloudRequestId) {
8244
+ if (cloudEnforced && cloudRequestId && cloudMayVote) {
8239
8245
  racePromises.push(
8240
8246
  (async () => {
8241
8247
  try {
@@ -11955,6 +11961,31 @@ function agentColorName(agent) {
11955
11961
  return "cyan";
11956
11962
  }
11957
11963
  }
11964
+ function computeLoopWaste(loops, perSession) {
11965
+ const rate = /* @__PURE__ */ new Map();
11966
+ for (const s of perSession) {
11967
+ if (!(s.toolCalls > 0) || !(s.costUSD > 0)) continue;
11968
+ const r = s.costUSD / s.toolCalls;
11969
+ if (!Number.isFinite(r) || r <= 0 || r > MAX_PLAUSIBLE_RATE_USD) continue;
11970
+ rate.set(s.sessionId, r);
11971
+ }
11972
+ let usd = 0;
11973
+ let pricedIterations = 0;
11974
+ let unpricedIterations = 0;
11975
+ for (const l of loops) {
11976
+ if (l.kind === "long-iteration") continue;
11977
+ const wasted = Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE);
11978
+ if (!Number.isFinite(wasted) || wasted === 0) continue;
11979
+ const r = rate.get(l.sessionId);
11980
+ if (r === void 0) {
11981
+ unpricedIterations += wasted;
11982
+ continue;
11983
+ }
11984
+ usd += wasted * r;
11985
+ pricedIterations += wasted;
11986
+ }
11987
+ return { usd, pricedIterations, unpricedIterations };
11988
+ }
11958
11989
  function buildScanSummary(agents) {
11959
11990
  const stats = {
11960
11991
  sessions: 0,
@@ -12015,8 +12046,11 @@ function buildScanSummary(agents) {
12015
12046
  costUSD: a.scan.totalCostUSD
12016
12047
  })).filter((s) => s.sessions > 0 || s.findings > 0);
12017
12048
  const sections = buildSections(allFindings);
12018
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
12019
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
12049
+ const loopWaste = computeLoopWaste(
12050
+ allLoops,
12051
+ agents.flatMap((a) => a.scan.perSession)
12052
+ );
12053
+ const loopWastedUSD = loopWaste.usd;
12020
12054
  return {
12021
12055
  stats,
12022
12056
  byVerdict,
@@ -12024,7 +12058,8 @@ function buildScanSummary(agents) {
12024
12058
  sections,
12025
12059
  leaks: allLeaks,
12026
12060
  loops: allLoops,
12027
- loopWastedUSD
12061
+ loopWastedUSD,
12062
+ loopWaste
12028
12063
  };
12029
12064
  }
12030
12065
  function buildSections(findings) {
@@ -12131,7 +12166,7 @@ function fullCommandOf(input) {
12131
12166
  const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
12132
12167
  return String(raw).replace(/\s+/g, " ").trim();
12133
12168
  }
12134
- var AGENT_SHORT, AGENT_LONG;
12169
+ var AGENT_SHORT, AGENT_LONG, MAX_PLAUSIBLE_RATE_USD;
12135
12170
  var init_scan_summary = __esm({
12136
12171
  "src/scan-summary.ts"() {
12137
12172
  "use strict";
@@ -12154,6 +12189,7 @@ var init_scan_summary = __esm({
12154
12189
  copilot: "GitHub Copilot",
12155
12190
  shell: "Shell"
12156
12191
  };
12192
+ MAX_PLAUSIBLE_RATE_USD = 50;
12157
12193
  }
12158
12194
  });
12159
12195
 
@@ -12362,7 +12398,7 @@ function topRulesByVerdict(sections, verdict, n) {
12362
12398
  }
12363
12399
  return matched.sort((a, b) => b.count - a.count).slice(0, n);
12364
12400
  }
12365
- function computeLoopWaste(loops, totalToolCalls) {
12401
+ function computeLoopWaste2(loops, totalToolCalls) {
12366
12402
  const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
12367
12403
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
12368
12404
  return { wastedCalls, wastePct };
@@ -13951,6 +13987,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
13951
13987
  result.sessions++;
13952
13988
  onProgress?.(result.filesScanned);
13953
13989
  const sessionId = file.replace(/\.jsonl$/, "");
13990
+ const session = { sessionId, costUSD: 0, toolCalls: 0 };
13954
13991
  let raw;
13955
13992
  try {
13956
13993
  raw = fs27.readFileSync(path29.join(projPath, file), "utf-8");
@@ -14038,7 +14075,9 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14038
14075
  if (usage && model) {
14039
14076
  const p = claudeModelPrice(model);
14040
14077
  if (p) {
14041
- 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;
14078
+ const rowCost = (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;
14079
+ result.totalCostUSD += rowCost;
14080
+ session.costUSD += rowCost;
14042
14081
  }
14043
14082
  }
14044
14083
  const content = entry.message?.content;
@@ -14046,6 +14085,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14046
14085
  for (const block of content) {
14047
14086
  if (block.type !== "tool_use") continue;
14048
14087
  result.totalToolCalls++;
14088
+ session.toolCalls++;
14049
14089
  const toolName = block.name ?? "";
14050
14090
  const toolNameLower = toolName.toLowerCase();
14051
14091
  const input = block.input ?? {};
@@ -14157,6 +14197,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
14157
14197
  if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
14158
14198
  result.sessionsWithEarlySecrets++;
14159
14199
  }
14200
+ result.perSession.push(session);
14160
14201
  }
14161
14202
  function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
14162
14203
  const projPath = path29.join(projectsDir, proj);
@@ -14201,7 +14242,8 @@ function emptyClaudeScan() {
14201
14242
  totalCostUSD: 0,
14202
14243
  firstDate: null,
14203
14244
  lastDate: null,
14204
- sessionsWithEarlySecrets: 0
14245
+ sessionsWithEarlySecrets: 0,
14246
+ perSession: []
14205
14247
  };
14206
14248
  }
14207
14249
  function scanClaudeHistory(startDate, onProgress, onLine) {
@@ -14243,7 +14285,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
14243
14285
  totalCostUSD: 0,
14244
14286
  firstDate: null,
14245
14287
  lastDate: null,
14246
- sessionsWithEarlySecrets: 0
14288
+ sessionsWithEarlySecrets: 0,
14289
+ perSession: []
14247
14290
  };
14248
14291
  const dedup = emptyScanDedup();
14249
14292
  if (!fs27.existsSync(tmpDir)) return result;
@@ -14481,7 +14524,8 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
14481
14524
  // transcripts carry no token/model data
14482
14525
  firstDate: null,
14483
14526
  lastDate: null,
14484
- sessionsWithEarlySecrets: 0
14527
+ sessionsWithEarlySecrets: 0,
14528
+ perSession: []
14485
14529
  };
14486
14530
  const dedup = emptyScanDedup();
14487
14531
  const brainDirs = antigravityBrainDirs();
@@ -14677,7 +14721,8 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
14677
14721
  // event logs carry no token/cost rollup
14678
14722
  firstDate: null,
14679
14723
  lastDate: null,
14680
- sessionsWithEarlySecrets: 0
14724
+ sessionsWithEarlySecrets: 0,
14725
+ perSession: []
14681
14726
  };
14682
14727
  const dedup = emptyScanDedup();
14683
14728
  if (!fs27.existsSync(sessionDir)) return result;
@@ -14859,7 +14904,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
14859
14904
  totalCostUSD: 0,
14860
14905
  firstDate: null,
14861
14906
  lastDate: null,
14862
- sessionsWithEarlySecrets: 0
14907
+ sessionsWithEarlySecrets: 0,
14908
+ perSession: []
14863
14909
  };
14864
14910
  const dedup = emptyScanDedup();
14865
14911
  if (!fs27.existsSync(sessionsBase)) return result;
@@ -15138,7 +15184,8 @@ function mergeScans(a, b) {
15138
15184
  totalCostUSD: a.totalCostUSD + b.totalCostUSD,
15139
15185
  firstDate: dates.length ? dates.sort()[0] : null,
15140
15186
  lastDate: lastDates.length ? lastDates.sort().at(-1) : null,
15141
- sessionsWithEarlySecrets: a.sessionsWithEarlySecrets + b.sessionsWithEarlySecrets
15187
+ sessionsWithEarlySecrets: a.sessionsWithEarlySecrets + b.sessionsWithEarlySecrets,
15188
+ perSession: [...a.perSession, ...b.perSession]
15142
15189
  };
15143
15190
  }
15144
15191
  function verdictIcon(verdict) {
@@ -15222,9 +15269,9 @@ function renderCompactScorecard(input) {
15222
15269
  const realLoops = scan.loopFindings.filter((l) => l.kind !== "long-iteration");
15223
15270
  const longIterations = scan.loopFindings.filter((l) => l.kind === "long-iteration");
15224
15271
  if (realLoops.length > 0) {
15225
- const { wastePct, wastedCalls } = computeLoopWaste(realLoops, scan.totalToolCalls);
15272
+ const { wastePct, wastedCalls } = computeLoopWaste2(realLoops, scan.totalToolCalls);
15226
15273
  const wasteParts = [];
15227
- if (wastedCalls > 0) wasteParts.push(`${wastedCalls} wasted calls (${wastePct}% of all calls)`);
15274
+ if (wastedCalls > 0) wasteParts.push(`${wastedCalls} repeat calls (${wastePct}% of all calls)`);
15228
15275
  if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
15229
15276
  const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
15230
15277
  console.log(
@@ -15302,10 +15349,10 @@ function renderNarrativeScorecard(input) {
15302
15349
  });
15303
15350
  }
15304
15351
  if (scan.loopFindings.length > 0) {
15305
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15352
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15306
15353
  const cost = summary.loopWastedUSD > 0 ? `, ~${fmtCost(summary.loopWastedUSD)} wasted` : "";
15307
15354
  medium.push({
15308
- label: `${scan.loopFindings.length} agent loops (${wastedCalls} wasted calls, ${wastePct}% of calls${cost})`,
15355
+ label: `${scan.loopFindings.length} agent loops (${wastedCalls} repeat calls, ${wastePct}% of calls${cost})`,
15309
15356
  count: scan.loopFindings.length
15310
15357
  });
15311
15358
  }
@@ -15417,13 +15464,13 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15417
15464
  );
15418
15465
  }
15419
15466
  if (scan.loopFindings.length > 0) {
15420
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15467
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15421
15468
  const byTool = /* @__PURE__ */ new Map();
15422
15469
  for (const f of scan.loopFindings) {
15423
15470
  byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
15424
15471
  }
15425
15472
  const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
15426
- const wasteSuffix = wastedCalls > 0 ? `, ${wastedCalls} wasted calls \xB7 ${wastePct}% of calls` : "";
15473
+ const wasteSuffix = wastedCalls > 0 ? `, ${wastedCalls} repeat calls \xB7 ${wastePct}% of calls` : "";
15427
15474
  const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
15428
15475
  topLines.push(
15429
15476
  mkLine(
@@ -15515,7 +15562,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15515
15562
  console.log("");
15516
15563
  }
15517
15564
  if (scan.loopFindings.length > 0) {
15518
- const { wastePct, wastedCalls } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15565
+ const { wastePct, wastedCalls } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15519
15566
  const byTool = /* @__PURE__ */ new Map();
15520
15567
  let totalRepeats = 0;
15521
15568
  for (const f of scan.loopFindings) {
@@ -15545,7 +15592,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
15545
15592
  loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, chalk6.bold], [target, chalk6.dim]));
15546
15593
  }
15547
15594
  }
15548
- const wasteSuffix = wastedCalls > 0 ? ` \xB7 ${wastedCalls} wasted calls (${wastePct}% of all calls)` : "";
15595
+ const wasteSuffix = wastedCalls > 0 ? ` \xB7 ${wastedCalls} repeat calls (${wastePct}% of all calls)` : "";
15549
15596
  const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} pattern${scan.loopFindings.length !== 1 ? "s" : ""}${wasteSuffix}`;
15550
15597
  for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
15551
15598
  console.log("");
@@ -15880,7 +15927,7 @@ function registerScanCommand(program2) {
15880
15927
  );
15881
15928
  }
15882
15929
  if (scan.loopFindings.length > 0) {
15883
- const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
15930
+ const { wastePct } = computeLoopWaste2(scan.loopFindings, scan.totalToolCalls);
15884
15931
  const wasteSuffix = wastePct > 0 ? chalk6.dim(` (${wastePct}% of calls)`) : "";
15885
15932
  cardParts.push(
15886
15933
  chalk6.yellow("\u{1F501} ") + chalk6.yellow.bold(String(scan.loopFindings.length)) + chalk6.dim(" loops") + wasteSuffix
@@ -21049,7 +21096,8 @@ data: ${JSON.stringify(item.data)}
21049
21096
  totalCostUSD: 0,
21050
21097
  firstDate: null,
21051
21098
  lastDate: null,
21052
- sessionsWithEarlySecrets: 0
21099
+ sessionsWithEarlySecrets: 0,
21100
+ perSession: []
21053
21101
  };
21054
21102
  let claude = EMPTY_SCAN;
21055
21103
  let gemini = EMPTY_SCAN;
@@ -3435,7 +3435,14 @@ var init_config = __esm({
3435
3435
  settings: {
3436
3436
  mode: "standard",
3437
3437
  autoStartDaemon: true,
3438
- enableHookLogDebug: true,
3438
+ // Off by default: this is a DEBUG facility, not part of the product's job.
3439
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
3440
+ // has no production reader and no size ceiling. Turn it on deliberately
3441
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
3442
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
3443
+ // keep working when it is off — see CLAUDE.md "Always write to
3444
+ // hook-debug.log in catch blocks that guard audit trail".
3445
+ enableHookLogDebug: false,
3439
3446
  approvalTimeoutMs: 12e4,
3440
3447
  // 120-second auto-deny timeout
3441
3448
  flightRecorder: true,
@@ -5040,6 +5047,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5040
5047
  result.sessions++;
5041
5048
  onProgress?.(result.filesScanned);
5042
5049
  const sessionId = file.replace(/\.jsonl$/, "");
5050
+ const session = { sessionId, costUSD: 0, toolCalls: 0 };
5043
5051
  let raw;
5044
5052
  try {
5045
5053
  raw = fs7.readFileSync(path10.join(projPath, file), "utf-8");
@@ -5127,7 +5135,9 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5127
5135
  if (usage && model) {
5128
5136
  const p = claudeModelPrice2(model);
5129
5137
  if (p) {
5130
- 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;
5138
+ const rowCost = (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;
5139
+ result.totalCostUSD += rowCost;
5140
+ session.costUSD += rowCost;
5131
5141
  }
5132
5142
  }
5133
5143
  const content = entry.message?.content;
@@ -5135,6 +5145,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5135
5145
  for (const block of content) {
5136
5146
  if (block.type !== "tool_use") continue;
5137
5147
  result.totalToolCalls++;
5148
+ session.toolCalls++;
5138
5149
  const toolName = block.name ?? "";
5139
5150
  const toolNameLower = toolName.toLowerCase();
5140
5151
  const input = block.input ?? {};
@@ -5246,6 +5257,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
5246
5257
  if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
5247
5258
  result.sessionsWithEarlySecrets++;
5248
5259
  }
5260
+ result.perSession.push(session);
5249
5261
  }
5250
5262
  async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
5251
5263
  const projPath = path10.join(projectsDir, proj);
@@ -5294,7 +5306,8 @@ function emptyClaudeScan() {
5294
5306
  totalCostUSD: 0,
5295
5307
  firstDate: null,
5296
5308
  lastDate: null,
5297
- sessionsWithEarlySecrets: 0
5309
+ sessionsWithEarlySecrets: 0,
5310
+ perSession: []
5298
5311
  };
5299
5312
  }
5300
5313
  async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
@@ -5336,7 +5349,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
5336
5349
  totalCostUSD: 0,
5337
5350
  firstDate: null,
5338
5351
  lastDate: null,
5339
- sessionsWithEarlySecrets: 0
5352
+ sessionsWithEarlySecrets: 0,
5353
+ perSession: []
5340
5354
  };
5341
5355
  const dedup = emptyScanDedup();
5342
5356
  if (!fs7.existsSync(tmpDir)) return result;
@@ -5563,7 +5577,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
5563
5577
  totalCostUSD: 0,
5564
5578
  firstDate: null,
5565
5579
  lastDate: null,
5566
- sessionsWithEarlySecrets: 0
5580
+ sessionsWithEarlySecrets: 0,
5581
+ perSession: []
5567
5582
  };
5568
5583
  const dedup = emptyScanDedup();
5569
5584
  if (!fs7.existsSync(sessionsBase)) return result;
@@ -7063,7 +7078,7 @@ function Cost({ audit }) {
7063
7078
  flexGrow: 1,
7064
7079
  flexBasis: 0,
7065
7080
  children: [
7066
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST" }),
7081
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST \xB7 API value" }),
7067
7082
  audit === null ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "loading\u2026" }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
7068
7083
  /* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
7069
7084
  /* @__PURE__ */ jsx3(Box3, { width: LABEL_W2, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
package/dist/index.js CHANGED
@@ -4185,7 +4185,14 @@ var DEFAULT_CONFIG = {
4185
4185
  settings: {
4186
4186
  mode: "standard",
4187
4187
  autoStartDaemon: true,
4188
- enableHookLogDebug: true,
4188
+ // Off by default: this is a DEBUG facility, not part of the product's job.
4189
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
4190
+ // has no production reader and no size ceiling. Turn it on deliberately
4191
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
4192
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
4193
+ // keep working when it is off — see CLAUDE.md "Always write to
4194
+ // hook-debug.log in catch blocks that guard audit trail".
4195
+ enableHookLogDebug: false,
4189
4196
  approvalTimeoutMs: 12e4,
4190
4197
  // 120-second auto-deny timeout
4191
4198
  flightRecorder: true,
@@ -6643,6 +6650,7 @@ ${appPermReview}`
6643
6650
  // not an approval of an exfiltration risk. Measured against the live BE:
6644
6651
  // {approved:true} without this flag, {pending:true} with it.
6645
6652
  !!taintWarning || void 0;
6653
+ const cloudMayVote = !hardBlockDowngraded;
6646
6654
  if (cloudEnforced) {
6647
6655
  try {
6648
6656
  const initResult = await initNode9SaaS(
@@ -6658,7 +6666,7 @@ ${appPermReview}`
6658
6666
  if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6659
6667
  return { approved: true, checkedBy: "cloud" };
6660
6668
  }
6661
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6669
+ if (!forceReview) {
6662
6670
  return {
6663
6671
  approved: !!initResult.approved,
6664
6672
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -6739,7 +6747,7 @@ ${appPermReview}`
6739
6747
  }
6740
6748
  }
6741
6749
  }
6742
- if (cloudEnforced && cloudRequestId) {
6750
+ if (cloudEnforced && cloudRequestId && cloudMayVote) {
6743
6751
  racePromises.push(
6744
6752
  (async () => {
6745
6753
  try {
package/dist/index.mjs CHANGED
@@ -4155,7 +4155,14 @@ var DEFAULT_CONFIG = {
4155
4155
  settings: {
4156
4156
  mode: "standard",
4157
4157
  autoStartDaemon: true,
4158
- enableHookLogDebug: true,
4158
+ // Off by default: this is a DEBUG facility, not part of the product's job.
4159
+ // When on, every tool call appends a row to ~/.node9/hook-debug.log, which
4160
+ // has no production reader and no size ceiling. Turn it on deliberately
4161
+ // (this setting, or NODE9_DEBUG=1) while investigating something.
4162
+ // Error breadcrumbs written from catch blocks are NOT gated by this and
4163
+ // keep working when it is off — see CLAUDE.md "Always write to
4164
+ // hook-debug.log in catch blocks that guard audit trail".
4165
+ enableHookLogDebug: false,
4159
4166
  approvalTimeoutMs: 12e4,
4160
4167
  // 120-second auto-deny timeout
4161
4168
  flightRecorder: true,
@@ -6613,6 +6620,7 @@ ${appPermReview}`
6613
6620
  // not an approval of an exfiltration risk. Measured against the live BE:
6614
6621
  // {approved:true} without this flag, {pending:true} with it.
6615
6622
  !!taintWarning || void 0;
6623
+ const cloudMayVote = !hardBlockDowngraded;
6616
6624
  if (cloudEnforced) {
6617
6625
  try {
6618
6626
  const initResult = await initNode9SaaS(
@@ -6628,7 +6636,7 @@ ${appPermReview}`
6628
6636
  if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6629
6637
  return { approved: true, checkedBy: "cloud" };
6630
6638
  }
6631
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6639
+ if (!forceReview) {
6632
6640
  return {
6633
6641
  approved: !!initResult.approved,
6634
6642
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -6709,7 +6717,7 @@ ${appPermReview}`
6709
6717
  }
6710
6718
  }
6711
6719
  }
6712
- if (cloudEnforced && cloudRequestId) {
6720
+ if (cloudEnforced && cloudRequestId && cloudMayVote) {
6713
6721
  racePromises.push(
6714
6722
  (async () => {
6715
6723
  try {
package/dist/scan-ink.mjs CHANGED
@@ -45,7 +45,7 @@ var LABEL_W = 16;
45
45
  function CostPanel({ summary, width }) {
46
46
  const total = summary.stats.totalCostUSD;
47
47
  return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
48
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST" }),
48
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST \xB7 API value" }),
49
49
  /* @__PURE__ */ jsxs3(Box3, { children: [
50
50
  /* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
51
51
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
@@ -1791,7 +1791,7 @@ function StaticScorecard({ input, rangeLabel, now }) {
1791
1791
  if (reviewCount > 0) parts.push(`${reviewCount} op${reviewCount !== 1 ? "s" : ""} flagged`);
1792
1792
  if (loopCount > 0)
1793
1793
  parts.push(
1794
- `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastedCalls > 0 ? ` \xB7 ${wastedCalls} wasted calls (${wastePct}% of all calls)` : ""}`
1794
+ `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastedCalls > 0 ? ` \xB7 ${wastedCalls} repeat calls (${wastePct}% of all calls)` : ""}`
1795
1795
  );
1796
1796
  return /* @__PURE__ */ jsxs11(Fragment2, { children: [
1797
1797
  /* @__PURE__ */ jsx11(SeverityBand, { label: `Medium (${parts.join(" \xB7 ")})`, width }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",