@chainpatrol/cli 0.3.4 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b5de201: Add a `healthchecks` namespace to the public API and CLI.
8
+
9
+ The new `healthchecks list` returns a registry of every check the platform
10
+ exposes today, including planned checks that are not yet implemented on the
11
+ backend (marked `implemented: false` with a `notImplementedReason`). The new
12
+ `healthchecks run <id|--all> --org <slug>` runs a single check or every
13
+ implemented check in parallel and returns each result in a uniform shape
14
+ (`id`, `severity`, `observed`, `threshold`, `findings`, `suggestedAction`).
15
+
16
+ Implemented checks in this release:
17
+
18
+ - `detections.silent-configs` — the existing detection-config healthcheck,
19
+ wrapped in the uniform shape. The original `detections healthcheck`
20
+ command continues to work.
21
+ - `reviewing.backlog` — counts pending review proposals and grades severity
22
+ against per-org thresholds (default warn=50, fail=100).
23
+ - `reviewing.old-proposals` — counts proposals older than the warn / fail
24
+ age thresholds (default 7 / 14 days) and lists the oldest offenders.
25
+ - `takedowns.stale-in-progress` — counts takedowns sitting in IN_PROGRESS
26
+ past the staleness threshold (default 7 days).
27
+
28
+ Planned but not yet implemented (still returned by `list` with
29
+ `implemented: false`): `detections.coverage-gaps`, `detections.spike`,
30
+ `detections.drop`, `reviewing.auto-approval-spike`,
31
+ `blocklisting.gsb-cancelled-rate`, `takedowns.todo-volume`,
32
+ `takedowns.cancelled-rate`, `takedowns.automation-off`.
33
+
34
+ The bundled Claude Code skill is updated to (a) document the new namespace,
35
+ (b) recommend `healthchecks run --all` as the canonical entrypoint when a
36
+ user asks to run a healthcheck on an org, and (c) mark every not-yet-
37
+ implemented check in the HealthCheck Guide with a "manual check, no API
38
+ yet" note so the agent surfaces that gap explicitly.
39
+
3
40
  ## 0.3.4
4
41
 
5
42
  ### Patch Changes
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -141,6 +141,12 @@ function createApiClient(options) {
141
141
  searchQuery: input.searchQuery,
142
142
  reportedByCustomer: input.reportedByCustomer
143
143
  });
144
+ },
145
+ listHealthchecks() {
146
+ return request("/healthchecks/list", {});
147
+ },
148
+ runHealthcheck(endpoint, input) {
149
+ return request(endpoint, input);
144
150
  }
145
151
  };
146
152
  }
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -297,6 +297,71 @@ Use it as the first signal in the Detection part of an org healthcheck, then
297
297
  fall back to the manual checks in the HealthCheck Guide below for everything
298
298
  else.
299
299
 
300
+ > Prefer the newer \`healthchecks\` namespace below. \`detections healthcheck\`
301
+ > is the original single-purpose command; the \`healthchecks\` namespace is
302
+ > the canonical place to discover and run every check we expose.
303
+
304
+ ### \`healthchecks list | run\` \u2014 Run uniform org healthchecks via the public API
305
+
306
+ The \`healthchecks\` namespace is the canonical way to run the named checks
307
+ from the Organization HealthCheck Guide below. Each implemented endpoint
308
+ returns the same uniform shape \u2014 \`{ id, ok, severity, observed, threshold,
309
+ findings, suggestedAction }\` \u2014 so the CLI / agent can render every check the
310
+ same way regardless of category.
311
+
312
+ \`\`\`bash
313
+ # Discover every check the platform exposes today, including planned checks
314
+ # that are not yet implemented on the backend.
315
+ chainpatrol --json healthchecks list
316
+
317
+ # Run a single named check.
318
+ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
319
+
320
+ # Run every implemented check in parallel and aggregate the results.
321
+ chainpatrol --json healthchecks run --all --org <slug>
322
+ \`\`\`
323
+
324
+ Each implemented check has a stable id of the form \`category.name\`, e.g.
325
+ \`detections.silent-configs\`, \`reviewing.backlog\`,
326
+ \`reviewing.old-proposals\`, \`takedowns.stale-in-progress\`.
327
+
328
+ Implemented checks today:
329
+
330
+ - **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
331
+ exposed under the uniform shape.
332
+ - **reviewing.backlog** \u2014 counts proposals in PENDING review state and grades
333
+ severity against per-org thresholds (default warn=50, fail=100).
334
+ - **reviewing.old-proposals** \u2014 counts proposals older than the warn / fail
335
+ age thresholds (default 7 / 14 days) and lists the oldest offenders.
336
+ - **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
337
+ past the staleness threshold (default 7 days) and lists the oldest.
338
+
339
+ The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
340
+ but **not yet implemented on the backend** \u2014 when the agent surfaces them in
341
+ a healthcheck report, mark them explicitly as "manual check, no API yet":
342
+
343
+ - **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
344
+ Still requires manual reasoning with \`configs list\` + \`reports list\`.
345
+ - **detections.spike** / **detections.drop** \u2014 require server-side baseline
346
+ modeling. Use \`metrics breakdown --by day\` as an interim signal.
347
+ - **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
348
+ human approvers in the review history. Use \`metrics breakdown\` as a proxy.
349
+ - **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
350
+ is not yet exposed in the public API.
351
+ - **takedowns.todo-volume** / **takedowns.cancelled-rate** /
352
+ **takedowns.automation-off** \u2014 the underlying read paths are not yet exposed.
353
+
354
+ When the user asks to "run a healthcheck on org X", the canonical command is:
355
+
356
+ \`\`\`bash
357
+ chainpatrol --json healthchecks run --all --org X
358
+ \`\`\`
359
+
360
+ This iterates the implemented entries in \`healthchecks list\`, runs them in
361
+ parallel, and aggregates the uniform results. Combine with the manual checks
362
+ in the HealthCheck Guide below for everything still marked
363
+ \`implemented: false\`.
364
+
300
365
  ### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
301
366
 
302
367
  Server-side aggregation of the operations review queue (pending proposals,
@@ -442,16 +507,26 @@ others are soft / qualitative signals that still need you to fetch data with
442
507
 
443
508
  ### Quick Path: CLI commands that automate parts of this guide
444
509
 
445
- Run these first to get cheap, structured signals before falling back to the
446
- manual checks in each subsection:
510
+ The canonical first step is now the \`healthchecks\` namespace, which runs
511
+ every implemented check via the public API and returns a uniform shape per
512
+ check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
513
+ \`suggestedAction\`):
447
514
 
448
515
  \`\`\`bash
449
- # Detection: configs that have gone silent or are erroring
450
- chainpatrol --json detections healthcheck --org <slug>
516
+ # Discover every check the platform exposes, implemented or planned.
517
+ chainpatrol --json healthchecks list
451
518
 
452
- # Reviewing & Takedowns: backlog, SLA breaches, stuck takedowns
453
- chainpatrol --json queues snapshot --org <slug>
519
+ # Run every implemented healthcheck in parallel and aggregate the results.
520
+ chainpatrol --json healthchecks run --all --org <slug>
521
+
522
+ # Run a single named check.
523
+ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
524
+ \`\`\`
454
525
 
526
+ After \`healthchecks run --all\`, use these complementary commands to cover
527
+ the signals that are not yet exposed as a uniform healthcheck endpoint:
528
+
529
+ \`\`\`bash
455
530
  # Spikes / drops in detection volume over time (compare windows)
456
531
  chainpatrol --json metrics breakdown --org <slug> --by day --this-week
457
532
 
@@ -461,6 +536,9 @@ chainpatrol --json reports list --org <slug> --reported-by-customer
461
536
  # What's enabled vs disabled vs not configured for the org
462
537
  chainpatrol --json configs list --org <slug>
463
538
 
539
+ # Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
540
+ chainpatrol --json queues snapshot --org <slug>
541
+
464
542
  # Packaged weekly customer-success sweep (preferred when it covers the ask)
465
543
  chainpatrol presets run cs-weekly-health --org <slug>
466
544
  \`\`\`
@@ -469,7 +547,8 @@ Treat each command's output as one input to the healthcheck. The manual
469
547
  checks below still apply \u2014 especially for signals the CLI cannot infer on
470
548
  its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
471
549
  is disabled", or "this drop is fine because the config isn't relevant
472
- to this org").
550
+ to this org"). Each subsection of the guide notes whether a healthcheck
551
+ endpoint exists today and what to fall back on when it doesn't.
473
552
 
474
553
  ### Reporting progress while running a healthcheck
475
554
 
@@ -524,13 +603,15 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
524
603
  detection sources like "Twitter / X User Search" or "Twitter Post Search" are
525
604
  disabled. Those should be turned on.
526
605
 
527
- **Run via CLI:** there is no single command for this \u2014 it's a manual
528
- correlation. Fetch the org's enabled vs disabled configs with
529
- \`chainpatrol --json configs list --org <slug>\`, then look at recent reports
530
- (\`chainpatrol --json reports list --org <slug>\`) and the asset types of any
531
- blocked items. Flag any asset type where blocked items exist but the matching
532
- detection source has \`status: "disabled"\` (or appears in the "Not configured"
533
- group).
606
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
607
+ \`healthchecks list\` as \`detections.coverage-gaps\` with
608
+ \`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
609
+ "manual check, no API yet". Until the endpoint lands, do the correlation
610
+ manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
611
+ disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
612
+ recent blocked-item asset types. Flag any asset type where blocked items
613
+ exist but the matching detection source has \`status: "disabled"\` (or
614
+ appears in the "Not configured" group).
534
615
 
535
616
  #### Spike in Detections
536
617
 
@@ -538,7 +619,9 @@ A spike in recent detections is worth investigating. It could be a bad config
538
619
  change, or it could be a legitimate new attack push in this area \u2014 useful
539
620
  intel to surface to the security team as a targeted spike.
540
621
 
541
- **Run via CLI:** \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
622
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
623
+ in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
624
+ Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
542
625
  (and a comparison window via \`--from\`/\`--to\`) to see daily detection
543
626
  volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
544
627
  against recent config changes for that source.
@@ -549,11 +632,15 @@ A drop is also worth looking into. It may be a bad config change, or it may
549
632
  mean the config is not really relevant and can be safely turned off (not all
550
633
  default-on configs are relevant to every org).
551
634
 
552
- **Run via CLI:** the same \`metrics breakdown\` time series catches drops.
553
- Additionally, \`chainpatrol --json detections healthcheck --org <slug>\`
554
- fails configs whose \`recentResultCount\` is below \`--min-results\` in the
555
- \`--lookback-hours\` window \u2014 that's exactly a "this source went silent"
556
- signal. Use \`--run\` to also catch configs that error when executed.
635
+ **Run via CLI:** the extreme case ("this source went silent") is covered
636
+ today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
637
+ which is the canonical replacement for the older \`detections healthcheck\`.
638
+ It fails any config whose \`recentResultCount\` is below \`--min-results\`
639
+ in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
640
+ \`detections healthcheck --run\`) to also catch configs that error when
641
+ executed. For soft drops (still producing results but below baseline),
642
+ \`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
643
+ \u2014 use \`metrics breakdown --by day\` and compare windows manually.
557
644
 
558
645
  ### Reviewing
559
646
 
@@ -564,12 +651,16 @@ reports, but really the threshold is relative to the average number of
564
651
  confirmed threats per week. Example: if an org only adds 5 blocked threats per
565
652
  week, then a 7-day backlog of even 10 proposals is a really big deal.
566
653
 
567
- **Run via CLI:** \`chainpatrol --json queues snapshot --org <slug>\` returns
568
- \`reviewQueue.totalPendingProposals\` and \`reviewQueue.distinctReports\`.
569
- Compare against the org's typical weekly throughput (use
570
- \`metrics summary --this-week\` for that baseline) \u2014 a backlog that exceeds
571
- a week of typical confirmed-threat volume is a finding regardless of the
572
- absolute number.
654
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
655
+ The endpoint counts proposals in PENDING review state and grades severity
656
+ against per-org thresholds (default warn=50, fail=100; override with
657
+ \`--warn-threshold\` / \`--fail-threshold\` via the run payload). For raw
658
+ counts plus SLA / age breakdowns, \`chainpatrol --json queues snapshot --org <slug>\`
659
+ remains useful and exposes \`reviewQueue.totalPendingProposals\` and
660
+ \`reviewQueue.distinctReports\`. Compare against the org's typical weekly
661
+ throughput (use \`metrics summary --this-week\` for that baseline) \u2014 a
662
+ backlog that exceeds a week of typical confirmed-threat volume is a finding
663
+ regardless of the absolute number.
573
664
 
574
665
  #### Really Old Proposals
575
666
 
@@ -578,11 +669,12 @@ gone wrong. Even complex investigations rarely take longer than this. Except
578
669
  for rare cases, these should be rejected or approved to prevent a backlog
579
670
  from building.
580
671
 
581
- **Run via CLI:** \`queues snapshot\` returns \`reviewQueue.ageBuckets.gte168h\`
582
- (proposals older than 7 days). Anything in that bucket is at least halfway
583
- to the 14-day threshold; investigate. \`reviewQueue.slaBuckets.breached\`
584
- captures the strictest SLA breaches separately \u2014 any non-zero value is
585
- worth raising.
672
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
673
+ The endpoint counts pending proposals older than the warn / fail age
674
+ thresholds (default 7 / 14 days) and lists the oldest offenders in
675
+ \`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
676
+ works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
677
+ strictest SLA breaches separately \u2014 any non-zero value is worth raising.
586
678
 
587
679
  #### Spike in Auto Approved Reports
588
680
 
@@ -594,8 +686,10 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
594
686
  adjusted recently, since those may be the cause of spam combined with a weak
595
687
  rule.
596
688
 
597
- **Run via CLI:** there is no dedicated subcommand for the auto-approval
598
- rate yet. As a proxy, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
689
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
690
+ in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
691
+ \`implemented: false\`. As a proxy until the endpoint lands, use
692
+ \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
599
693
  and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
600
694
  isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
601
695
  points at automation doing the approving.
@@ -613,9 +707,10 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
613
707
  are too many false positives landing on the blocklist, indicating issues with
614
708
  detection and reviewing rules.
615
709
 
616
- **Run via CLI:** not yet \u2014 the CLI does not expose Google Safe Browsing
617
- submission state today. Until the new public API lands, this remains a
618
- manual / engineering-team check.
710
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
711
+ in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
712
+ \`implemented: false\`. Until Google Safe Browsing submission state is
713
+ exposed in the public API, this remains a manual / engineering-team check.
619
714
 
620
715
  ### Takedowns
621
716
 
@@ -625,8 +720,11 @@ Can mean a gap in automated takedowns not being implemented for some new area
625
720
  of threats. It can also mean the areas that require manual takedowns are
626
721
  being missed by the takedown team.
627
722
 
628
- **Run via CLI:** \`chainpatrol --json queues snapshot --org <slug>\` returns
629
- \`takedownQueue.totalOpen\` and breakdowns by status. A persistently large
723
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
724
+ in \`healthchecks list\` as \`takedowns.todo-volume\` with
725
+ \`implemented: false\` (needs a per-org throughput baseline). As an interim
726
+ signal, \`chainpatrol --json queues snapshot --org <slug>\` returns
727
+ \`takedownQueue.totalOpen\` and breakdowns by status; a persistently large
630
728
  ToDo bucket relative to the org's typical takedown rate (use
631
729
  \`metrics summary\` for that baseline) is the finding.
632
730
 
@@ -636,10 +734,12 @@ Typically means something is wrong with the submission itself. The takedown
636
734
  may need to be resubmitted, the vendor asked for more evidence, or we may
637
735
  have submitted it in the wrong place.
638
736
 
639
- **Run via CLI:** \`queues snapshot\` returns \`takedownQueue.staleInProgress\`
640
- \u2014 takedowns that have sat in In Progress past the expected vendor turnaround.
641
- Any non-zero value is worth investigating; a growing number across snapshots
642
- strongly suggests a vendor-side or submission-format problem.
737
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.stale-in-progress\`.**
738
+ The endpoint counts takedowns sitting in IN_PROGRESS past the staleness
739
+ threshold (default 7 days) and lists the oldest offenders in \`findings\`.
740
+ \`queues snapshot\` (\`takedownQueue.staleInProgress\`) still works as a raw
741
+ view. Any non-zero value is worth investigating; a growing number across
742
+ snapshots strongly suggests a vendor-side or submission-format problem.
643
743
 
644
744
  #### Too Many Cancelled Takedowns
645
745
 
@@ -648,7 +748,10 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
648
748
  when it's already taken down are treated as completed, not cancelled. So even
649
749
  3 cancelled takedowns in a 7-day period is too many.
650
750
 
651
- **Run via CLI:** no first-class command yet. As an interim signal, use
751
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
752
+ in \`healthchecks list\` as \`takedowns.cancelled-rate\` with
753
+ \`implemented: false\` (public API does not yet expose CANCELLED takedown
754
+ counts over a rolling window). As an interim signal, use
652
755
  \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
653
756
  and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
654
757
  divergence \u2014 a widening gap with no In-Progress growth often shows up as
@@ -660,9 +763,12 @@ Automated takedowns should be on by default for nearly every organization.
660
763
  Any issue that would make you want to turn off automated takedowns should be
661
764
  resolved within 30 days.
662
765
 
663
- **Run via CLI:** not yet exposed in a single command. Check this manually
664
- with the org's takedown automation settings; the CLI's role here is mostly
665
- to highlight stale state in \`queues snapshot\` so you know to ask.
766
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
767
+ in \`healthchecks list\` as \`takedowns.automation-off\` with
768
+ \`implemented: false\` (public API does not yet expose automated-takedown
769
+ enablement state). Check this manually with the org's takedown automation
770
+ settings; the CLI's role here is mostly to highlight stale state in
771
+ \`queues snapshot\` so you know to ask.
666
772
  `;
667
773
  }
668
774
  function getBundledSkillVersion() {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-DOL35U2S.js";
16
+ } from "./chunk-S65NM7FF.js";
17
17
  import "./chunk-IUZB3DQW.js";
18
18
  import {
19
19
  DateTime
@@ -281,6 +281,35 @@ var HELP = {
281
281
  "--window-hours <n> Hours used for staleness windows"
282
282
  ]
283
283
  },
284
+ healthchecks: {
285
+ description: "Run organization healthchecks via the public API. Use `list` to see every available check (including planned ones not yet implemented on the backend) and `run` to execute one or all implemented checks.",
286
+ usage: "chainpatrol healthchecks <list|run <id>|run --all>",
287
+ options: ["--org <slug> Organization slug (required for `run`)"],
288
+ examples: [
289
+ "chainpatrol healthchecks list",
290
+ "chainpatrol healthchecks run --all --org acme",
291
+ "chainpatrol healthchecks run reviewing.backlog --org acme",
292
+ "chainpatrol healthchecks run takedowns.stale-in-progress --org acme"
293
+ ]
294
+ },
295
+ "healthchecks list": {
296
+ description: "List every healthcheck the platform exposes. Each entry reports whether it is implemented today; not-yet-implemented entries include a `notImplementedReason` you can surface to users.",
297
+ usage: "chainpatrol healthchecks list"
298
+ },
299
+ "healthchecks run": {
300
+ description: "Run a single healthcheck by id, or pass `--all` to run every implemented check in parallel and aggregate the results. Returns the uniform healthcheck shape (id, severity, observed, threshold, findings, suggestedAction).",
301
+ usage: "chainpatrol healthchecks run <id|--all> --org <slug>",
302
+ options: [
303
+ "--org <slug> Organization slug (required)",
304
+ "--all Run every implemented healthcheck in parallel",
305
+ "--min-results <n> Pass through to detections.silent-configs",
306
+ "--lookback-hours <n> Pass through to detections.silent-configs"
307
+ ],
308
+ examples: [
309
+ "chainpatrol healthchecks run reviewing.backlog --org acme",
310
+ "chainpatrol healthchecks run --all --org acme"
311
+ ]
312
+ },
284
313
  presets: {
285
314
  description: "Run saved workflows for common jobs.",
286
315
  usage: "chainpatrol presets <list|run <id>>",
@@ -336,6 +365,7 @@ function getTopLevelHelp() {
336
365
  " logout Clear stored credentials",
337
366
  " configs Manage detection configs",
338
367
  " detections Validate, run, update detection configs",
368
+ " healthchecks List and run organization healthchecks",
339
369
  " metrics Query organization metrics and breakdowns",
340
370
  " reports Create and list reports from terminal",
341
371
  " queues Snapshot operations review/takedown queues",
@@ -504,6 +534,7 @@ var COMMANDS = [
504
534
  "logout",
505
535
  "configs",
506
536
  "detections",
537
+ "healthchecks",
507
538
  "metrics",
508
539
  "reports",
509
540
  "queues",
@@ -686,12 +717,12 @@ function parseAttachmentUrls() {
686
717
  }
687
718
  async function handleConfigsList(org) {
688
719
  if (jsonMode) {
689
- const { listConfigsJson } = await import("./list-json-SUZL2GBJ.js");
720
+ const { listConfigsJson } = await import("./list-json-WTMYLZGY.js");
690
721
  await listConfigsJson({ org });
691
722
  return;
692
723
  }
693
724
  const { render } = await import("ink");
694
- const { default: ConfigsList } = await import("./list-TMFLCS5U.js");
725
+ const { default: ConfigsList } = await import("./list-GEMCFDD5.js");
695
726
  const { default: React } = await import("react");
696
727
  render(React.createElement(ConfigsList, { org }));
697
728
  }
@@ -789,7 +820,7 @@ async function main() {
789
820
  case "detections": {
790
821
  const org = await resolveOrg();
791
822
  if (subcommand === "healthcheck") {
792
- const { runDetectionsHealthcheck } = await import("./healthcheck-SIE5EXN3.js");
823
+ const { runDetectionsHealthcheck } = await import("./healthcheck-KAONRGSS.js");
793
824
  await runDetectionsHealthcheck({
794
825
  org,
795
826
  source: cli.flags.source,
@@ -804,7 +835,7 @@ async function main() {
804
835
  break;
805
836
  }
806
837
  if (subcommand === "validate") {
807
- const { runDetectionsValidate } = await import("./validate-II6GH6H6.js");
838
+ const { runDetectionsValidate } = await import("./validate-BJFEKI2N.js");
808
839
  await runDetectionsValidate({
809
840
  org,
810
841
  source: cli.flags.source,
@@ -819,7 +850,7 @@ async function main() {
819
850
  break;
820
851
  }
821
852
  if (subcommand === "drift") {
822
- const { runDetectionsDrift } = await import("./drift-PASGDEEX.js");
853
+ const { runDetectionsDrift } = await import("./drift-DZ6A7JL5.js");
823
854
  await runDetectionsDrift({
824
855
  org,
825
856
  source: cli.flags.source,
@@ -833,7 +864,7 @@ async function main() {
833
864
  break;
834
865
  }
835
866
  if (subcommand === "run") {
836
- const { runDetectionsRun } = await import("./run-MGOASUON.js");
867
+ const { runDetectionsRun } = await import("./run-43CC5AXR.js");
837
868
  await runDetectionsRun({
838
869
  org,
839
870
  configId: cli.flags.configId,
@@ -852,7 +883,7 @@ async function main() {
852
883
  break;
853
884
  }
854
885
  if (action === "run") {
855
- const { runDetectionsRun } = await import("./run-MGOASUON.js");
886
+ const { runDetectionsRun } = await import("./run-43CC5AXR.js");
856
887
  await runDetectionsRun({
857
888
  org,
858
889
  configId: cli.flags.configId,
@@ -870,7 +901,7 @@ async function main() {
870
901
  throw new Error("detections configs update requires --config-id");
871
902
  }
872
903
  const configPatch = getConfigPatchFromSetFlags();
873
- const { runDetectionsConfigsUpdate } = await import("./configs-update-VOWH674W.js");
904
+ const { runDetectionsConfigsUpdate } = await import("./configs-update-RPN32YTL.js");
874
905
  await runDetectionsConfigsUpdate({
875
906
  org,
876
907
  configId: cli.flags.configId,
@@ -897,7 +928,7 @@ async function main() {
897
928
  case "metrics": {
898
929
  const org = await resolveOrg();
899
930
  if (subcommand === "summary") {
900
- const { runMetricsSummary } = await import("./summary-QORQFCMW.js");
931
+ const { runMetricsSummary } = await import("./summary-6NCA7PDP.js");
901
932
  await runMetricsSummary({
902
933
  org,
903
934
  from: cli.flags.from,
@@ -909,7 +940,7 @@ async function main() {
909
940
  break;
910
941
  }
911
942
  if (subcommand === "found") {
912
- const { runMetricsFound } = await import("./found-TIW7T4VX.js");
943
+ const { runMetricsFound } = await import("./found-AOPBSLRD.js");
913
944
  await runMetricsFound({
914
945
  org,
915
946
  from: cli.flags.from,
@@ -926,7 +957,7 @@ async function main() {
926
957
  if (!by || !["day", "type", "brand"].includes(by)) {
927
958
  throw new Error("metrics breakdown requires --by <day|type|brand>");
928
959
  }
929
- const { runMetricsBreakdown } = await import("./breakdown-FFNYSXRQ.js");
960
+ const { runMetricsBreakdown } = await import("./breakdown-EBSACUST.js");
930
961
  await runMetricsBreakdown({
931
962
  org,
932
963
  by,
@@ -946,7 +977,7 @@ async function main() {
946
977
  case "reports": {
947
978
  if (subcommand === "list") {
948
979
  const org = await resolveOrg();
949
- const { runReportsList } = await import("./list-CEWBMKJ3.js");
980
+ const { runReportsList } = await import("./list-MWDFCHMJ.js");
950
981
  await runReportsList({
951
982
  org,
952
983
  limit: cli.flags.limit,
@@ -962,7 +993,7 @@ async function main() {
962
993
  }
963
994
  if (subcommand === "create") {
964
995
  const org = await tryResolveOrg();
965
- const { runReportsCreate } = await import("./create-ASGUBKZ7.js");
996
+ const { runReportsCreate } = await import("./create-QP3M7EZM.js");
966
997
  await runReportsCreate({
967
998
  org,
968
999
  title: cli.flags.title,
@@ -986,7 +1017,7 @@ async function main() {
986
1017
  }
987
1018
  case "queues": {
988
1019
  if (subcommand === "snapshot") {
989
- const { runQueuesSnapshot } = await import("./snapshot-MQ6DWDFG.js");
1020
+ const { runQueuesSnapshot } = await import("./snapshot-E3TPZOKT.js");
990
1021
  await runQueuesSnapshot({
991
1022
  org: cli.flags.org,
992
1023
  all: cli.flags.all,
@@ -1002,9 +1033,41 @@ async function main() {
1002
1033
  subcommand ? `Unknown subcommand: queues ${subcommand}${hint ? `. Did you mean "queues ${hint}"?` : ""}` : "Usage: chainpatrol queues snapshot [--org <slug>|--all]"
1003
1034
  );
1004
1035
  }
1036
+ case "healthchecks": {
1037
+ if (subcommand === "list") {
1038
+ const { runHealthchecksList } = await import("./list-UW63DIKX.js");
1039
+ await runHealthchecksList({
1040
+ json: jsonMode,
1041
+ outputFormat: cliContext.outputFormat
1042
+ });
1043
+ break;
1044
+ }
1045
+ if (subcommand === "run") {
1046
+ const org = await resolveOrg();
1047
+ const thresholds = {};
1048
+ if (cli.flags.minResults !== void 0)
1049
+ thresholds.minResults = cli.flags.minResults;
1050
+ if (cli.flags.lookbackHours !== void 0)
1051
+ thresholds.lookbackHours = cli.flags.lookbackHours;
1052
+ const { runHealthchecksRun } = await import("./run-7THXM7GF.js");
1053
+ await runHealthchecksRun({
1054
+ org,
1055
+ id: action,
1056
+ all: cli.flags.all,
1057
+ thresholds,
1058
+ json: jsonMode,
1059
+ outputFormat: cliContext.outputFormat
1060
+ });
1061
+ break;
1062
+ }
1063
+ const hint = subcommand ? suggest(subcommand, ["list", "run"]) : null;
1064
+ throw new Error(
1065
+ subcommand ? `Unknown subcommand: healthchecks ${subcommand}${hint ? `. Did you mean "healthchecks ${hint}"?` : ""}` : "Usage: chainpatrol healthchecks <list|run <id>>"
1066
+ );
1067
+ }
1005
1068
  case "presets": {
1006
1069
  if (subcommand === "list") {
1007
- const { runPresetsList } = await import("./list-KTIZ3UHA.js");
1070
+ const { runPresetsList } = await import("./list-LN6NOZIJ.js");
1008
1071
  await runPresetsList({ outputFormat: cliContext.outputFormat });
1009
1072
  break;
1010
1073
  }
@@ -1015,7 +1078,7 @@ async function main() {
1015
1078
  );
1016
1079
  }
1017
1080
  const org = await resolveOrg();
1018
- const { runPresetsRun } = await import("./run-3TTLZ6HA.js");
1081
+ const { runPresetsRun } = await import("./run-MH5RYPWA.js");
1019
1082
  await runPresetsRun({
1020
1083
  presetId: action,
1021
1084
  org,
@@ -1032,12 +1095,12 @@ async function main() {
1032
1095
  case "setup":
1033
1096
  case "install":
1034
1097
  case "i": {
1035
- const { setupSkill } = await import("./setup-skill-PCUBJJYU.js");
1098
+ const { setupSkill } = await import("./setup-skill-Z5RVCWCU.js");
1036
1099
  setupSkill({ json: jsonMode });
1037
1100
  break;
1038
1101
  }
1039
1102
  case "uninstall": {
1040
- const { uninstallSkill } = await import("./setup-skill-PCUBJJYU.js");
1103
+ const { uninstallSkill } = await import("./setup-skill-Z5RVCWCU.js");
1041
1104
  uninstallSkill({ json: jsonMode });
1042
1105
  break;
1043
1106
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-44FSS3CZ.js";
10
+ } from "./chunk-MXUZR2BV.js";
11
11
  import "./chunk-EEG7T6WT.js";
12
12
  import "./chunk-TFCNKBRC.js";
13
13
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import {
10
10
  DateTime
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-JCMWDZYY.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import {
9
9
  AuthCorruptedError,
10
10
  AuthExpiredError,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  PRESETS
3
- } from "./chunk-WBKCXGLV.js";
3
+ } from "./chunk-PIYOWGBZ.js";
4
4
  import "./chunk-E2LAMILJ.js";
5
5
  import {
6
6
  printOutput,
7
7
  toCsvRows
8
8
  } from "./chunk-VFT3TD3E.js";
9
- import "./chunk-44FSS3CZ.js";
9
+ import "./chunk-MXUZR2BV.js";
10
10
  import "./chunk-EEG7T6WT.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,73 @@
1
+ import {
2
+ printOutput,
3
+ toCsvRows
4
+ } from "./chunk-VFT3TD3E.js";
5
+ import {
6
+ createApiClient
7
+ } from "./chunk-MXUZR2BV.js";
8
+ import "./chunk-EEG7T6WT.js";
9
+ import "./chunk-TFCNKBRC.js";
10
+ import "./chunk-U73SABXK.js";
11
+
12
+ // src/commands/healthchecks/list.ts
13
+ async function runHealthchecksList(options) {
14
+ const client = options.apiClient ?? createApiClient();
15
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
16
+ const result = await client.listHealthchecks();
17
+ printOutput({
18
+ outputFormat,
19
+ json: result,
20
+ markdown: [
21
+ "# Healthchecks",
22
+ "",
23
+ ...result.checks.map((entry) => {
24
+ const status = entry.implemented ? "implemented" : "not-implemented";
25
+ const lines = [
26
+ `## ${entry.id} _(${status})_`,
27
+ `- Title: ${entry.title}`,
28
+ `- Category: ${entry.category}`,
29
+ `- Description: ${entry.description}`
30
+ ];
31
+ if (entry.endpoint) {
32
+ lines.push(`- Endpoint: \`${entry.endpoint}\``);
33
+ }
34
+ if (entry.notImplementedReason) {
35
+ lines.push(`- Not yet implemented: ${entry.notImplementedReason}`);
36
+ }
37
+ return lines.join("\n");
38
+ })
39
+ ].join("\n\n"),
40
+ csv: toCsvRows(
41
+ result.checks.map((entry) => ({
42
+ id: entry.id,
43
+ category: entry.category,
44
+ implemented: entry.implemented,
45
+ title: entry.title,
46
+ endpoint: entry.endpoint ?? ""
47
+ }))
48
+ ),
49
+ human: () => {
50
+ const implemented = result.checks.filter((entry) => entry.implemented);
51
+ const notImplemented = result.checks.filter((entry) => !entry.implemented);
52
+ console.log(
53
+ `Healthchecks: ${implemented.length} implemented, ${notImplemented.length} not yet implemented (planned)`
54
+ );
55
+ console.log("");
56
+ console.log("Implemented:");
57
+ for (const entry of implemented) {
58
+ console.log(` \u2713 ${entry.id} [${entry.category}] ${entry.title}`);
59
+ }
60
+ console.log("");
61
+ console.log("Not yet implemented (data gaps; follow-up tracked):");
62
+ for (const entry of notImplemented) {
63
+ console.log(` \xB7 ${entry.id} [${entry.category}] ${entry.title}`);
64
+ if (entry.notImplementedReason) {
65
+ console.log(` ${entry.notImplementedReason}`);
66
+ }
67
+ }
68
+ }
69
+ });
70
+ }
71
+ export {
72
+ runHealthchecksList
73
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-44FSS3CZ.js";
3
+ } from "./chunk-MXUZR2BV.js";
4
4
  import "./chunk-EEG7T6WT.js";
5
5
  import "./chunk-TFCNKBRC.js";
6
6
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,209 @@
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
+ createApiClient
11
+ } from "./chunk-MXUZR2BV.js";
12
+ import "./chunk-EEG7T6WT.js";
13
+ import "./chunk-TFCNKBRC.js";
14
+ import "./chunk-U73SABXK.js";
15
+
16
+ // src/commands/healthchecks/run.ts
17
+ function buildPayload(entry, org, thresholds) {
18
+ const payload = { slug: org };
19
+ const allowedKeys = /* @__PURE__ */ new Set([
20
+ ...Object.keys(entry.defaultThreshold ?? {}),
21
+ "source",
22
+ "minResults",
23
+ "lookbackHours",
24
+ "runBeforeValidate",
25
+ "includeDisabled",
26
+ "warnThreshold",
27
+ "failThreshold",
28
+ "warnAgeHours",
29
+ "failAgeHours",
30
+ "staleThresholdHours"
31
+ ]);
32
+ for (const [key, value] of Object.entries(thresholds)) {
33
+ if (allowedKeys.has(key)) {
34
+ payload[key] = value;
35
+ }
36
+ }
37
+ return payload;
38
+ }
39
+ function statusSymbol(severity) {
40
+ if (severity === "ok") return "\u2713";
41
+ if (severity === "warn") return "\u26A0";
42
+ return "\u2717";
43
+ }
44
+ function statusLabel(severity) {
45
+ if (severity === "ok") return "DONE";
46
+ if (severity === "warn") return "WARN";
47
+ return "FAIL";
48
+ }
49
+ function summariseObserved(result) {
50
+ const parts = Object.entries(result.observed).slice(0, 4).map(([key, value]) => `${key}=${value}`);
51
+ return parts.join(", ");
52
+ }
53
+ async function runHealthchecksRun(options) {
54
+ const client = options.apiClient ?? createApiClient();
55
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
56
+ const thresholds = options.thresholds ?? {};
57
+ const isMachineFormat = outputFormat !== "human";
58
+ const registry = await client.listHealthchecks();
59
+ const targets = [];
60
+ if (options.all) {
61
+ for (const entry of registry.checks) {
62
+ if (entry.implemented && entry.endpoint) targets.push(entry);
63
+ }
64
+ } else if (options.id) {
65
+ const entry = registry.checks.find((item) => item.id === options.id);
66
+ if (!entry) {
67
+ throw new CliExitError(
68
+ `Unknown healthcheck id: '${options.id}'. Run 'chainpatrol healthchecks list' to see available checks.`,
69
+ ExitCode.USAGE
70
+ );
71
+ }
72
+ if (!entry.implemented || !entry.endpoint) {
73
+ throw new CliExitError(
74
+ `Healthcheck '${entry.id}' is not yet implemented on the backend.${entry.notImplementedReason ? ` Reason: ${entry.notImplementedReason}` : ""}`,
75
+ ExitCode.USAGE
76
+ );
77
+ }
78
+ targets.push(entry);
79
+ } else {
80
+ throw new CliExitError(
81
+ "Provide a healthcheck id or --all. Example: 'chainpatrol healthchecks run reviewing.backlog --org acme' or 'chainpatrol healthchecks run --all --org acme'.",
82
+ ExitCode.USAGE
83
+ );
84
+ }
85
+ if (targets.length === 0) {
86
+ throw new CliExitError(
87
+ "No implemented healthchecks available to run.",
88
+ ExitCode.USAGE
89
+ );
90
+ }
91
+ const tasks = targets.map(async (entry) => {
92
+ if (!isMachineFormat) {
93
+ console.log(`Running ${entry.id} for ${options.org}\u2026`);
94
+ }
95
+ try {
96
+ const result = await client.runHealthcheck(
97
+ entry.endpoint,
98
+ buildPayload(entry, options.org, thresholds)
99
+ );
100
+ if (!isMachineFormat) {
101
+ const detail = summariseObserved(result);
102
+ console.log(
103
+ `${statusLabel(result.severity)} \u2014 ${entry.id} ${statusSymbol(result.severity)} (${detail})`
104
+ );
105
+ }
106
+ return { entry, result, error: null };
107
+ } catch (err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ if (!isMachineFormat) {
110
+ console.log(`FAIL \u2014 ${entry.id} \u2717 (error: ${message})`);
111
+ }
112
+ return { entry, result: null, error: message };
113
+ }
114
+ });
115
+ const settled = await Promise.all(tasks);
116
+ const results = settled.filter(
117
+ (entry) => entry.result !== null
118
+ );
119
+ const errors = settled.filter((entry) => entry.error !== null);
120
+ const overallOk = results.every((entry) => entry.result.ok) && errors.length === 0;
121
+ printOutput({
122
+ outputFormat,
123
+ json: {
124
+ org: options.org,
125
+ ok: overallOk,
126
+ results: results.map((entry) => entry.result),
127
+ errors: errors.map((entry) => ({ id: entry.entry.id, error: entry.error }))
128
+ },
129
+ markdown: [
130
+ `# Healthchecks (${options.org})`,
131
+ "",
132
+ `Overall: ${overallOk ? "OK" : "ISSUES FOUND"}`,
133
+ "",
134
+ ...results.map((entry) => {
135
+ const lines = [
136
+ `## ${entry.result.id} \u2014 ${statusLabel(entry.result.severity)}`,
137
+ `- Title: ${entry.result.title}`,
138
+ `- Severity: ${entry.result.severity}`,
139
+ `- Observed: ${JSON.stringify(entry.result.observed)}`,
140
+ `- Threshold: ${JSON.stringify(entry.result.threshold)}`
141
+ ];
142
+ if (entry.result.findings.length > 0) {
143
+ lines.push("- Findings:");
144
+ for (const finding of entry.result.findings) {
145
+ lines.push(
146
+ ` - [${finding.severity}] ${finding.kind}${finding.ref ? ` (${finding.ref})` : ""}: ${finding.message}`
147
+ );
148
+ }
149
+ }
150
+ if (entry.result.suggestedAction) {
151
+ lines.push(`- Suggested action: ${entry.result.suggestedAction}`);
152
+ }
153
+ return lines.join("\n");
154
+ }),
155
+ ...errors.map((entry) => `## ${entry.entry.id} \u2014 ERROR
156
+ - ${entry.error}`)
157
+ ].join("\n\n"),
158
+ csv: toCsvRows(
159
+ results.map((entry) => ({
160
+ id: entry.result.id,
161
+ severity: entry.result.severity,
162
+ ok: entry.result.ok,
163
+ observed: JSON.stringify(entry.result.observed),
164
+ threshold: JSON.stringify(entry.result.threshold),
165
+ findings: entry.result.findings.length
166
+ }))
167
+ ),
168
+ human: () => {
169
+ if (targets.length > 1) {
170
+ console.log("");
171
+ console.log("Summary:");
172
+ for (const entry of results) {
173
+ console.log(
174
+ ` ${statusSymbol(entry.result.severity)} ${entry.result.id} ${statusLabel(entry.result.severity)} ${summariseObserved(entry.result)}`
175
+ );
176
+ }
177
+ for (const entry of errors) {
178
+ console.log(` \u2717 ${entry.entry.id} FAIL error=${entry.error}`);
179
+ }
180
+ }
181
+ for (const entry of results) {
182
+ if (entry.result.findings.length > 0) {
183
+ console.log("");
184
+ console.log(`Findings for ${entry.result.id}:`);
185
+ for (const finding of entry.result.findings) {
186
+ const ref = finding.ref ? ` (${finding.ref})` : "";
187
+ console.log(
188
+ ` [${finding.severity}] ${finding.kind}${ref}: ${finding.message}`
189
+ );
190
+ }
191
+ }
192
+ if (entry.result.suggestedAction) {
193
+ console.log("");
194
+ console.log(`Suggested action for ${entry.result.id}:`);
195
+ console.log(` ${entry.result.suggestedAction}`);
196
+ }
197
+ }
198
+ }
199
+ });
200
+ if (!overallOk) {
201
+ throw new CliExitError(
202
+ "One or more healthchecks failed or errored.",
203
+ ExitCode.CHECK_FAILED
204
+ );
205
+ }
206
+ }
207
+ export {
208
+ runHealthchecksRun
209
+ };
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-WBKCXGLV.js";
4
+ } from "./chunk-PIYOWGBZ.js";
5
5
  import {
6
6
  CliExitError,
7
7
  ExitCode
8
8
  } from "./chunk-E2LAMILJ.js";
9
9
  import "./chunk-VFT3TD3E.js";
10
- import "./chunk-44FSS3CZ.js";
10
+ import "./chunk-MXUZR2BV.js";
11
11
  import "./chunk-EEG7T6WT.js";
12
12
  import "./chunk-TFCNKBRC.js";
13
13
  import "./chunk-U73SABXK.js";
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-DOL35U2S.js";
9
+ } from "./chunk-S65NM7FF.js";
10
10
  import "./chunk-IUZB3DQW.js";
11
11
  export {
12
12
  getBundledSkillContent,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chainpatrol/cli",
3
3
  "description": "The official ChainPatrol CLI — terminal interface for threat detection",
4
4
  "author": "Umar Ahmed <umar@chainpatrol.io>",
5
- "version": "0.3.4",
5
+ "version": "0.4.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [