@chainpatrol/cli 0.1.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/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # ChainPatrol CLI
2
+
3
+ Terminal interface for ChainPatrol threat detection.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g chainpatrol
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ chainpatrol login
15
+ chainpatrol configs list --org my-org
16
+ chainpatrol --json configs list --org my-org
17
+ chainpatrol detections healthcheck --org my-org --run
18
+ chainpatrol detections drift --org my-org --lookback-hours 168
19
+ chainpatrol detections configs update --org my-org --config-id 42 --enable --set query=brand-phishing
20
+ chainpatrol metrics found --org my-org --this-week
21
+ chainpatrol reports create --org my-org --title "Phishing report" --asset "https://phish.example:BLOCKED"
22
+ chainpatrol reports list --org my-org --limit 10
23
+ chainpatrol metrics breakdown --org my-org --by type --from 2026-01-01 --to 2026-01-08
24
+ chainpatrol queues snapshot --all --output markdown
25
+ chainpatrol presets run cs-weekly-health --org my-org --output json
26
+ ```
27
+
28
+ ### Global agent-first flags
29
+
30
+ ```bash
31
+ # preview mutations without sending API write requests
32
+ chainpatrol reports create --org my-org --title "Preview" --asset "https://phish.example:BLOCKED" --dry-run
33
+
34
+ # include recommendation context and emit markdown
35
+ chainpatrol detections drift --org my-org --explain --output markdown
36
+
37
+ # CSV handoff for docs/slides
38
+ chainpatrol queues snapshot --all --output csv
39
+ ```
40
+
41
+ ### Detection commands
42
+
43
+ ```bash
44
+ # run a full healthcheck with source validation
45
+ chainpatrol detections healthcheck --org my-org --run --min-results 1 --lookback-hours 168
46
+
47
+ # validate only
48
+ chainpatrol detections validate --org my-org --source urlscan
49
+
50
+ # run configs on demand
51
+ chainpatrol detections run --org my-org --source google_search
52
+
53
+ # detect noisy/stale/zero-result drift
54
+ chainpatrol detections drift --org my-org --lookback-hours 168
55
+
56
+ # update one config
57
+ chainpatrol detections configs update --org my-org --config-id 42 --disable
58
+ chainpatrol detections configs update --org my-org --config-id 42 --set query=brand-phishing --set maxResults=100
59
+
60
+ # dry-run config update
61
+ chainpatrol detections configs update --org my-org --config-id 42 --set query=brand-phishing --dry-run --output json
62
+ ```
63
+
64
+ ### Metrics commands
65
+
66
+ ```bash
67
+ # summary metrics
68
+ chainpatrol metrics summary --org my-org --from 2026-01-01 --to 2026-01-08
69
+
70
+ # default "found" metric = customer-facing new threats
71
+ chainpatrol metrics found --org my-org --this-week
72
+
73
+ # grouped metrics
74
+ chainpatrol metrics breakdown --org my-org --by day --from 2026-01-01 --to 2026-01-08
75
+ chainpatrol metrics breakdown --org my-org --by type --from 2026-01-01 --to 2026-01-08
76
+ chainpatrol metrics breakdown --org my-org --by brand --from 2026-01-01 --to 2026-01-08
77
+ ```
78
+
79
+ ### Reports commands
80
+
81
+ ```bash
82
+ # create report with inline assets
83
+ chainpatrol reports create --org my-org --title "New phishing campaign" --description "Detected from CLI" --asset "https://phish.example:BLOCKED"
84
+
85
+ # add multiple assets and attachments
86
+ chainpatrol reports create --org my-org --title "Batch report" --asset "https://a.example:BLOCKED" --asset "https://b.example:ALLOWED" --attachment-url "https://cdn.example/screenshot1.png"
87
+
88
+ # use full payload for advanced fields (externalReporter, enrichments, rawAssetsInput)
89
+ chainpatrol reports create --payload-file ./report-payload.json --json
90
+
91
+ # dry-run then apply
92
+ chainpatrol reports create --org my-org --title "Batch report" --asset "https://a.example:BLOCKED" --dry-run --output markdown
93
+ chainpatrol reports create --org my-org --title "Batch report" --asset "https://a.example:BLOCKED"
94
+
95
+ # confirm recently created reports
96
+ chainpatrol reports list --org my-org --limit 5
97
+ chainpatrol reports list --org my-org --status TODO --search "Phishing report"
98
+ ```
99
+
100
+ ### Operations commands
101
+
102
+ ```bash
103
+ # org-level queue snapshot
104
+ chainpatrol queues snapshot --org my-org
105
+
106
+ # all-org queue snapshot
107
+ chainpatrol queues snapshot --all --window-hours 168 --output markdown
108
+ ```
109
+
110
+ ### Presets
111
+
112
+ ```bash
113
+ # list available presets
114
+ chainpatrol presets list
115
+
116
+ # run CS weekly health workflow
117
+ chainpatrol presets run cs-weekly-health --org my-org --output json
118
+ ```
119
+
120
+ ### AI-friendly workflows
121
+
122
+ Use `--json` in automations and agent prompts:
123
+
124
+ ```bash
125
+ chainpatrol --json detections healthcheck --org my-org --run --min-results 1
126
+ chainpatrol --json metrics found --org my-org --this-week
127
+ chainpatrol --json metrics breakdown --org my-org --by type --from 2026-01-01 --to 2026-01-08
128
+ ```
129
+
130
+ Deterministic exit codes:
131
+
132
+ - `0` success
133
+ - `2` auth errors
134
+ - `3` usage/argument errors
135
+ - `4` not found
136
+ - `5` forbidden
137
+ - `6` check failure (health/drift/queue thresholds)
138
+ - `7` mutation partial failure
139
+ - `8` network/API unavailable
140
+ - `1` unknown fallback
141
+
142
+ ## Development
143
+
144
+ ### Build and install locally
145
+
146
+ From the repo root:
147
+
148
+ ```bash
149
+ yarn cli:local:install
150
+ ```
151
+
152
+ And set up the config:
153
+
154
+ ```bash
155
+ mkdir -p ~/.chainpatrol
156
+ echo '{"apiUrl": "https://api.chainpatrol.dev"}' > ~/.chainpatrol/config.json
157
+ ```
158
+
159
+ ### Install the Claude Code skill
160
+
161
+ ```bash
162
+ chainpatrol setup
163
+ ```
164
+
165
+ This copies the `/chainpatrol` skill to `~/.claude/skills/chainpatrol/SKILL.md` so Claude Code can help you use the CLI in any project.
166
+
167
+ ### Testing locally
168
+
169
+ Install the CLI and point it at your local dev server:
170
+
171
+ ```bash
172
+ yarn cli:local:install
173
+ chainpatrol setup
174
+
175
+ mkdir -p ~/.chainpatrol
176
+ echo '{"apiUrl": "http://localhost:3000"}' > ~/.chainpatrol/config.json
177
+ ```
178
+
179
+ XDG path is also supported:
180
+
181
+ ```bash
182
+ mkdir -p ~/.config/chainpatrol
183
+ echo '{"apiUrl": "http://localhost:3000"}' > ~/.config/chainpatrol/config.json
184
+ ```
185
+
186
+ Start the dev server (`yarn dev`), then test:
187
+
188
+ ```bash
189
+ # authenticate
190
+ chainpatrol login
191
+
192
+ # list configs (sets default org)
193
+ chainpatrol configs list --org chainpatrol
194
+
195
+ # uses saved default org
196
+ chainpatrol configs list
197
+
198
+ # JSON output
199
+ chainpatrol configs list --json
200
+
201
+ # detection healthcheck
202
+ chainpatrol detections healthcheck --org chainpatrol --run
203
+
204
+ # metrics checks
205
+ chainpatrol metrics found --org chainpatrol --this-week
206
+ chainpatrol metrics breakdown --org chainpatrol --by type --from 2026-01-01 --to 2026-01-08
207
+ ```
208
+
209
+ ### Uninstall
210
+
211
+ ```bash
212
+ chainpatrol uninstall # remove Claude Code skill
213
+ npm unlink chainpatrol # remove global CLI link
214
+ ```
215
+
216
+ ### Scripts
217
+
218
+ | Script | Description |
219
+ | ---------------- | ----------------------------- |
220
+ | `yarn dev` | Run CLI from source via tsx |
221
+ | `yarn dev:setup` | Run setup command from source |
222
+ | `yarn build` | Build with tsup |
223
+ | `yarn typecheck` | Type check |
224
+ | `yarn test` | Run unit tests |
@@ -0,0 +1,69 @@
1
+ import {
2
+ printOutput,
3
+ toCsvRows
4
+ } from "./chunk-VFT3TD3E.js";
5
+ import {
6
+ createApiClient
7
+ } from "./chunk-H7UKKLCV.js";
8
+ import "./chunk-EEG7T6WT.js";
9
+ import "./chunk-U73SABXK.js";
10
+
11
+ // src/commands/metrics/breakdown.ts
12
+ async function runMetricsBreakdown(options) {
13
+ const client = options.apiClient ?? createApiClient();
14
+ const result = await client.getMetricsBreakdown({
15
+ slug: options.org,
16
+ by: options.by,
17
+ startDate: options.from,
18
+ endDate: options.to,
19
+ brandIds: options.brandIds
20
+ });
21
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
22
+ printOutput({
23
+ outputFormat,
24
+ json: result,
25
+ markdown: [
26
+ `# Metrics Breakdown (${options.org})`,
27
+ "",
28
+ `- Grouped by: ${result.by}`,
29
+ "",
30
+ ...result.points.map((point) => {
31
+ if (result.by === "day") {
32
+ return `- ${point.date ?? point.key}: ${point.count}`;
33
+ }
34
+ if (result.by === "type") {
35
+ return `- ${point.type ?? point.key}: ${point.count}`;
36
+ }
37
+ return `- ${point.brandName ?? point.brandSlug ?? point.key}: ${point.count}`;
38
+ })
39
+ ].join("\n"),
40
+ csv: toCsvRows(
41
+ result.points.map((point) => ({
42
+ key: point.key,
43
+ count: point.count,
44
+ date: point.date ?? null,
45
+ type: point.type ?? null,
46
+ brandId: point.brandId ?? null,
47
+ brandSlug: point.brandSlug ?? null,
48
+ brandName: point.brandName ?? null
49
+ }))
50
+ ),
51
+ human: () => {
52
+ console.log(`Metrics breakdown for ${options.org} by ${result.by}`);
53
+ for (const point of result.points) {
54
+ if (result.by === "day") {
55
+ console.log(`${point.date ?? point.key}: ${point.count}`);
56
+ continue;
57
+ }
58
+ if (result.by === "type") {
59
+ console.log(`${point.type ?? point.key}: ${point.count}`);
60
+ continue;
61
+ }
62
+ console.log(`${point.brandName ?? point.brandSlug ?? point.key}: ${point.count}`);
63
+ }
64
+ }
65
+ });
66
+ }
67
+ export {
68
+ runMetricsBreakdown
69
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ CliExitError,
3
+ ExitCode
4
+ } from "./chunk-E2LAMILJ.js";
5
+ import {
6
+ printOutput,
7
+ toCsvRows
8
+ } from "./chunk-VFT3TD3E.js";
9
+ import {
10
+ DateTime,
11
+ createApiClient
12
+ } from "./chunk-H7UKKLCV.js";
13
+
14
+ // src/presets/index.ts
15
+ var PRESETS = [
16
+ {
17
+ id: "cs-weekly-health",
18
+ title: "CS Weekly Health",
19
+ description: "Weekly customer health package with found threats, summary metrics, and detection validation."
20
+ }
21
+ ];
22
+ function getPresetDefinition(id) {
23
+ return PRESETS.find((preset) => preset.id === id);
24
+ }
25
+ async function runPreset({
26
+ presetId,
27
+ org,
28
+ outputFormat,
29
+ explain,
30
+ apiClient
31
+ }) {
32
+ const client = apiClient ?? createApiClient();
33
+ if (presetId !== "cs-weekly-health") {
34
+ throw new CliExitError(`Unknown preset '${presetId}'.`, ExitCode.USAGE);
35
+ }
36
+ const now = DateTime.now().toUTC();
37
+ const startDate = now.startOf("week").toISO();
38
+ const endDate = now.toISO();
39
+ if (!startDate || !endDate) {
40
+ throw new CliExitError("Failed to resolve preset date range.", ExitCode.UNKNOWN);
41
+ }
42
+ const [summary, found, validation] = await Promise.all([
43
+ client.getMetricsSummary({
44
+ slug: org,
45
+ startDate,
46
+ endDate
47
+ }),
48
+ client.getMetricsFound({
49
+ slug: org,
50
+ startDate,
51
+ endDate
52
+ }),
53
+ client.validateDetectionConfigs({
54
+ slug: org,
55
+ lookbackHours: 168,
56
+ minResults: 1,
57
+ runBeforeValidate: false,
58
+ includeDisabled: false
59
+ })
60
+ ]);
61
+ const payload = {
62
+ presetId,
63
+ org,
64
+ range: { startDate, endDate },
65
+ summary: summary.metrics,
66
+ found,
67
+ detectionValidation: validation.summary,
68
+ failedDetectionConfigs: validation.validations.filter((item) => !item.valid)
69
+ };
70
+ printOutput({
71
+ outputFormat,
72
+ json: {
73
+ ...payload,
74
+ explanation: explain ? "Combines customer-facing weekly threat metrics with detector healthcheck coverage." : void 0
75
+ },
76
+ markdown: [
77
+ `# Preset: CS Weekly Health (${org})`,
78
+ "",
79
+ `- Range: ${startDate} -> ${endDate}`,
80
+ `- Reports: ${summary.metrics.reports}`,
81
+ `- New threats: ${summary.metrics.newThreats}`,
82
+ `- Found metric: ${found.found}`,
83
+ `- Detection configs checked: ${validation.summary.checkedConfigs}`,
84
+ `- Detection configs failing: ${validation.summary.failingConfigs}`
85
+ ].join("\n"),
86
+ csv: toCsvRows([
87
+ {
88
+ org,
89
+ startDate,
90
+ endDate,
91
+ reports: summary.metrics.reports,
92
+ newThreats: summary.metrics.newThreats,
93
+ found: found.found,
94
+ checkedConfigs: validation.summary.checkedConfigs,
95
+ failingConfigs: validation.summary.failingConfigs
96
+ }
97
+ ]),
98
+ human: () => {
99
+ console.log(`Preset "${presetId}" for ${org}`);
100
+ console.log(`Range: ${startDate} -> ${endDate}`);
101
+ console.log(
102
+ `Metrics: reports=${summary.metrics.reports} newThreats=${summary.metrics.newThreats} watchlisted=${summary.metrics.threatsWatchlisted}`
103
+ );
104
+ console.log(
105
+ `Found threats this week: ${found.found} | Detector health failures: ${validation.summary.failingConfigs}`
106
+ );
107
+ if (explain) {
108
+ console.log(
109
+ "This preset highlights weekly CS health outcomes plus detector reliability checks."
110
+ );
111
+ }
112
+ }
113
+ });
114
+ if (!validation.ok) {
115
+ throw new CliExitError(
116
+ "Preset completed with detector health failures.",
117
+ ExitCode.CHECK_FAILED
118
+ );
119
+ }
120
+ }
121
+
122
+ export {
123
+ PRESETS,
124
+ getPresetDefinition,
125
+ runPreset
126
+ };
@@ -0,0 +1,48 @@
1
+ // src/lib/exit-codes.ts
2
+ var ExitCode = {
3
+ SUCCESS: 0,
4
+ UNKNOWN: 1,
5
+ AUTH: 2,
6
+ USAGE: 3,
7
+ NOT_FOUND: 4,
8
+ FORBIDDEN: 5,
9
+ CHECK_FAILED: 6,
10
+ MUTATION_PARTIAL_FAILURE: 7,
11
+ NETWORK_UNAVAILABLE: 8
12
+ };
13
+ var CliExitError = class extends Error {
14
+ exitCode;
15
+ constructor(message, exitCode) {
16
+ super(message);
17
+ this.name = "CliExitError";
18
+ this.exitCode = exitCode;
19
+ }
20
+ };
21
+ function mapErrorToExitCode(error) {
22
+ if (error instanceof CliExitError) {
23
+ return error.exitCode;
24
+ }
25
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
26
+ if (message.includes("authentication failed") || message.includes("run `chainpatrol login`")) {
27
+ return ExitCode.AUTH;
28
+ }
29
+ if (message.includes("permission") || message.includes("forbidden")) {
30
+ return ExitCode.FORBIDDEN;
31
+ }
32
+ if (message.includes("not found")) {
33
+ return ExitCode.NOT_FOUND;
34
+ }
35
+ if (message.includes("cannot connect") || message.includes("network error") || message.includes("timed out") || message.includes("temporarily unavailable")) {
36
+ return ExitCode.NETWORK_UNAVAILABLE;
37
+ }
38
+ if (message.includes("usage") || message.includes("requires --") || message.includes("unknown command")) {
39
+ return ExitCode.USAGE;
40
+ }
41
+ return ExitCode.UNKNOWN;
42
+ }
43
+
44
+ export {
45
+ ExitCode,
46
+ CliExitError,
47
+ mapErrorToExitCode
48
+ };