@chainpatrol/cli 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9126b45: Split the `reviewing.backlog` healthcheck into reviewable vs. total pending so the count matches the reviewing page in the app. The endpoint now reports `pendingProposals` (total), `reviewableProposals` (the subset the UI shows by default — assets not watchlisted, or reports from a customer), and `watchlistedHiddenProposals` (delta). Severity grades on the reviewable count; a non-zero watchlisted bucket surfaces as a warn-level finding. The bundled CLI skill is updated to document the new shape.
8
+ - fb0b846: Split the reviewing healthchecks along the operational divide between "Needs Review" (high-priority queue reviewers act on) and "Watchlisted" (intentionally deferred bucket):
9
+
10
+ - `reviewing.backlog` and `reviewing.old-proposals` now grade the Needs-Review bucket only — assets not on a watchlist, or reports submitted by a customer. Their counts now match what the reviewing page in the app shows by default.
11
+ - Two new checks cover the Watchlisted bucket as separate first-class signals: `reviewing.watchlist-backlog` (pile-up of watchlisted pending proposals) and `reviewing.watchlist-old` (watchlisted proposals deferred too long). Severity for both is **capped at warn** — watchlist cleanup matters but should never block on the same SLA as Needs Review.
12
+
13
+ The CLI skill is updated to spell out the distinction so a healthcheck report groups findings under the right bucket.
14
+
15
+ ## 0.4.0
16
+
17
+ ### Minor Changes
18
+
19
+ - b5de201: Add a `healthchecks` namespace to the public API and CLI.
20
+
21
+ The new `healthchecks list` returns a registry of every check the platform
22
+ exposes today, including planned checks that are not yet implemented on the
23
+ backend (marked `implemented: false` with a `notImplementedReason`). The new
24
+ `healthchecks run <id|--all> --org <slug>` runs a single check or every
25
+ implemented check in parallel and returns each result in a uniform shape
26
+ (`id`, `severity`, `observed`, `threshold`, `findings`, `suggestedAction`).
27
+
28
+ Implemented checks in this release:
29
+
30
+ - `detections.silent-configs` — the existing detection-config healthcheck,
31
+ wrapped in the uniform shape. The original `detections healthcheck`
32
+ command continues to work.
33
+ - `reviewing.backlog` — counts pending review proposals and grades severity
34
+ against per-org thresholds (default warn=50, fail=100).
35
+ - `reviewing.old-proposals` — counts proposals older than the warn / fail
36
+ age thresholds (default 7 / 14 days) and lists the oldest offenders.
37
+ - `takedowns.stale-in-progress` — counts takedowns sitting in IN_PROGRESS
38
+ past the staleness threshold (default 7 days).
39
+
40
+ Planned but not yet implemented (still returned by `list` with
41
+ `implemented: false`): `detections.coverage-gaps`, `detections.spike`,
42
+ `detections.drop`, `reviewing.auto-approval-spike`,
43
+ `blocklisting.gsb-cancelled-rate`, `takedowns.todo-volume`,
44
+ `takedowns.cancelled-rate`, `takedowns.automation-off`.
45
+
46
+ The bundled Claude Code skill is updated to (a) document the new namespace,
47
+ (b) recommend `healthchecks run --all` as the canonical entrypoint when a
48
+ user asks to run a healthcheck on an org, and (c) mark every not-yet-
49
+ implemented check in the HealthCheck Guide with a "manual check, no API
50
+ yet" note so the agent surfaces that gap explicitly.
51
+
3
52
  ## 0.3.4
4
53
 
5
54
  ### 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";
@@ -297,6 +297,96 @@ 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\`, \`reviewing.watchlist-backlog\`,
327
+ \`reviewing.watchlist-old\`, \`takedowns.stale-in-progress\`.
328
+
329
+ ### Pending proposals: "Needs Review" vs "Watchlisted"
330
+
331
+ PENDING proposals split into two operationally distinct buckets, and we
332
+ grade them with separate checks:
333
+
334
+ - **Needs Review** \u2014 pending proposals the reviewing UI shows by default
335
+ (\`excludeWatchlisted=true\`): assets that are NOT on a watchlist, OR
336
+ reports submitted by a customer (those stay visible even when the asset
337
+ is watchlisted). This is the actionable queue reviewers work from, so
338
+ pile-ups and old items here are high-priority signals (\`fail\` severity
339
+ is reachable).
340
+ - **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
341
+ customer-reported reports). The reviewing UI hides these by default
342
+ because watchlisting is the act of intentionally deferring an asset.
343
+ Pile-ups and aged items here are worth surfacing as cleanup work, but
344
+ severity is **capped at warn** so they never block on the same SLA as
345
+ Needs Review. When reporting findings, treat these as lower-priority.
346
+
347
+ Implemented checks today:
348
+
349
+ - **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
350
+ exposed under the uniform shape.
351
+ - **reviewing.backlog** \u2014 counts Needs Review pending proposals and grades
352
+ severity against per-org thresholds (default warn=50, fail=100).
353
+ - **reviewing.old-proposals** \u2014 counts Needs Review proposals older than
354
+ the warn / fail age thresholds (default 7 / 14 days) and lists the
355
+ oldest offenders.
356
+ - **reviewing.watchlist-backlog** \u2014 counts watchlisted pending proposals
357
+ (default warn=200). Severity capped at warn.
358
+ - **reviewing.watchlist-old** \u2014 counts watchlisted pending proposals older
359
+ than the warn-age threshold (default 30 days) and lists the oldest.
360
+ Severity capped at warn.
361
+ - **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
362
+ past the staleness threshold (default 7 days) and lists the oldest.
363
+
364
+ The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
365
+ but **not yet implemented on the backend** \u2014 when the agent surfaces them in
366
+ a healthcheck report, mark them explicitly as "manual check, no API yet":
367
+
368
+ - **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
369
+ Still requires manual reasoning with \`configs list\` + \`reports list\`.
370
+ - **detections.spike** / **detections.drop** \u2014 require server-side baseline
371
+ modeling. Use \`metrics breakdown --by day\` as an interim signal.
372
+ - **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
373
+ human approvers in the review history. Use \`metrics breakdown\` as a proxy.
374
+ - **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
375
+ is not yet exposed in the public API.
376
+ - **takedowns.todo-volume** / **takedowns.cancelled-rate** /
377
+ **takedowns.automation-off** \u2014 the underlying read paths are not yet exposed.
378
+
379
+ When the user asks to "run a healthcheck on org X", the canonical command is:
380
+
381
+ \`\`\`bash
382
+ chainpatrol --json healthchecks run --all --org X
383
+ \`\`\`
384
+
385
+ This iterates the implemented entries in \`healthchecks list\`, runs them in
386
+ parallel, and aggregates the uniform results. Combine with the manual checks
387
+ in the HealthCheck Guide below for everything still marked
388
+ \`implemented: false\`.
389
+
300
390
  ### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
301
391
 
302
392
  Server-side aggregation of the operations review queue (pending proposals,
@@ -442,16 +532,26 @@ others are soft / qualitative signals that still need you to fetch data with
442
532
 
443
533
  ### Quick Path: CLI commands that automate parts of this guide
444
534
 
445
- Run these first to get cheap, structured signals before falling back to the
446
- manual checks in each subsection:
535
+ The canonical first step is now the \`healthchecks\` namespace, which runs
536
+ every implemented check via the public API and returns a uniform shape per
537
+ check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
538
+ \`suggestedAction\`):
447
539
 
448
540
  \`\`\`bash
449
- # Detection: configs that have gone silent or are erroring
450
- chainpatrol --json detections healthcheck --org <slug>
541
+ # Discover every check the platform exposes, implemented or planned.
542
+ chainpatrol --json healthchecks list
451
543
 
452
- # Reviewing & Takedowns: backlog, SLA breaches, stuck takedowns
453
- chainpatrol --json queues snapshot --org <slug>
544
+ # Run every implemented healthcheck in parallel and aggregate the results.
545
+ chainpatrol --json healthchecks run --all --org <slug>
546
+
547
+ # Run a single named check.
548
+ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
549
+ \`\`\`
550
+
551
+ After \`healthchecks run --all\`, use these complementary commands to cover
552
+ the signals that are not yet exposed as a uniform healthcheck endpoint:
454
553
 
554
+ \`\`\`bash
455
555
  # Spikes / drops in detection volume over time (compare windows)
456
556
  chainpatrol --json metrics breakdown --org <slug> --by day --this-week
457
557
 
@@ -461,6 +561,9 @@ chainpatrol --json reports list --org <slug> --reported-by-customer
461
561
  # What's enabled vs disabled vs not configured for the org
462
562
  chainpatrol --json configs list --org <slug>
463
563
 
564
+ # Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
565
+ chainpatrol --json queues snapshot --org <slug>
566
+
464
567
  # Packaged weekly customer-success sweep (preferred when it covers the ask)
465
568
  chainpatrol presets run cs-weekly-health --org <slug>
466
569
  \`\`\`
@@ -469,7 +572,8 @@ Treat each command's output as one input to the healthcheck. The manual
469
572
  checks below still apply \u2014 especially for signals the CLI cannot infer on
470
573
  its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
471
574
  is disabled", or "this drop is fine because the config isn't relevant
472
- to this org").
575
+ to this org"). Each subsection of the guide notes whether a healthcheck
576
+ endpoint exists today and what to fall back on when it doesn't.
473
577
 
474
578
  ### Reporting progress while running a healthcheck
475
579
 
@@ -524,13 +628,15 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
524
628
  detection sources like "Twitter / X User Search" or "Twitter Post Search" are
525
629
  disabled. Those should be turned on.
526
630
 
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).
631
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
632
+ \`healthchecks list\` as \`detections.coverage-gaps\` with
633
+ \`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
634
+ "manual check, no API yet". Until the endpoint lands, do the correlation
635
+ manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
636
+ disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
637
+ recent blocked-item asset types. Flag any asset type where blocked items
638
+ exist but the matching detection source has \`status: "disabled"\` (or
639
+ appears in the "Not configured" group).
534
640
 
535
641
  #### Spike in Detections
536
642
 
@@ -538,7 +644,9 @@ A spike in recent detections is worth investigating. It could be a bad config
538
644
  change, or it could be a legitimate new attack push in this area \u2014 useful
539
645
  intel to surface to the security team as a targeted spike.
540
646
 
541
- **Run via CLI:** \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
647
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
648
+ in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
649
+ Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
542
650
  (and a comparison window via \`--from\`/\`--to\`) to see daily detection
543
651
  volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
544
652
  against recent config changes for that source.
@@ -549,40 +657,85 @@ A drop is also worth looking into. It may be a bad config change, or it may
549
657
  mean the config is not really relevant and can be safely turned off (not all
550
658
  default-on configs are relevant to every org).
551
659
 
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.
660
+ **Run via CLI:** the extreme case ("this source went silent") is covered
661
+ today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
662
+ which is the canonical replacement for the older \`detections healthcheck\`.
663
+ It fails any config whose \`recentResultCount\` is below \`--min-results\`
664
+ in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
665
+ \`detections healthcheck --run\`) to also catch configs that error when
666
+ executed. For soft drops (still producing results but below baseline),
667
+ \`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
668
+ \u2014 use \`metrics breakdown --by day\` and compare windows manually.
557
669
 
558
670
  ### Reviewing
559
671
 
560
- #### Pile Up / Backlog of Unreviewed Proposals
672
+ PENDING proposals split into two operationally distinct buckets:
673
+
674
+ - **Needs Review** \u2014 assets not on a watchlist, or reports submitted by a
675
+ customer. This is the reviewing UI's default view (\`excludeWatchlisted=true\`)
676
+ and the actionable queue reviewers work from. Pile-ups and aged items
677
+ here are high priority \u2014 \`fail\` severity is reachable.
678
+ - **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
679
+ customer-reported reports). The UI hides these by default because
680
+ watchlisting is the act of intentionally deferring the asset. Pile-ups
681
+ and aged items here are worth surfacing as cleanup work but **severity
682
+ is capped at warn** \u2014 they should never block on the same SLA as Needs
683
+ Review.
684
+
685
+ Each bucket has its own pile-up and age check, so you can grade them
686
+ independently and tune thresholds without one drowning the other.
687
+
688
+ #### Pile Up / Backlog of Needs-Review Proposals
561
689
 
562
690
  Too many proposals waiting in review. For most organizations this is over 100
563
691
  reports, but really the threshold is relative to the average number of
564
692
  confirmed threats per week. Example: if an org only adds 5 blocked threats per
565
693
  week, then a 7-day backlog of even 10 proposals is a really big deal.
566
694
 
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.
573
-
574
- #### Really Old Proposals
575
-
576
- Any proposal waiting for review longer than 14 days is a sign something has
577
- gone wrong. Even complex investigations rarely take longer than this. Except
578
- for rare cases, these should be rejected or approved to prevent a backlog
579
- from building.
580
-
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.
695
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
696
+ The endpoint counts the **Needs Review** subset only (assets not
697
+ watchlisted, or reports marked \`reportedByCustomer\`) and grades severity
698
+ against per-org thresholds (default warn=50, fail=100; override with
699
+ \`--warn-threshold\` / \`--fail-threshold\` via the run payload). This is
700
+ the number the reviewing page in the app shows by default, so the
701
+ healthcheck output matches what reviewers see.
702
+
703
+ For raw counts plus SLA / age breakdowns,
704
+ \`chainpatrol --json queues snapshot --org <slug>\` remains useful and
705
+ exposes \`reviewQueue.totalPendingProposals\` and
706
+ \`reviewQueue.distinctReports\` (note: \`queues snapshot\` does NOT apply
707
+ the watchlist filter, so its number is the sum of Needs Review +
708
+ Watchlisted). Compare against the org's typical weekly throughput
709
+ (use \`metrics summary --this-week\` for that baseline) \u2014 a backlog that
710
+ exceeds a week of typical confirmed-threat volume is a finding regardless
711
+ of the absolute number.
712
+
713
+ #### Really Old Needs-Review Proposals
714
+
715
+ Any Needs-Review proposal waiting longer than 14 days is a sign something
716
+ has gone wrong. Even complex investigations rarely take longer than this.
717
+ Except for rare cases, these should be rejected or approved to prevent a
718
+ backlog from building.
719
+
720
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
721
+ The endpoint counts Needs-Review proposals older than the warn / fail age
722
+ thresholds (default 7 / 14 days) and lists the oldest offenders in
723
+ \`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
724
+ works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
725
+ strictest SLA breaches separately \u2014 any non-zero value is worth raising.
726
+
727
+ #### Watchlist Pile-Up / Old Watchlisted Proposals
728
+
729
+ Watchlisted-pending proposals are deferred on purpose, but they shouldn't
730
+ grow unbounded \u2014 a huge pile or very-old items signal that the watchlist
731
+ needs a cleanup pass. These don't block on the same SLA as Needs Review.
732
+
733
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.watchlist-backlog\`**
734
+ (pile-up count, default warn=200) and
735
+ **\`healthchecks run reviewing.watchlist-old\`** (default warn-age 30
736
+ days). Both cap severity at warn. When reporting findings during an org
737
+ healthcheck, group them under "watchlist cleanup" rather than mixing with
738
+ Needs-Review findings \u2014 they're operationally different concerns.
586
739
 
587
740
  #### Spike in Auto Approved Reports
588
741
 
@@ -594,8 +747,10 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
594
747
  adjusted recently, since those may be the cause of spam combined with a weak
595
748
  rule.
596
749
 
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\`
750
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
751
+ in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
752
+ \`implemented: false\`. As a proxy until the endpoint lands, use
753
+ \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
599
754
  and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
600
755
  isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
601
756
  points at automation doing the approving.
@@ -613,9 +768,10 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
613
768
  are too many false positives landing on the blocklist, indicating issues with
614
769
  detection and reviewing rules.
615
770
 
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.
771
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
772
+ in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
773
+ \`implemented: false\`. Until Google Safe Browsing submission state is
774
+ exposed in the public API, this remains a manual / engineering-team check.
619
775
 
620
776
  ### Takedowns
621
777
 
@@ -625,8 +781,11 @@ Can mean a gap in automated takedowns not being implemented for some new area
625
781
  of threats. It can also mean the areas that require manual takedowns are
626
782
  being missed by the takedown team.
627
783
 
628
- **Run via CLI:** \`chainpatrol --json queues snapshot --org <slug>\` returns
629
- \`takedownQueue.totalOpen\` and breakdowns by status. A persistently large
784
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
785
+ in \`healthchecks list\` as \`takedowns.todo-volume\` with
786
+ \`implemented: false\` (needs a per-org throughput baseline). As an interim
787
+ signal, \`chainpatrol --json queues snapshot --org <slug>\` returns
788
+ \`takedownQueue.totalOpen\` and breakdowns by status; a persistently large
630
789
  ToDo bucket relative to the org's typical takedown rate (use
631
790
  \`metrics summary\` for that baseline) is the finding.
632
791
 
@@ -636,10 +795,12 @@ Typically means something is wrong with the submission itself. The takedown
636
795
  may need to be resubmitted, the vendor asked for more evidence, or we may
637
796
  have submitted it in the wrong place.
638
797
 
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.
798
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.stale-in-progress\`.**
799
+ The endpoint counts takedowns sitting in IN_PROGRESS past the staleness
800
+ threshold (default 7 days) and lists the oldest offenders in \`findings\`.
801
+ \`queues snapshot\` (\`takedownQueue.staleInProgress\`) still works as a raw
802
+ view. Any non-zero value is worth investigating; a growing number across
803
+ snapshots strongly suggests a vendor-side or submission-format problem.
643
804
 
644
805
  #### Too Many Cancelled Takedowns
645
806
 
@@ -648,7 +809,10 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
648
809
  when it's already taken down are treated as completed, not cancelled. So even
649
810
  3 cancelled takedowns in a 7-day period is too many.
650
811
 
651
- **Run via CLI:** no first-class command yet. As an interim signal, use
812
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
813
+ in \`healthchecks list\` as \`takedowns.cancelled-rate\` with
814
+ \`implemented: false\` (public API does not yet expose CANCELLED takedown
815
+ counts over a rolling window). As an interim signal, use
652
816
  \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
653
817
  and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
654
818
  divergence \u2014 a widening gap with no In-Progress growth often shows up as
@@ -660,9 +824,12 @@ Automated takedowns should be on by default for nearly every organization.
660
824
  Any issue that would make you want to turn off automated takedowns should be
661
825
  resolved within 30 days.
662
826
 
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.
827
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
828
+ in \`healthchecks list\` as \`takedowns.automation-off\` with
829
+ \`implemented: false\` (public API does not yet expose automated-takedown
830
+ enablement state). Check this manually with the org's takedown automation
831
+ settings; the CLI's role here is mostly to highlight stale state in
832
+ \`queues snapshot\` so you know to ask.
666
833
  `;
667
834
  }
668
835
  function getBundledSkillVersion() {
@@ -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";
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-F4GU6AII.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-2SEVH3M6.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-2SEVH3M6.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-F4GU6AII.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.1",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [