@node9/proxy 2.8.1 → 2.8.3
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 +3 -1
- package/dist/cli.js +24 -11
- package/dist/cli.mjs +24 -11
- package/dist/dashboard.mjs +40 -5
- package/dist/scan-ink.mjs +575 -348
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<a href="https://www.npmjs.com/package/node9-ai"><img src="https://img.shields.io/npm/dm/node9-ai.svg" alt="monthly downloads" /></a>
|
|
6
6
|
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" /></a>
|
|
7
7
|
<a href="https://node9.ai/docs"><img src="https://img.shields.io/badge/docs-node9.ai-blue" alt="Documentation" /></a>
|
|
8
|
-
<a href="https://
|
|
8
|
+
<a href="https://github.com/node9-ai/node9-proxy/blob/main/.github/workflows/agent-security.yml"><img src="https://img.shields.io/badge/node9-self--scanned-a855f7?style=flat&labelColor=%231A1A2E&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNCAxNCI+PHBhdGggZmlsbD0iI0Y1RTlGRiIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03IDAuNCAxLjYgMi41djQuMmMwIDMuMSAyLjMgNS42IDUuNCA2LjkgMy4xLTEuMyA1LjQtMy44IDUuNC02LjlWMi41TDcgMC40Wm0wIDEuNSAzLjkgMS41djMuM2MwIDIuMy0xLjYgNC4yLTMuOSA1LjMtMi4zLTEuMS0zLjktMy0zLjktNS4zVjMuNEw3IDEuOVptMCAyLjJhMS45IDEuOSAwIDAgMC0xIDMuNXYxLjZoMlY3LjZhMS45IDEuOSAwIDAgMC0xLTMuNVoiLz48L3N2Zz4K" alt="node9 self-scanned" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
Node9 sits between your AI agent and the tools it can use — **discover** what it's already been doing, **protect** against risky actions in real time, and **review** what happened over any time window.
|
|
@@ -115,6 +115,8 @@ What it checks:
|
|
|
115
115
|
|
|
116
116
|
Marketplace: **[node9 Agent Security](https://github.com/marketplace/actions/node9-agent-security)**
|
|
117
117
|
|
|
118
|
+
Running it? Add the **[`scanned by node9` badge](docs/badges.md)** to your README.
|
|
119
|
+
|
|
118
120
|
## Live monitoring
|
|
119
121
|
|
|
120
122
|
<p align="center">
|
package/dist/cli.js
CHANGED
|
@@ -15757,6 +15757,7 @@ function registerScanCommand(program2) {
|
|
|
15757
15757
|
})();
|
|
15758
15758
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
15759
15759
|
const enabledShields = getConfig().policy.appliedShields ?? [];
|
|
15760
|
+
const keyed = isKeyedForPolicy();
|
|
15760
15761
|
const enabledShieldSet = new Set(enabledShields);
|
|
15761
15762
|
const screenshotMode = options.compact || options.narrative;
|
|
15762
15763
|
const useInk = !options.classic && !drillDown;
|
|
@@ -15909,7 +15910,8 @@ function registerScanCommand(program2) {
|
|
|
15909
15910
|
blastExposures,
|
|
15910
15911
|
blockedCount,
|
|
15911
15912
|
reviewCount,
|
|
15912
|
-
enabledShields
|
|
15913
|
+
enabledShields,
|
|
15914
|
+
keyed
|
|
15913
15915
|
});
|
|
15914
15916
|
return;
|
|
15915
15917
|
}
|
|
@@ -15921,7 +15923,8 @@ function registerScanCommand(program2) {
|
|
|
15921
15923
|
blastExposures,
|
|
15922
15924
|
blockedCount,
|
|
15923
15925
|
reviewCount,
|
|
15924
|
-
enabledShields
|
|
15926
|
+
enabledShields,
|
|
15927
|
+
keyed
|
|
15925
15928
|
});
|
|
15926
15929
|
return;
|
|
15927
15930
|
}
|
|
@@ -16012,7 +16015,8 @@ function registerScanCommand(program2) {
|
|
|
16012
16015
|
blastExposures,
|
|
16013
16016
|
blockedCount,
|
|
16014
16017
|
reviewCount,
|
|
16015
|
-
enabledShields
|
|
16018
|
+
enabledShields,
|
|
16019
|
+
keyed
|
|
16016
16020
|
},
|
|
16017
16021
|
rangeLabel2
|
|
16018
16022
|
);
|
|
@@ -16025,7 +16029,8 @@ function registerScanCommand(program2) {
|
|
|
16025
16029
|
blastExposures,
|
|
16026
16030
|
blockedCount,
|
|
16027
16031
|
reviewCount,
|
|
16028
|
-
enabledShields
|
|
16032
|
+
enabledShields,
|
|
16033
|
+
keyed
|
|
16029
16034
|
});
|
|
16030
16035
|
}
|
|
16031
16036
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
@@ -16248,6 +16253,7 @@ var import_chalk6, import_fs26, import_path28, import_os25, import_string_width2
|
|
|
16248
16253
|
var init_scan = __esm({
|
|
16249
16254
|
"src/cli/commands/scan.ts"() {
|
|
16250
16255
|
"use strict";
|
|
16256
|
+
init_keyed_guard();
|
|
16251
16257
|
import_chalk6 = __toESM(require("chalk"));
|
|
16252
16258
|
import_fs26 = __toESM(require("fs"));
|
|
16253
16259
|
import_path28 = __toESM(require("path"));
|
|
@@ -48272,33 +48278,39 @@ REQUIRED ACTIONS:
|
|
|
48272
48278
|
1. Remove the hardcoded credential from your command or code.
|
|
48273
48279
|
2. Use an environment variable or a dedicated secrets manager instead.
|
|
48274
48280
|
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
48275
|
-
Do NOT attempt to bypass this check or pass the credential through another tool
|
|
48281
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.
|
|
48282
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48276
48283
|
}
|
|
48277
48284
|
if (label2.includes("sql safety") && label2.includes("delete without where")) {
|
|
48278
48285
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
48279
48286
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
48280
|
-
Do NOT retry without a WHERE clause
|
|
48287
|
+
Do NOT retry without a WHERE clause.
|
|
48288
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48281
48289
|
}
|
|
48282
48290
|
if (label2.includes("sql safety") && label2.includes("update without where")) {
|
|
48283
48291
|
return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
|
|
48284
48292
|
INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
|
|
48285
|
-
Do NOT retry without a WHERE clause
|
|
48293
|
+
Do NOT retry without a WHERE clause.
|
|
48294
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48286
48295
|
}
|
|
48287
48296
|
if (label2.includes("dangerous word")) {
|
|
48288
48297
|
const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
|
|
48289
48298
|
const word = match?.[1] ?? "a dangerous keyword";
|
|
48290
48299
|
return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
|
|
48291
48300
|
INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
|
|
48292
|
-
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again
|
|
48301
|
+
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
|
|
48302
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48293
48303
|
}
|
|
48294
48304
|
if (label2.includes("path blocked") || label2.includes("sandbox")) {
|
|
48295
48305
|
return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
|
|
48296
48306
|
INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
|
|
48297
|
-
Do NOT retry on the same path
|
|
48307
|
+
Do NOT retry on the same path.
|
|
48308
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48298
48309
|
}
|
|
48299
48310
|
if (label2.includes("inline execution")) {
|
|
48300
48311
|
return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
|
|
48301
|
-
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string
|
|
48312
|
+
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
|
|
48313
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48302
48314
|
}
|
|
48303
48315
|
if (label2.includes("strict mode")) {
|
|
48304
48316
|
return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
|
|
@@ -48309,7 +48321,8 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
|
|
|
48309
48321
|
const rule = match?.[1] ?? "a policy rule";
|
|
48310
48322
|
return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
|
|
48311
48323
|
INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
|
|
48312
|
-
Do NOT attempt to bypass this rule
|
|
48324
|
+
Do NOT attempt to bypass this rule.
|
|
48325
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48313
48326
|
}
|
|
48314
48327
|
const recovery = recoveryCommand ? `
|
|
48315
48328
|
REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
|
package/dist/cli.mjs
CHANGED
|
@@ -15761,6 +15761,7 @@ function registerScanCommand(program2) {
|
|
|
15761
15761
|
})();
|
|
15762
15762
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
15763
15763
|
const enabledShields = getConfig().policy.appliedShields ?? [];
|
|
15764
|
+
const keyed = isKeyedForPolicy();
|
|
15764
15765
|
const enabledShieldSet = new Set(enabledShields);
|
|
15765
15766
|
const screenshotMode = options.compact || options.narrative;
|
|
15766
15767
|
const useInk = !options.classic && !drillDown;
|
|
@@ -15913,7 +15914,8 @@ function registerScanCommand(program2) {
|
|
|
15913
15914
|
blastExposures,
|
|
15914
15915
|
blockedCount,
|
|
15915
15916
|
reviewCount,
|
|
15916
|
-
enabledShields
|
|
15917
|
+
enabledShields,
|
|
15918
|
+
keyed
|
|
15917
15919
|
});
|
|
15918
15920
|
return;
|
|
15919
15921
|
}
|
|
@@ -15925,7 +15927,8 @@ function registerScanCommand(program2) {
|
|
|
15925
15927
|
blastExposures,
|
|
15926
15928
|
blockedCount,
|
|
15927
15929
|
reviewCount,
|
|
15928
|
-
enabledShields
|
|
15930
|
+
enabledShields,
|
|
15931
|
+
keyed
|
|
15929
15932
|
});
|
|
15930
15933
|
return;
|
|
15931
15934
|
}
|
|
@@ -16016,7 +16019,8 @@ function registerScanCommand(program2) {
|
|
|
16016
16019
|
blastExposures,
|
|
16017
16020
|
blockedCount,
|
|
16018
16021
|
reviewCount,
|
|
16019
|
-
enabledShields
|
|
16022
|
+
enabledShields,
|
|
16023
|
+
keyed
|
|
16020
16024
|
},
|
|
16021
16025
|
rangeLabel2
|
|
16022
16026
|
);
|
|
@@ -16029,7 +16033,8 @@ function registerScanCommand(program2) {
|
|
|
16029
16033
|
blastExposures,
|
|
16030
16034
|
blockedCount,
|
|
16031
16035
|
reviewCount,
|
|
16032
|
-
enabledShields
|
|
16036
|
+
enabledShields,
|
|
16037
|
+
keyed
|
|
16033
16038
|
});
|
|
16034
16039
|
}
|
|
16035
16040
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
@@ -16252,6 +16257,7 @@ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTE
|
|
|
16252
16257
|
var init_scan = __esm({
|
|
16253
16258
|
"src/cli/commands/scan.ts"() {
|
|
16254
16259
|
"use strict";
|
|
16260
|
+
init_keyed_guard();
|
|
16255
16261
|
init_shields();
|
|
16256
16262
|
init_config();
|
|
16257
16263
|
init_policy();
|
|
@@ -48264,33 +48270,39 @@ REQUIRED ACTIONS:
|
|
|
48264
48270
|
1. Remove the hardcoded credential from your command or code.
|
|
48265
48271
|
2. Use an environment variable or a dedicated secrets manager instead.
|
|
48266
48272
|
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
48267
|
-
Do NOT attempt to bypass this check or pass the credential through another tool
|
|
48273
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.
|
|
48274
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48268
48275
|
}
|
|
48269
48276
|
if (label2.includes("sql safety") && label2.includes("delete without where")) {
|
|
48270
48277
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
48271
48278
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
48272
|
-
Do NOT retry without a WHERE clause
|
|
48279
|
+
Do NOT retry without a WHERE clause.
|
|
48280
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48273
48281
|
}
|
|
48274
48282
|
if (label2.includes("sql safety") && label2.includes("update without where")) {
|
|
48275
48283
|
return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
|
|
48276
48284
|
INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
|
|
48277
|
-
Do NOT retry without a WHERE clause
|
|
48285
|
+
Do NOT retry without a WHERE clause.
|
|
48286
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48278
48287
|
}
|
|
48279
48288
|
if (label2.includes("dangerous word")) {
|
|
48280
48289
|
const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
|
|
48281
48290
|
const word = match?.[1] ?? "a dangerous keyword";
|
|
48282
48291
|
return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
|
|
48283
48292
|
INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
|
|
48284
|
-
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again
|
|
48293
|
+
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
|
|
48294
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48285
48295
|
}
|
|
48286
48296
|
if (label2.includes("path blocked") || label2.includes("sandbox")) {
|
|
48287
48297
|
return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
|
|
48288
48298
|
INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
|
|
48289
|
-
Do NOT retry on the same path
|
|
48299
|
+
Do NOT retry on the same path.
|
|
48300
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48290
48301
|
}
|
|
48291
48302
|
if (label2.includes("inline execution")) {
|
|
48292
48303
|
return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
|
|
48293
|
-
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string
|
|
48304
|
+
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
|
|
48305
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48294
48306
|
}
|
|
48295
48307
|
if (label2.includes("strict mode")) {
|
|
48296
48308
|
return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
|
|
@@ -48301,7 +48313,8 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
|
|
|
48301
48313
|
const rule = match?.[1] ?? "a policy rule";
|
|
48302
48314
|
return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
|
|
48303
48315
|
INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
|
|
48304
|
-
Do NOT attempt to bypass this rule
|
|
48316
|
+
Do NOT attempt to bypass this rule.
|
|
48317
|
+
- Tell the user node9 blocked this and why, then ask how to proceed.`;
|
|
48305
48318
|
}
|
|
48306
48319
|
const recovery = recoveryCommand ? `
|
|
48307
48320
|
REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
|
package/dist/dashboard.mjs
CHANGED
|
@@ -4809,6 +4809,22 @@ var init_report_audit = __esm({
|
|
|
4809
4809
|
}
|
|
4810
4810
|
});
|
|
4811
4811
|
|
|
4812
|
+
// src/config/keyed-guard.ts
|
|
4813
|
+
import chalk2 from "chalk";
|
|
4814
|
+
function isKeyedForPolicy() {
|
|
4815
|
+
try {
|
|
4816
|
+
return getConfig().policySource === "workspace";
|
|
4817
|
+
} catch {
|
|
4818
|
+
return false;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
var init_keyed_guard = __esm({
|
|
4822
|
+
"src/config/keyed-guard.ts"() {
|
|
4823
|
+
"use strict";
|
|
4824
|
+
init_config();
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
|
|
4812
4828
|
// src/utils/provenance.ts
|
|
4813
4829
|
import path9 from "path";
|
|
4814
4830
|
import os8 from "os";
|
|
@@ -4891,7 +4907,7 @@ var init_setup_pi_shim = __esm({
|
|
|
4891
4907
|
});
|
|
4892
4908
|
|
|
4893
4909
|
// src/setup.ts
|
|
4894
|
-
import
|
|
4910
|
+
import chalk3 from "chalk";
|
|
4895
4911
|
import { confirm as rawConfirm } from "@inquirer/prompts";
|
|
4896
4912
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4897
4913
|
import * as yaml from "yaml";
|
|
@@ -4906,7 +4922,7 @@ var init_setup = __esm({
|
|
|
4906
4922
|
});
|
|
4907
4923
|
|
|
4908
4924
|
// src/cli/render/scan-derive.ts
|
|
4909
|
-
import
|
|
4925
|
+
import chalk4 from "chalk";
|
|
4910
4926
|
import stringWidth from "string-width";
|
|
4911
4927
|
var init_scan_derive = __esm({
|
|
4912
4928
|
"src/cli/render/scan-derive.ts"() {
|
|
@@ -4930,7 +4946,7 @@ var init_scan_history = __esm({
|
|
|
4930
4946
|
});
|
|
4931
4947
|
|
|
4932
4948
|
// src/cli/commands/scan.ts
|
|
4933
|
-
import
|
|
4949
|
+
import chalk5 from "chalk";
|
|
4934
4950
|
import fs8 from "fs";
|
|
4935
4951
|
import path11 from "path";
|
|
4936
4952
|
import os10 from "os";
|
|
@@ -5845,6 +5861,7 @@ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTE
|
|
|
5845
5861
|
var init_scan = __esm({
|
|
5846
5862
|
"src/cli/commands/scan.ts"() {
|
|
5847
5863
|
"use strict";
|
|
5864
|
+
init_keyed_guard();
|
|
5848
5865
|
init_shields();
|
|
5849
5866
|
init_config();
|
|
5850
5867
|
init_policy();
|
|
@@ -7093,6 +7110,22 @@ var init_Protection = __esm({
|
|
|
7093
7110
|
}
|
|
7094
7111
|
});
|
|
7095
7112
|
|
|
7113
|
+
// src/cli/render/ink/panels/cost-note.ts
|
|
7114
|
+
function costNoteLines(keyed) {
|
|
7115
|
+
return keyed ? ["Your plan and exact spend:", "dashboard \u2192 Report"] : ["To see your plan or exact spend, connect:", "node9 login (for free)"];
|
|
7116
|
+
}
|
|
7117
|
+
function costNoteLinesForThisMachine() {
|
|
7118
|
+
return costNoteLines(isKeyedForPolicy());
|
|
7119
|
+
}
|
|
7120
|
+
var COST_LABEL;
|
|
7121
|
+
var init_cost_note = __esm({
|
|
7122
|
+
"src/cli/render/ink/panels/cost-note.ts"() {
|
|
7123
|
+
"use strict";
|
|
7124
|
+
init_keyed_guard();
|
|
7125
|
+
COST_LABEL = "COST \xB7 based on API value";
|
|
7126
|
+
}
|
|
7127
|
+
});
|
|
7128
|
+
|
|
7096
7129
|
// src/tui/dashboard/views/report/panels/Cost.tsx
|
|
7097
7130
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
7098
7131
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
@@ -7116,7 +7149,7 @@ function Cost({ audit }) {
|
|
|
7116
7149
|
flexGrow: 1,
|
|
7117
7150
|
flexBasis: 0,
|
|
7118
7151
|
children: [
|
|
7119
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children:
|
|
7152
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: COST_LABEL }),
|
|
7120
7153
|
audit === null ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "loading\u2026" }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
7121
7154
|
/* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
7122
7155
|
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W2, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
@@ -7139,7 +7172,8 @@ function Cost({ audit }) {
|
|
|
7139
7172
|
/* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
7140
7173
|
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Trend" }) }),
|
|
7141
7174
|
/* @__PURE__ */ jsx3(Text3, { children: trend || /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "(no data)" }) })
|
|
7142
|
-
] })
|
|
7175
|
+
] }),
|
|
7176
|
+
costNoteLinesForThisMachine().map((line) => /* @__PURE__ */ jsx3(Box3, { height: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: line }) }, line))
|
|
7143
7177
|
] })
|
|
7144
7178
|
]
|
|
7145
7179
|
}
|
|
@@ -7151,6 +7185,7 @@ var init_Cost = __esm({
|
|
|
7151
7185
|
"use strict";
|
|
7152
7186
|
init_panels();
|
|
7153
7187
|
init_format();
|
|
7188
|
+
init_cost_note();
|
|
7154
7189
|
init_util();
|
|
7155
7190
|
LABEL_W2 = 10;
|
|
7156
7191
|
}
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -39,327 +39,169 @@ function formatCost(usd) {
|
|
|
39
39
|
return `$${(usd / 1e3).toFixed(1)}K`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// src/
|
|
43
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
44
|
-
var LABEL_W = 16;
|
|
45
|
-
function CostPanel({ summary, width }) {
|
|
46
|
-
const total = summary.stats.totalCostUSD;
|
|
47
|
-
return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
48
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "COST \xB7 API value" }),
|
|
49
|
-
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
50
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
51
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
|
|
52
|
-
] }),
|
|
53
|
-
summary.byAgent.map((agent) => /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
54
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: agent.label }) }),
|
|
55
|
-
/* @__PURE__ */ jsx3(Text3, { children: formatCost(agent.costUSD) })
|
|
56
|
-
] }, agent.id)),
|
|
57
|
-
summary.loopWastedUSD > 0 ? /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
58
|
-
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Wasted on loops" }) }),
|
|
59
|
-
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "~" + formatCost(summary.loopWastedUSD) })
|
|
60
|
-
] }) : null
|
|
61
|
-
] });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/cli/render/ink/panels/ActivityPanel.tsx
|
|
65
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
66
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
67
|
-
var LABEL_W2 = 16;
|
|
68
|
-
function fmtNum(n) {
|
|
69
|
-
return n.toLocaleString();
|
|
70
|
-
}
|
|
71
|
-
function countMcpFindings(scan) {
|
|
72
|
-
let n = 0;
|
|
73
|
-
for (const f of scan.findings) {
|
|
74
|
-
if (f.toolName.startsWith("mcp__")) n++;
|
|
75
|
-
}
|
|
76
|
-
return n;
|
|
77
|
-
}
|
|
78
|
-
function ActivityPanel({ summary, scan, width }) {
|
|
79
|
-
const { sessions, totalToolCalls, bashCalls, totalCostUSD } = summary.stats;
|
|
80
|
-
const perSession = sessions > 0 ? totalCostUSD / sessions : 0;
|
|
81
|
-
const toolsPerSession = sessions > 0 ? Math.round(totalToolCalls / sessions) : 0;
|
|
82
|
-
const mcpCount = countMcpFindings(scan);
|
|
83
|
-
const topAgent = summary.byAgent.slice().sort((a, b) => b.sessions - a.sessions)[0];
|
|
84
|
-
return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
85
|
-
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "ACTIVITY" }),
|
|
86
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
87
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Sessions" }) }),
|
|
88
|
-
/* @__PURE__ */ jsx4(Text4, { bold: true, children: fmtNum(sessions) })
|
|
89
|
-
] }),
|
|
90
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
91
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools" }) }),
|
|
92
|
-
/* @__PURE__ */ jsx4(Text4, { children: fmtNum(totalToolCalls) })
|
|
93
|
-
] }),
|
|
94
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
95
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Shell" }) }),
|
|
96
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(bashCalls) })
|
|
97
|
-
] }),
|
|
98
|
-
mcpCount > 0 ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
99
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " MCP" }) }),
|
|
100
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(mcpCount) })
|
|
101
|
-
] }) : null,
|
|
102
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
103
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools / session" }) }),
|
|
104
|
-
/* @__PURE__ */ jsx4(Text4, { children: "~" + fmtNum(toolsPerSession) })
|
|
105
|
-
] }),
|
|
106
|
-
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
107
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Cost / session" }) }),
|
|
108
|
-
/* @__PURE__ */ jsx4(Text4, { children: "~$" + Math.round(perSession).toLocaleString() })
|
|
109
|
-
] }),
|
|
110
|
-
topAgent ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
111
|
-
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Top agent" }) }),
|
|
112
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${topAgent.label} (${fmtNum(topAgent.sessions)})` })
|
|
113
|
-
] }) : null
|
|
114
|
-
] });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
118
|
-
import { Box as Box5, Text as Text5 } from "ink";
|
|
119
|
-
|
|
120
|
-
// src/cli/render/scan-derive.ts
|
|
42
|
+
// src/config/keyed-guard.ts
|
|
121
43
|
import chalk from "chalk";
|
|
122
|
-
import stringWidth from "string-width";
|
|
123
|
-
function topRulesByVerdict(sections, verdict, n) {
|
|
124
|
-
const matched = [];
|
|
125
|
-
for (const section of sections) {
|
|
126
|
-
for (const rule of section.rules) {
|
|
127
|
-
const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
|
|
128
|
-
if (matches) matched.push({ name: rule.name, count: rule.findings.length });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return matched.sort((a, b) => b.count - a.count).slice(0, n);
|
|
132
|
-
}
|
|
133
|
-
function computeLoopWaste(loops, totalToolCalls) {
|
|
134
|
-
const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
135
|
-
const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
|
|
136
|
-
return { wastedCalls, wastePct };
|
|
137
|
-
}
|
|
138
|
-
function rollupByShield(sections, topRulesPerShield = 3) {
|
|
139
|
-
const out = [];
|
|
140
|
-
for (const section of sections) {
|
|
141
|
-
if (section.sourceType !== "shield") continue;
|
|
142
|
-
if (!section.shieldKey) continue;
|
|
143
|
-
const totalCatches = section.blockedCount + section.reviewCount;
|
|
144
|
-
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);
|
|
145
|
-
out.push({
|
|
146
|
-
shieldName: section.shieldKey,
|
|
147
|
-
totalCatches,
|
|
148
|
-
blockCatches: section.blockedCount,
|
|
149
|
-
reviewCatches: section.reviewCount,
|
|
150
|
-
topRuleLabels
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return out.sort((a, b) => b.totalCatches - a.totalCatches);
|
|
154
|
-
}
|
|
155
|
-
function originForRule(ruleName, sections, enabled) {
|
|
156
|
-
for (const section of sections) {
|
|
157
|
-
if (section.rules.some((r) => r.name === ruleName)) {
|
|
158
|
-
if (section.sourceType === "default") return "default";
|
|
159
|
-
if (section.sourceType === "shield") {
|
|
160
|
-
const key = section.shieldKey ?? section.id;
|
|
161
|
-
return enabled.has(key) ? `\u2713 shield:${key}` : `needs shield:${key}`;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
167
|
-
function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
168
|
-
const t = new Date(timestamp).getTime();
|
|
169
|
-
if (Number.isNaN(t)) return "?";
|
|
170
|
-
const days = Math.floor((now.getTime() - t) / 864e5);
|
|
171
|
-
if (days < 1) return "today";
|
|
172
|
-
if (days > 90) return "90d+";
|
|
173
|
-
return `${days}d`;
|
|
174
|
-
}
|
|
175
44
|
|
|
176
|
-
// src/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
] }, i)),
|
|
198
|
-
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
199
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2192 " }),
|
|
200
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "DLP" }),
|
|
201
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
|
|
202
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "node9 mask" }),
|
|
203
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: " (runtime + cleanup)" })
|
|
204
|
-
] })
|
|
205
|
-
] });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/cli/render/ink/panels/BlockedPanel.tsx
|
|
209
|
-
import { Box as Box6, Text as Text6 } from "ink";
|
|
210
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
211
|
-
var ROW_LIMIT2 = 12;
|
|
212
|
-
function BlockedPanel({
|
|
213
|
-
summary,
|
|
214
|
-
width,
|
|
215
|
-
enabledShields = []
|
|
216
|
-
}) {
|
|
217
|
-
const enabled = new Set(enabledShields);
|
|
218
|
-
const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
|
|
219
|
-
const rules = allRules.slice(0, ROW_LIMIT2);
|
|
220
|
-
if (rules.length === 0) return null;
|
|
221
|
-
const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
|
|
222
|
-
const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
|
|
223
|
-
const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
|
|
224
|
-
return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
225
|
-
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
|
|
226
|
-
"WOULD HAVE BLOCKED",
|
|
227
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
|
|
228
|
-
] }),
|
|
229
|
-
rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
230
|
-
/* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
|
|
231
|
-
/* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
|
|
232
|
-
/* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
233
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
234
|
-
] }, i)),
|
|
235
|
-
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
|
|
236
|
-
] });
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// src/cli/render/ink/panels/BlastRadiusPanel.tsx
|
|
240
|
-
import { Box as Box7, Text as Text7 } from "ink";
|
|
241
|
-
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
242
|
-
var ROW_LIMIT3 = 4;
|
|
243
|
-
var ENV_ROW_LIMIT = 3;
|
|
244
|
-
function BlastRadiusPanel({
|
|
245
|
-
blast,
|
|
246
|
-
blastExposures,
|
|
247
|
-
width
|
|
248
|
-
}) {
|
|
249
|
-
if (blastExposures === 0) return null;
|
|
250
|
-
const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
|
|
251
|
-
return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
|
|
252
|
-
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
|
|
253
|
-
"BLAST RADIUS",
|
|
254
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
|
|
255
|
-
] }),
|
|
256
|
-
blast.reachable.slice(0, ROW_LIMIT3).map((path, i) => {
|
|
257
|
-
const desc = path.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
|
|
258
|
-
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
259
|
-
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
|
|
260
|
-
/* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path.label }) }),
|
|
261
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
|
|
262
|
-
] }, i);
|
|
263
|
-
}),
|
|
264
|
-
blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
265
|
-
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
|
|
266
|
-
/* @__PURE__ */ jsx7(Text7, { children: env.key }),
|
|
267
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
|
|
268
|
-
] }, `env-${i}`)),
|
|
269
|
-
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
270
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
|
|
271
|
-
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
|
|
272
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
|
|
273
|
-
] })
|
|
274
|
-
] });
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/cli/render/ink/panels/ReviewQueuePanel.tsx
|
|
278
|
-
import { Box as Box8, Text as Text8 } from "ink";
|
|
279
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
280
|
-
var ROW_LIMIT4 = 5;
|
|
281
|
-
function ReviewQueuePanel({
|
|
282
|
-
summary,
|
|
283
|
-
width,
|
|
284
|
-
enabledShields = []
|
|
285
|
-
}) {
|
|
286
|
-
const enabled = new Set(enabledShields);
|
|
287
|
-
const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
|
|
288
|
-
if (rules.length === 0) return null;
|
|
289
|
-
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
290
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
|
|
291
|
-
rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
292
|
-
/* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
|
|
293
|
-
/* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
294
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
295
|
-
] }, i)),
|
|
296
|
-
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
297
|
-
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
298
|
-
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
|
|
299
|
-
] })
|
|
300
|
-
] });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// src/cli/render/ink/panels/AgentLoopsPanel.tsx
|
|
304
|
-
import { Box as Box9, Text as Text9 } from "ink";
|
|
305
|
-
import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
306
|
-
var TOOL_ROW_LIMIT = 5;
|
|
307
|
-
var STUCK_ROW_LIMIT = 1;
|
|
308
|
-
function fmtNum2(n) {
|
|
309
|
-
return n.toLocaleString();
|
|
310
|
-
}
|
|
311
|
-
function trimRight(s, width) {
|
|
312
|
-
if (s.length <= width) return s;
|
|
313
|
-
return "\u2026" + s.slice(s.length - (width - 1));
|
|
314
|
-
}
|
|
315
|
-
function AgentLoopsPanel({ loopFindings, width }) {
|
|
316
|
-
if (loopFindings.length === 0) return null;
|
|
317
|
-
const byTool = /* @__PURE__ */ new Map();
|
|
318
|
-
let totalRepeats = 0;
|
|
319
|
-
for (const f of loopFindings) {
|
|
320
|
-
const repeats = Math.max(0, f.count - 1);
|
|
321
|
-
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
|
|
322
|
-
totalRepeats += repeats;
|
|
323
|
-
}
|
|
324
|
-
const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
|
|
325
|
-
const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
|
|
326
|
-
return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
327
|
-
/* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
|
|
328
|
-
"AGENT LOOPS",
|
|
329
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
|
|
330
|
-
] }),
|
|
331
|
-
toolEntries.map(([tool, repeats]) => {
|
|
332
|
-
const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
|
|
333
|
-
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
334
|
-
/* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
|
|
335
|
-
/* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
|
|
336
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
|
|
337
|
-
] }, tool);
|
|
338
|
-
}),
|
|
339
|
-
topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
340
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
|
|
341
|
-
topStuck.map((f, i) => {
|
|
342
|
-
const target = trimRight(f.commandPreview || f.toolName, 32);
|
|
343
|
-
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
344
|
-
/* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
|
|
345
|
-
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
|
|
346
|
-
] }, `stuck-${i}`);
|
|
45
|
+
// src/config-schema.ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
48
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
49
|
+
});
|
|
50
|
+
var SmartConditionSchema = z.object({
|
|
51
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
52
|
+
op: z.enum(
|
|
53
|
+
[
|
|
54
|
+
"matches",
|
|
55
|
+
"notMatches",
|
|
56
|
+
"contains",
|
|
57
|
+
"notContains",
|
|
58
|
+
"exists",
|
|
59
|
+
"notExists",
|
|
60
|
+
"matchesGlob",
|
|
61
|
+
"notMatchesGlob"
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
errorMap: () => ({
|
|
65
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
347
66
|
})
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
value: z.string().optional(),
|
|
70
|
+
flags: z.string().optional()
|
|
71
|
+
}).refine(
|
|
72
|
+
(c) => {
|
|
73
|
+
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
{ message: "matchesGlob and notMatchesGlob conditions require a value field" }
|
|
77
|
+
);
|
|
78
|
+
var SmartRuleSchema = z.object({
|
|
79
|
+
name: z.string().optional(),
|
|
80
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
81
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
82
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
83
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
84
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
85
|
+
}),
|
|
86
|
+
reason: z.string().optional(),
|
|
87
|
+
description: z.string().optional(),
|
|
88
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
89
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
90
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
91
|
+
dependsOnState: z.array(z.string()).transform(
|
|
92
|
+
(arr) => arr.filter(
|
|
93
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
94
|
+
)
|
|
95
|
+
).optional(),
|
|
96
|
+
recoveryCommand: z.string().optional()
|
|
97
|
+
});
|
|
98
|
+
var ConfigFileSchema = z.object({
|
|
99
|
+
version: z.string().optional(),
|
|
100
|
+
settings: z.object({
|
|
101
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
102
|
+
autoStartDaemon: z.boolean().optional(),
|
|
103
|
+
// enableUndo / policy.snapshot: removed with the undo feature. The
|
|
104
|
+
// schema strips undeclared keys silently (plain zod object — no
|
|
105
|
+
// .strict(), no .passthrough()), so a config still carrying them stays
|
|
106
|
+
// valid and simply loses them. Keeping the fields would instead make
|
|
107
|
+
// `node9_config_get` advertise a removed feature as a live knob.
|
|
108
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
109
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
110
|
+
approvalTimeoutSeconds: z.number().nonnegative().optional(),
|
|
111
|
+
flightRecorder: z.boolean().optional(),
|
|
112
|
+
approvers: z.object({
|
|
113
|
+
native: z.boolean().optional(),
|
|
114
|
+
browser: z.boolean().optional(),
|
|
115
|
+
cloud: z.boolean().optional(),
|
|
116
|
+
terminal: z.boolean().optional()
|
|
117
|
+
}).optional(),
|
|
118
|
+
environment: z.string().optional(),
|
|
119
|
+
slackEnabled: z.boolean().optional(),
|
|
120
|
+
enableTrustSessions: z.boolean().optional(),
|
|
121
|
+
allowGlobalPause: z.boolean().optional(),
|
|
122
|
+
auditHashArgs: z.boolean().optional(),
|
|
123
|
+
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
124
|
+
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
125
|
+
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
126
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
127
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
128
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
129
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
130
|
+
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
131
|
+
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
132
|
+
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
133
|
+
// must run them from the CLI. node9's threat model is the agent itself.
|
|
134
|
+
mcpAllowWeakening: z.boolean().optional(),
|
|
135
|
+
// Auto-wire reconciler (P3 2.6): auto-wrap new ungoverned MCP servers vs
|
|
136
|
+
// nudge-only (default), and the scan cadence in minutes.
|
|
137
|
+
mcpAutoWrap: z.boolean().optional(),
|
|
138
|
+
mcpReconcileIntervalMinutes: z.number().positive().optional(),
|
|
139
|
+
mcpStaleAfterDays: z.number().min(0).optional(),
|
|
140
|
+
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
141
|
+
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
142
|
+
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
143
|
+
// for an incident; clamped to a 15s floor so it can't hammer the API.
|
|
144
|
+
// Unset → falls back to hours, then the 5h default.
|
|
145
|
+
cloudSyncIntervalSeconds: z.number().positive().optional(),
|
|
146
|
+
// Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
|
|
147
|
+
// to true; set false to fall back to local-only auditing.
|
|
148
|
+
shipper: z.object({
|
|
149
|
+
enabled: z.boolean().optional(),
|
|
150
|
+
intervalSeconds: z.number().min(5).optional()
|
|
151
|
+
}).optional()
|
|
152
|
+
}).optional(),
|
|
153
|
+
policy: z.object({
|
|
154
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
155
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
156
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
157
|
+
toolInspection: z.record(z.string()).optional(),
|
|
158
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
159
|
+
dlp: z.object({
|
|
160
|
+
enabled: z.boolean().optional(),
|
|
161
|
+
scanIgnoredTools: z.boolean().optional(),
|
|
162
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
163
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
164
|
+
}).optional(),
|
|
165
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
166
|
+
// deliberately exclude 'off' — tighten-only.
|
|
167
|
+
commandChecks: z.object({
|
|
168
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
169
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
170
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
171
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
172
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
173
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
174
|
+
}).optional(),
|
|
175
|
+
egress: z.object({
|
|
176
|
+
enabled: z.boolean().optional(),
|
|
177
|
+
mode: z.enum(["off", "review", "block"]).optional(),
|
|
178
|
+
allow: z.array(z.string()).optional(),
|
|
179
|
+
deny: z.array(z.string()).optional(),
|
|
180
|
+
allowPrivate: z.boolean().optional()
|
|
181
|
+
}).optional(),
|
|
182
|
+
loopDetection: z.object({
|
|
183
|
+
enabled: z.boolean().optional(),
|
|
184
|
+
threshold: z.number().min(2).optional(),
|
|
185
|
+
windowSeconds: z.number().min(10).optional()
|
|
186
|
+
}).optional(),
|
|
187
|
+
injectionScan: z.object({
|
|
188
|
+
enabled: z.boolean().optional(),
|
|
189
|
+
minConfidence: z.enum(["medium", "high"]).optional(),
|
|
190
|
+
allow: z.array(z.string()).optional()
|
|
191
|
+
}).optional(),
|
|
192
|
+
skillPinning: z.object({
|
|
193
|
+
enabled: z.boolean().optional(),
|
|
194
|
+
mode: z.enum(["warn", "block"]).optional(),
|
|
195
|
+
roots: z.array(z.string()).optional()
|
|
196
|
+
}).optional()
|
|
197
|
+
}).optional(),
|
|
198
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
199
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
358
200
|
|
|
359
|
-
// src/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
201
|
+
// src/shields.ts
|
|
202
|
+
import fs from "fs";
|
|
203
|
+
import path from "path";
|
|
204
|
+
import os from "os";
|
|
363
205
|
|
|
364
206
|
// packages/policy-engine/dist/index.mjs
|
|
365
207
|
import safeRegex from "safe-regex2";
|
|
@@ -1637,40 +1479,425 @@ var redis_default = {
|
|
|
1637
1479
|
verdict: "review",
|
|
1638
1480
|
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
1639
1481
|
}
|
|
1640
|
-
],
|
|
1641
|
-
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
1642
|
-
};
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
};
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1482
|
+
],
|
|
1483
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
1484
|
+
};
|
|
1485
|
+
function validateShieldDefinition(raw) {
|
|
1486
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1487
|
+
return { error: "Shield file is not an object" };
|
|
1488
|
+
}
|
|
1489
|
+
const r = raw;
|
|
1490
|
+
if (typeof r.name !== "string" || !r.name) return { error: "Shield file missing 'name'" };
|
|
1491
|
+
if (typeof r.description !== "string") return { error: "Shield file missing 'description'" };
|
|
1492
|
+
if (!Array.isArray(r.aliases)) return { error: "Shield file missing 'aliases' array" };
|
|
1493
|
+
if (!Array.isArray(r.smartRules)) return { error: "Shield file missing 'smartRules' array" };
|
|
1494
|
+
if (!Array.isArray(r.dangerousWords))
|
|
1495
|
+
return { error: "Shield file missing 'dangerousWords' array" };
|
|
1496
|
+
return { ok: r };
|
|
1497
|
+
}
|
|
1498
|
+
var BUILTIN_SHIELDS = {
|
|
1499
|
+
[aws_default.name]: aws_default,
|
|
1500
|
+
[bash_safe_default.name]: bash_safe_default,
|
|
1501
|
+
[docker_default.name]: docker_default,
|
|
1502
|
+
[filesystem_default.name]: filesystem_default,
|
|
1503
|
+
[github_default.name]: github_default,
|
|
1504
|
+
[k8s_default.name]: k8s_default,
|
|
1505
|
+
[mongodb_default.name]: mongodb_default,
|
|
1506
|
+
[postgres_default.name]: postgres_default,
|
|
1507
|
+
[project_jail_default.name]: project_jail_default,
|
|
1508
|
+
[redis_default.name]: redis_default
|
|
1509
|
+
};
|
|
1510
|
+
function assertBuiltinShieldRegexesAreSafe() {
|
|
1511
|
+
for (const shield of Object.values(BUILTIN_SHIELDS)) {
|
|
1512
|
+
for (const rule of shield.smartRules) {
|
|
1513
|
+
const conditions = rule.conditions ?? [];
|
|
1514
|
+
for (const cond of conditions) {
|
|
1515
|
+
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
1516
|
+
const pattern = cond.value;
|
|
1517
|
+
if (!pattern) continue;
|
|
1518
|
+
if (!safeRegex3(pattern)) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
assertBuiltinShieldRegexesAreSafe();
|
|
1528
|
+
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
1529
|
+
|
|
1530
|
+
// src/shields.ts
|
|
1531
|
+
var USER_SHIELDS_DIR = path.join(os.homedir(), ".node9", "shields");
|
|
1532
|
+
function loadUserShields() {
|
|
1533
|
+
const result = {};
|
|
1534
|
+
let entries;
|
|
1535
|
+
try {
|
|
1536
|
+
entries = fs.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
if (err.code !== "ENOENT") {
|
|
1539
|
+
process.stderr.write(
|
|
1540
|
+
`[node9] Could not read user shields dir ${USER_SHIELDS_DIR}: ${String(err)}
|
|
1541
|
+
`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
return result;
|
|
1545
|
+
}
|
|
1546
|
+
for (const file of entries) {
|
|
1547
|
+
const filePath = path.join(USER_SHIELDS_DIR, file);
|
|
1548
|
+
try {
|
|
1549
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1550
|
+
const v = validateShieldDefinition(raw);
|
|
1551
|
+
if ("error" in v) {
|
|
1552
|
+
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
1553
|
+
`);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
result[v.ok.name] = v.ok;
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
1559
|
+
`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return result;
|
|
1563
|
+
}
|
|
1564
|
+
function buildSHIELDS() {
|
|
1565
|
+
return { ...BUILTIN_SHIELDS, ...loadUserShields() };
|
|
1566
|
+
}
|
|
1567
|
+
var SHIELDS = buildSHIELDS();
|
|
1568
|
+
var SHIELDS_STATE_FILE = path.join(os.homedir(), ".node9", "shields.json");
|
|
1569
|
+
|
|
1570
|
+
// src/config/index.ts
|
|
1571
|
+
var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
1572
|
+
|
|
1573
|
+
// src/cli/render/ink/panels/cost-note.ts
|
|
1574
|
+
var COST_LABEL = "COST \xB7 based on API value";
|
|
1575
|
+
function costNoteLines(keyed) {
|
|
1576
|
+
return keyed ? ["Your plan and exact spend:", "dashboard \u2192 Report"] : ["To see your plan or exact spend, connect:", "node9 login (for free)"];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/cli/render/ink/panels/CostPanel.tsx
|
|
1580
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1581
|
+
var LABEL_W = 16;
|
|
1582
|
+
function CostPanel({ summary, width, keyed }) {
|
|
1583
|
+
const total = summary.stats.totalCostUSD;
|
|
1584
|
+
return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1585
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: COST_LABEL }),
|
|
1586
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1587
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: "Total" }) }),
|
|
1588
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: formatCost(total) })
|
|
1589
|
+
] }),
|
|
1590
|
+
summary.byAgent.map((agent) => /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1591
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { children: agent.label }) }),
|
|
1592
|
+
/* @__PURE__ */ jsx3(Text3, { children: formatCost(agent.costUSD) })
|
|
1593
|
+
] }, agent.id)),
|
|
1594
|
+
summary.loopWastedUSD > 0 ? /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1595
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_W, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Wasted on loops" }) }),
|
|
1596
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "~" + formatCost(summary.loopWastedUSD) })
|
|
1597
|
+
] }) : null,
|
|
1598
|
+
costNoteLines(keyed ?? false).map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: line }, line))
|
|
1599
|
+
] });
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/cli/render/ink/panels/ActivityPanel.tsx
|
|
1603
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1604
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1605
|
+
var LABEL_W2 = 16;
|
|
1606
|
+
function fmtNum(n) {
|
|
1607
|
+
return n.toLocaleString();
|
|
1608
|
+
}
|
|
1609
|
+
function countMcpFindings(scan) {
|
|
1610
|
+
let n = 0;
|
|
1611
|
+
for (const f of scan.findings) {
|
|
1612
|
+
if (f.toolName.startsWith("mcp__")) n++;
|
|
1613
|
+
}
|
|
1614
|
+
return n;
|
|
1615
|
+
}
|
|
1616
|
+
function ActivityPanel({ summary, scan, width }) {
|
|
1617
|
+
const { sessions, totalToolCalls, bashCalls, totalCostUSD } = summary.stats;
|
|
1618
|
+
const perSession = sessions > 0 ? totalCostUSD / sessions : 0;
|
|
1619
|
+
const toolsPerSession = sessions > 0 ? Math.round(totalToolCalls / sessions) : 0;
|
|
1620
|
+
const mcpCount = countMcpFindings(scan);
|
|
1621
|
+
const topAgent = summary.byAgent.slice().sort((a, b) => b.sessions - a.sessions)[0];
|
|
1622
|
+
return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1623
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "ACTIVITY" }),
|
|
1624
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1625
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Sessions" }) }),
|
|
1626
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: fmtNum(sessions) })
|
|
1627
|
+
] }),
|
|
1628
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1629
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools" }) }),
|
|
1630
|
+
/* @__PURE__ */ jsx4(Text4, { children: fmtNum(totalToolCalls) })
|
|
1631
|
+
] }),
|
|
1632
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1633
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Shell" }) }),
|
|
1634
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(bashCalls) })
|
|
1635
|
+
] }),
|
|
1636
|
+
mcpCount > 0 ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1637
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " MCP" }) }),
|
|
1638
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: fmtNum(mcpCount) })
|
|
1639
|
+
] }) : null,
|
|
1640
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1641
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Tools / session" }) }),
|
|
1642
|
+
/* @__PURE__ */ jsx4(Text4, { children: "~" + fmtNum(toolsPerSession) })
|
|
1643
|
+
] }),
|
|
1644
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1645
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { children: "Cost / session" }) }),
|
|
1646
|
+
/* @__PURE__ */ jsx4(Text4, { children: "~$" + Math.round(perSession).toLocaleString() })
|
|
1647
|
+
] }),
|
|
1648
|
+
topAgent ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1649
|
+
/* @__PURE__ */ jsx4(Box4, { width: LABEL_W2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Top agent" }) }),
|
|
1650
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${topAgent.label} (${fmtNum(topAgent.sessions)})` })
|
|
1651
|
+
] }) : null
|
|
1652
|
+
] });
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
1656
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1657
|
+
|
|
1658
|
+
// src/cli/render/scan-derive.ts
|
|
1659
|
+
import chalk2 from "chalk";
|
|
1660
|
+
import stringWidth from "string-width";
|
|
1661
|
+
function topRulesByVerdict(sections, verdict, n) {
|
|
1662
|
+
const matched = [];
|
|
1663
|
+
for (const section of sections) {
|
|
1664
|
+
for (const rule of section.rules) {
|
|
1665
|
+
const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
|
|
1666
|
+
if (matches) matched.push({ name: rule.name, count: rule.findings.length });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return matched.sort((a, b) => b.count - a.count).slice(0, n);
|
|
1670
|
+
}
|
|
1671
|
+
function computeLoopWaste(loops, totalToolCalls) {
|
|
1672
|
+
const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
1673
|
+
const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
|
|
1674
|
+
return { wastedCalls, wastePct };
|
|
1675
|
+
}
|
|
1676
|
+
function rollupByShield(sections, topRulesPerShield = 3) {
|
|
1677
|
+
const out = [];
|
|
1678
|
+
for (const section of sections) {
|
|
1679
|
+
if (section.sourceType !== "shield") continue;
|
|
1680
|
+
if (!section.shieldKey) continue;
|
|
1681
|
+
const totalCatches = section.blockedCount + section.reviewCount;
|
|
1682
|
+
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);
|
|
1683
|
+
out.push({
|
|
1684
|
+
shieldName: section.shieldKey,
|
|
1685
|
+
totalCatches,
|
|
1686
|
+
blockCatches: section.blockedCount,
|
|
1687
|
+
reviewCatches: section.reviewCount,
|
|
1688
|
+
topRuleLabels
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return out.sort((a, b) => b.totalCatches - a.totalCatches);
|
|
1692
|
+
}
|
|
1693
|
+
function originForRule(ruleName, sections, enabled) {
|
|
1694
|
+
for (const section of sections) {
|
|
1695
|
+
if (section.rules.some((r) => r.name === ruleName)) {
|
|
1696
|
+
if (section.sourceType === "default") return "default";
|
|
1697
|
+
if (section.sourceType === "shield") {
|
|
1698
|
+
const key = section.shieldKey ?? section.id;
|
|
1699
|
+
return enabled.has(key) ? `\u2713 shield:${key}` : `needs shield:${key}`;
|
|
1668
1700
|
}
|
|
1669
1701
|
}
|
|
1670
1702
|
}
|
|
1703
|
+
return "";
|
|
1671
1704
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1705
|
+
function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
1706
|
+
const t = new Date(timestamp).getTime();
|
|
1707
|
+
if (Number.isNaN(t)) return "?";
|
|
1708
|
+
const days = Math.floor((now.getTime() - t) / 864e5);
|
|
1709
|
+
if (days < 1) return "today";
|
|
1710
|
+
if (days > 90) return "90d+";
|
|
1711
|
+
return `${days}d`;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/cli/render/ink/panels/title-count.ts
|
|
1715
|
+
function topOf(shown, total) {
|
|
1716
|
+
return total > shown ? ` \xB7 top ${shown} of ${total}` : "";
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/cli/render/ink/panels/LeaksPanel.tsx
|
|
1720
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1721
|
+
var ROW_LIMIT = 4;
|
|
1722
|
+
function LeaksPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
|
|
1723
|
+
const leaks = summary.leaks;
|
|
1724
|
+
if (leaks.length === 0) return null;
|
|
1725
|
+
return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
1726
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "red", children: [
|
|
1727
|
+
"CREDENTIAL LEAKS",
|
|
1728
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: topOf(Math.min(leaks.length, ROW_LIMIT), leaks.length) })
|
|
1729
|
+
] }),
|
|
1730
|
+
leaks.slice(0, ROW_LIMIT).map((leak, i) => /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1731
|
+
/* @__PURE__ */ jsx5(Box5, { width: 5, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: relativeDate(leak.timestamp, now).padStart(4) }) }),
|
|
1732
|
+
/* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, wrap: "truncate-end", children: leak.patternName }) }),
|
|
1733
|
+
/* @__PURE__ */ jsx5(Box5, { width: 15, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: `[${leak.toolName}]` }) }),
|
|
1734
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: leak.agent })
|
|
1735
|
+
] }, i)),
|
|
1736
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1737
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2192 " }),
|
|
1738
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "DLP" }),
|
|
1739
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
|
|
1740
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "node9 mask" }),
|
|
1741
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: " (runtime + cleanup)" })
|
|
1742
|
+
] })
|
|
1743
|
+
] });
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/cli/render/ink/panels/BlockedPanel.tsx
|
|
1747
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1748
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1749
|
+
var ROW_LIMIT2 = 12;
|
|
1750
|
+
function BlockedPanel({
|
|
1751
|
+
summary,
|
|
1752
|
+
width,
|
|
1753
|
+
enabledShields = []
|
|
1754
|
+
}) {
|
|
1755
|
+
const enabled = new Set(enabledShields);
|
|
1756
|
+
const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
|
|
1757
|
+
const rules = allRules.slice(0, ROW_LIMIT2);
|
|
1758
|
+
if (rules.length === 0) return null;
|
|
1759
|
+
const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
|
|
1760
|
+
const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
|
|
1761
|
+
const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
|
|
1762
|
+
return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
|
|
1763
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
|
|
1764
|
+
"WOULD HAVE BLOCKED",
|
|
1765
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
|
|
1766
|
+
] }),
|
|
1767
|
+
rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1768
|
+
/* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
|
|
1769
|
+
/* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
|
|
1770
|
+
/* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
1771
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
1772
|
+
] }, i)),
|
|
1773
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
|
|
1774
|
+
] });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/cli/render/ink/panels/BlastRadiusPanel.tsx
|
|
1778
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1779
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1780
|
+
var ROW_LIMIT3 = 4;
|
|
1781
|
+
var ENV_ROW_LIMIT = 3;
|
|
1782
|
+
function BlastRadiusPanel({
|
|
1783
|
+
blast,
|
|
1784
|
+
blastExposures,
|
|
1785
|
+
width
|
|
1786
|
+
}) {
|
|
1787
|
+
if (blastExposures === 0) return null;
|
|
1788
|
+
const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
|
|
1789
|
+
return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
|
|
1790
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
|
|
1791
|
+
"BLAST RADIUS",
|
|
1792
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
|
|
1793
|
+
] }),
|
|
1794
|
+
blast.reachable.slice(0, ROW_LIMIT3).map((path2, i) => {
|
|
1795
|
+
const desc = path2.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
|
|
1796
|
+
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1797
|
+
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
|
|
1798
|
+
/* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path2.label }) }),
|
|
1799
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
|
|
1800
|
+
] }, i);
|
|
1801
|
+
}),
|
|
1802
|
+
blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1803
|
+
/* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
|
|
1804
|
+
/* @__PURE__ */ jsx7(Text7, { children: env.key }),
|
|
1805
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
|
|
1806
|
+
] }, `env-${i}`)),
|
|
1807
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1808
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
|
|
1809
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
|
|
1810
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
|
|
1811
|
+
] })
|
|
1812
|
+
] });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/cli/render/ink/panels/ReviewQueuePanel.tsx
|
|
1816
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1817
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1818
|
+
var ROW_LIMIT4 = 5;
|
|
1819
|
+
function ReviewQueuePanel({
|
|
1820
|
+
summary,
|
|
1821
|
+
width,
|
|
1822
|
+
enabledShields = []
|
|
1823
|
+
}) {
|
|
1824
|
+
const enabled = new Set(enabledShields);
|
|
1825
|
+
const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
|
|
1826
|
+
if (rules.length === 0) return null;
|
|
1827
|
+
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1828
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
|
|
1829
|
+
rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1830
|
+
/* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
|
|
1831
|
+
/* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
|
|
1832
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
|
|
1833
|
+
] }, i)),
|
|
1834
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1835
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
1836
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
|
|
1837
|
+
] })
|
|
1838
|
+
] });
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/cli/render/ink/panels/AgentLoopsPanel.tsx
|
|
1842
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1843
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1844
|
+
var TOOL_ROW_LIMIT = 5;
|
|
1845
|
+
var STUCK_ROW_LIMIT = 1;
|
|
1846
|
+
function fmtNum2(n) {
|
|
1847
|
+
return n.toLocaleString();
|
|
1848
|
+
}
|
|
1849
|
+
function trimRight(s, width) {
|
|
1850
|
+
if (s.length <= width) return s;
|
|
1851
|
+
return "\u2026" + s.slice(s.length - (width - 1));
|
|
1852
|
+
}
|
|
1853
|
+
function AgentLoopsPanel({ loopFindings, width }) {
|
|
1854
|
+
if (loopFindings.length === 0) return null;
|
|
1855
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
1856
|
+
let totalRepeats = 0;
|
|
1857
|
+
for (const f of loopFindings) {
|
|
1858
|
+
const repeats = Math.max(0, f.count - 1);
|
|
1859
|
+
byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
|
|
1860
|
+
totalRepeats += repeats;
|
|
1861
|
+
}
|
|
1862
|
+
const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
|
|
1863
|
+
const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
|
|
1864
|
+
return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
|
|
1865
|
+
/* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
|
|
1866
|
+
"AGENT LOOPS",
|
|
1867
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
|
|
1868
|
+
] }),
|
|
1869
|
+
toolEntries.map(([tool, repeats]) => {
|
|
1870
|
+
const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
|
|
1871
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1872
|
+
/* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
|
|
1873
|
+
/* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
|
|
1874
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
|
|
1875
|
+
] }, tool);
|
|
1876
|
+
}),
|
|
1877
|
+
topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
1878
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
|
|
1879
|
+
topStuck.map((f, i) => {
|
|
1880
|
+
const target = trimRight(f.commandPreview || f.toolName, 32);
|
|
1881
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1882
|
+
/* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
|
|
1883
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
|
|
1884
|
+
] }, `stuck-${i}`);
|
|
1885
|
+
})
|
|
1886
|
+
] }) : null,
|
|
1887
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1888
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
|
|
1889
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", wrap: "truncate-end", children: "live loop-detector" })
|
|
1890
|
+
] })
|
|
1891
|
+
] });
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/cli/render/ink/panels/ShieldsPanel.tsx
|
|
1895
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1896
|
+
|
|
1897
|
+
// src/protection.ts
|
|
1898
|
+
var PROTECTIVE_SHIELD_DISCOUNTS = {
|
|
1899
|
+
"project-jail": 0.7
|
|
1900
|
+
};
|
|
1674
1901
|
|
|
1675
1902
|
// src/cli/render/ink/panels/ShieldsPanel.tsx
|
|
1676
1903
|
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
@@ -1739,7 +1966,7 @@ function StaticScorecard({ input, rangeLabel, now }) {
|
|
|
1739
1966
|
/* @__PURE__ */ jsx11(Header, { rangeLabel }),
|
|
1740
1967
|
/* @__PURE__ */ jsx11(SeverityBand, { label: "Spend & activity", width }),
|
|
1741
1968
|
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
|
|
1742
|
-
/* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth }),
|
|
1969
|
+
/* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth, keyed: input.keyed }),
|
|
1743
1970
|
/* @__PURE__ */ jsx11(ActivityPanel, { summary, scan: input.scan, width: halfWidth })
|
|
1744
1971
|
] }),
|
|
1745
1972
|
hasCritical ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
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",
|