@chainpatrol/cli 0.4.1 → 0.6.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,36 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 575b671: Add `chainpatrol orgs list` — lists organizations accessible to the caller along with each org's subscription status and active/automated service flags (reporting, reviewing, protection, takedowns, detection, dark web monitoring). Filter flags compose with AND and are applied server-side:
8
+
9
+ - `--subscription-status <list>` — comma-separated `PROSPECT`, `TRIAL`, `ACTIVE`, `INTEGRATION` (`INACTIVE` is intentionally not reachable through this filter)
10
+ - `--service-active <list>` / `--service-inactive <list>` — services that must be active / inactive
11
+ - `--service-automated <list>` / `--service-manual <list>` — services whose automation must be on / off (`reporting`, `reviewing`, `protection`, `takedowns`)
12
+ - `--query <text>` — partial name match
13
+
14
+ Use cases: "which ACTIVE customers have takedowns enabled but automation off", "which prospects don't have detection turned on yet". The bundled CLI skill gets a new `orgs list` section under the `queues snapshot` heading.
15
+
16
+ ## 0.5.0
17
+
18
+ ### Minor Changes
19
+
20
+ - f8d9454: Add five new healthchecks covering the takedown pipeline and asset liveness, expanding the runnable surface for `chainpatrol healthchecks run`:
21
+
22
+ - **`takedowns.todo-volume`** — counts takedowns sitting in TODO. Default warn=50, fail=100. Catches automation gaps on new threat surfaces or manual-filing capacity issues.
23
+ - **`takedowns.in-progress-volume`** — counts takedowns currently IN*PROGRESS regardless of age. Default warn=30, fail=75. Complements `takedowns.stale-in-progress` (age-based) by catching submission-format / vendor-side issues \_before* items go stale.
24
+ - **`takedowns.cancelled-count`** — counts transitions into CANCELLED from the `TakedownEvent` log over a rolling window. Default 7d / warn=3 / fail=10. Replaces the placeholder `takedowns.cancelled-rate` registry entry. Uses the event log rather than `Takedown.updatedAt` so the cancellation date is correct even if the takedown has since been edited.
25
+ - **`takedowns.automation-off`** — flags orgs with takedown service enabled but `isAutomatedTakedownsActive` off for too long. Default warn=30d, fail=60d. Age is derived from the most recent `SERVICES_AUTOMATED_TAKEDOWNS_UPDATED` `OrganizationEvent`, falling back to `Organization.updatedAt`. Orgs with takedown service entirely disabled are skipped.
26
+ - **`assets.dead-asset-spike`** — compares `DETECTED_AS_DEAD` events in the current window against the prior baseline rate; warns on a multiplier exceeding the threshold (default 24h vs 7d, ×2 warn / ×4 fail) once the current count clears the `minSpikeCount` floor. Catches liveness-checker regressions after platform changes.
27
+
28
+ The bundled CLI skill gets a rewritten **Takedowns** section covering all four takedown checks and a new top-level **Assets** section covering `dead-asset-spike` plus guidance on the still-unimplemented `assets.dead-but-alive` / `assets.alive-but-marked-dead` (which require live HTTP probes and are not a fit for synchronous healthchecks).
29
+
30
+ The public `HealthcheckResult.category` enum gains `"assets"`. `observed` and `threshold` records now also accept booleans and null (for fields like `automatedTakedownsActive` and the nullable `ratio` on the spike check).
31
+
32
+ `HealthcheckResult` also gains an `appUrl` field (string or null) that deep-links to the relevant filtered admin page in the web app. For example, `takedowns.stale-in-progress` returns `https://app.chainpatrol.io/admin/<slug>/takedowns?takedownStatus=IN_PROGRESS&sortBy=oldest` and `reviewing.backlog` returns `https://app.chainpatrol.io/admin/<slug>/review?excludeWatchlisted=true`. The CLI prints `View in app: <url>` after each result. Checks without a sensible filtered view (`detections.silent-configs`, `assets.dead-asset-spike`) return `null`. The base URL respects `BETTER_AUTH_URL` and falls back to `https://app.chainpatrol.io`.
33
+
3
34
  ## 0.4.1
4
35
 
5
36
  ### Patch Changes
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-MXUZR2BV.js";
7
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -147,6 +147,9 @@ function createApiClient(options) {
147
147
  },
148
148
  runHealthcheck(endpoint, input) {
149
149
  return request(endpoint, input);
150
+ },
151
+ getUserOrgs(input) {
152
+ return request("/user/orgs", input);
150
153
  }
151
154
  };
152
155
  }
@@ -82,7 +82,9 @@ description: |
82
82
  "am I logged in", "list configs", "use the cli", "list reports",
83
83
  "customer reports", "reports reported by customer", "find detection gaps",
84
84
  "org healthcheck", "organization health check", "audit my org",
85
- "what's wrong with org", "review org setup".
85
+ "what's wrong with org", "review org setup", "list orgs", "list organizations",
86
+ "orgs with takedowns off", "automation off across orgs",
87
+ "which customers have X enabled", "service toggles by org".
86
88
  allowed-tools:
87
89
  - Bash
88
90
  - Read
@@ -321,10 +323,13 @@ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
321
323
  chainpatrol --json healthchecks run --all --org <slug>
322
324
  \`\`\`
323
325
 
324
- Each implemented check has a stable id of the form \`category.name\`, e.g.
325
- \`detections.silent-configs\`, \`reviewing.backlog\`,
326
+ Each implemented check has a stable id of the form \`category.name\`. Implemented
327
+ ids today: \`detections.silent-configs\`, \`reviewing.backlog\`,
326
328
  \`reviewing.old-proposals\`, \`reviewing.watchlist-backlog\`,
327
- \`reviewing.watchlist-old\`, \`takedowns.stale-in-progress\`.
329
+ \`reviewing.watchlist-old\`, \`takedowns.todo-volume\`,
330
+ \`takedowns.in-progress-volume\`, \`takedowns.stale-in-progress\`,
331
+ \`takedowns.cancelled-count\`, \`takedowns.automation-off\`,
332
+ \`assets.dead-asset-spike\`.
328
333
 
329
334
  ### Pending proposals: "Needs Review" vs "Watchlisted"
330
335
 
@@ -344,6 +349,15 @@ grade them with separate checks:
344
349
  severity is **capped at warn** so they never block on the same SLA as
345
350
  Needs Review. When reporting findings, treat these as lower-priority.
346
351
 
352
+ Each healthcheck result includes an \`appUrl\` field (string or null) that
353
+ deep-links to the relevant filtered admin page in the web app \u2014 e.g. the
354
+ takedowns page filtered to IN_PROGRESS for \`takedowns.stale-in-progress\`,
355
+ or the review page filtered to oldest pending for \`reviewing.old-proposals\`.
356
+ **When reporting a non-OK healthcheck to the user, always surface the
357
+ \`appUrl\` so they can jump straight to the right view.** Some checks
358
+ (\`detections.silent-configs\`, \`assets.dead-asset-spike\`) emit \`null\`
359
+ because no filterable list page exists for that signal yet.
360
+
347
361
  Implemented checks today:
348
362
 
349
363
  - **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
@@ -358,8 +372,27 @@ Implemented checks today:
358
372
  - **reviewing.watchlist-old** \u2014 counts watchlisted pending proposals older
359
373
  than the warn-age threshold (default 30 days) and lists the oldest.
360
374
  Severity capped at warn.
375
+ - **takedowns.todo-volume** \u2014 counts takedowns in TODO (default warn=50,
376
+ fail=100). Pile-ups here usually mean an automation gap on a new threat
377
+ surface, or manual-filing capacity issues.
378
+ - **takedowns.in-progress-volume** \u2014 counts takedowns currently IN_PROGRESS
379
+ regardless of age (default warn=30, fail=75). Complements
380
+ \`stale-in-progress\` \u2014 a high count signals vendor-side or
381
+ submission-format problems even before items go stale.
361
382
  - **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
362
383
  past the staleness threshold (default 7 days) and lists the oldest.
384
+ - **takedowns.cancelled-count** \u2014 counts CANCELLED transitions from the
385
+ TakedownEvent log over a rolling window (default 7d, warn=3, fail=10).
386
+ Cancellations should be rare; a spike usually means a proposal-funnel
387
+ quality problem or misuse of the CANCELLED status.
388
+ - **takedowns.automation-off** \u2014 flags orgs with takedown service enabled
389
+ but \`isAutomatedTakedownsActive\` off for too long (default warn=30d,
390
+ fail=60d). Skipped for orgs with takedown service entirely disabled.
391
+ - **assets.dead-asset-spike** \u2014 compares DEAD-detection events in the
392
+ current window against the prior baseline rate; warns on a multiplier
393
+ exceeding the threshold (default 24h vs 7d, \xD72 warn / \xD74 fail) once the
394
+ current count clears the \`minSpikeCount\` floor. Catches liveness-checker
395
+ regressions after platform changes.
363
396
 
364
397
  The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
365
398
  but **not yet implemented on the backend** \u2014 when the agent surfaces them in
@@ -373,8 +406,11 @@ a healthcheck report, mark them explicitly as "manual check, no API yet":
373
406
  human approvers in the review history. Use \`metrics breakdown\` as a proxy.
374
407
  - **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
375
408
  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.
409
+ - **assets.dead-but-alive** / **assets.alive-but-marked-dead** \u2014 require live
410
+ HTTP probes against asset URLs, which is not a synchronous-healthcheck
411
+ shape. Until a dedicated probe command exists, sample a handful manually
412
+ from \`assets list --status DEAD\` (or ALIVE) and verify in a browser. Notify
413
+ ChainPatrol engineering if the liveness checker looks miscalibrated.
378
414
 
379
415
  When the user asks to "run a healthcheck on org X", the canonical command is:
380
416
 
@@ -409,6 +445,51 @@ Guide. Key signals in the response:
409
445
 
410
446
  Use \`--all\` to snapshot every org you have access to instead of a single slug.
411
447
 
448
+ ### \`orgs list\` \u2014 List organizations with subscription status and service toggles
449
+
450
+ Returns every organization the caller can see, with each org's
451
+ subscription status (\`PROSPECT\`, \`TRIAL\`, \`ACTIVE\`, \`INTEGRATION\`) and
452
+ which services are active and automated. Use it to answer questions like
453
+ "which customers have takedowns enabled but automation off?" or "which
454
+ prospects don't have detection turned on yet?" \u2014 filters compose with AND
455
+ and are applied server-side, so one call returns the final list.
456
+
457
+ \`\`\`bash
458
+ chainpatrol --json orgs list \\
459
+ --subscription-status ACTIVE \\
460
+ --service-active takedowns \\
461
+ --service-manual takedowns
462
+ \`\`\`
463
+
464
+ Filter flags (all optional, all comma-separated lists):
465
+
466
+ - \`--query <text>\` partial name match (substring, case-insensitive)
467
+ - \`--subscription-status <list>\` one or more of \`PROSPECT\`, \`TRIAL\`,
468
+ \`ACTIVE\`, \`INTEGRATION\`. \`INACTIVE\` is intentionally not reachable
469
+ through this filter \u2014 \`orgs list\` only ever returns live customers.
470
+ - \`--service-active <list>\` services that must be active
471
+ - \`--service-inactive <list>\` services that must be inactive
472
+ - \`--service-automated <list>\` services whose automation must be ON
473
+ (\`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\` \u2014 the four
474
+ with an automation toggle)
475
+ - \`--service-manual <list>\` services whose automation must be OFF
476
+ (same four)
477
+
478
+ Service names: \`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\`,
479
+ \`detection\`, \`darkWebMonitoring\`.
480
+
481
+ Customers see only orgs they're a member of. Staff/superuser sessions see
482
+ every matching org. The response is the same in both cases; visibility is
483
+ enforced server-side.
484
+
485
+ #### Use case: finding service configuration gaps across the customer base
486
+
487
+ When the user asks something like "which customers are paying us but don't
488
+ have takedowns automated yet?" or "any orgs running detection without
489
+ takedowns?", reach for \`orgs list\` \u2014 it's the only command that exposes
490
+ service flags across multiple orgs in one call. Run it in \`--json\` mode
491
+ and summarize patterns by service or by subscription tier.
492
+
412
493
  ### \`metrics summary | found | breakdown\` \u2014 Org metrics for spike/drop analysis
413
494
 
414
495
  \`\`\`bash
@@ -775,19 +856,23 @@ exposed in the public API, this remains a manual / engineering-team check.
775
856
 
776
857
  ### Takedowns
777
858
 
859
+ The takedown pipeline has three stages \u2014 TODO (queued, not yet filed),
860
+ IN_PROGRESS (filed, waiting on vendor / customer / refile), and a terminal
861
+ state (COMPLETED or CANCELLED). Healthchecks cover pile-ups at each stage,
862
+ plus quality/configuration issues.
863
+
778
864
  #### Too Many Takedowns in ToDo
779
865
 
780
866
  Can mean a gap in automated takedowns not being implemented for some new area
781
867
  of threats. It can also mean the areas that require manual takedowns are
782
868
  being missed by the takedown team.
783
869
 
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
789
- ToDo bucket relative to the org's typical takedown rate (use
790
- \`metrics summary\` for that baseline) is the finding.
870
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.todo-volume\`.**
871
+ Counts takedowns sitting in TODO (default warn=50, fail=100). For raw
872
+ breakdowns by type, cross-reference with
873
+ \`chainpatrol --json metrics breakdown --org <slug> --by assetType\` \u2014
874
+ items piled up on a specific platform usually point at an automation
875
+ gap there.
791
876
 
792
877
  #### Too Many Takedowns In Progress
793
878
 
@@ -795,12 +880,16 @@ Typically means something is wrong with the submission itself. The takedown
795
880
  may need to be resubmitted, the vendor asked for more evidence, or we may
796
881
  have submitted it in the wrong place.
797
882
 
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.
883
+ **Run via CLI:** Two checks, complementary:
884
+
885
+ - **\`healthchecks run takedowns.in-progress-volume\`** \u2014 counts all
886
+ IN_PROGRESS takedowns regardless of age (default warn=30, fail=75).
887
+ Catches a vendor-side or submission-format problem before items go
888
+ stale.
889
+ - **\`healthchecks run takedowns.stale-in-progress\`** \u2014 counts IN_PROGRESS
890
+ takedowns past a staleness threshold (default 7 days), lists the oldest
891
+ offenders. Any non-zero value is worth investigating; a growing count
892
+ across snapshots strongly suggests vendor-side or format issues.
804
893
 
805
894
  #### Too Many Cancelled Takedowns
806
895
 
@@ -809,14 +898,13 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
809
898
  when it's already taken down are treated as completed, not cancelled. So even
810
899
  3 cancelled takedowns in a 7-day period is too many.
811
900
 
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
816
- \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
817
- and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
818
- divergence \u2014 a widening gap with no In-Progress growth often shows up as
819
- cancellations.
901
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.cancelled-count\`.**
902
+ Counts transitions into the CANCELLED status from the TakedownEvent log
903
+ within the lookback window (default 7 days, warn=3, fail=10). The check
904
+ uses the event log rather than \`Takedown.updatedAt\` so it correctly
905
+ attributes the cancellation date even if the takedown has since been
906
+ edited. A spike usually means a quality problem in the proposal funnel
907
+ or the CANCELLED status being used as a catch-all.
820
908
 
821
909
  #### Automated Takedowns Turned Off for Over 30 Days
822
910
 
@@ -824,12 +912,56 @@ Automated takedowns should be on by default for nearly every organization.
824
912
  Any issue that would make you want to turn off automated takedowns should be
825
913
  resolved within 30 days.
826
914
 
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.
915
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.automation-off\`.**
916
+ Checks \`Organization.isAutomatedTakedownsActive\` and derives the
917
+ off-duration from the most recent \`SERVICES_AUTOMATED_TAKEDOWNS_UPDATED\`
918
+ entry in \`OrganizationEvent\` (default warn 30 days, fail 60 days). Orgs
919
+ with takedown service entirely disabled (\`isTakedownsActive=0\`) are
920
+ skipped \u2014 automation being off is implied in that case.
921
+
922
+ ### Assets
923
+
924
+ Healthchecks on the asset model \u2014 specifically asset liveness state, which
925
+ the takedown team depends on for follow-up.
926
+
927
+ #### Spike in Recently Dead Assets
928
+
929
+ A sudden spike in DEAD detections can be a good signal (takedowns or platform
930
+ moderation working) but it can also mean the liveness checker is
931
+ misclassifying assets after a platform change, captcha rollout, or anti-bot
932
+ update. If many assets become dead at once, sample a few manually.
933
+
934
+ **Run via CLI:** **Implemented as \`healthchecks run assets.dead-asset-spike\`.**
935
+ Compares \`DETECTED_AS_DEAD\` events in the current window (default 24h)
936
+ against the baseline rate from the prior \`baselineDays\` (default 7d).
937
+ Severity fires only when the current count clears \`minSpikeCount\` (default
938
+ 10) AND exceeds the multiplier (default warn \xD72, fail \xD74). The
939
+ \`minSpikeCount\` floor suppresses noise on orgs with near-zero baseline
940
+ activity. When this fires, pull a sample of recent DEAD assets, verify a
941
+ few in a browser, and notify ChainPatrol engineering if the sample is
942
+ clearly still live.
943
+
944
+ #### Assets Marked Dead but Still Online / Assets Not Marked Dead Even Though They Are Down
945
+
946
+ These two opposite failure modes are **not implemented as healthchecks**
947
+ (listed as \`assets.dead-but-alive\` and \`assets.alive-but-marked-dead\` with
948
+ \`implemented: false\`). Both require live HTTP probes against asset URLs,
949
+ which is not a synchronous-healthcheck shape.
950
+
951
+ Until a dedicated probe command exists:
952
+
953
+ - For "marked dead but still alive": sample a handful of recently-DEAD
954
+ assets, open them in a browser, and watch for any that load. Common
955
+ causes: bot protection, geo-blocking, rate limits, or liveness logic
956
+ that does not handle the asset type correctly.
957
+ - For "alive but marked dead": after a known takedown event, sample
958
+ assets that *should* be dead but are still marked alive. Common causes:
959
+ cached responses, soft-404 pages, parked-domain redirects, platform
960
+ suspension pages still returning 200.
961
+
962
+ In both cases, if liveness looks miscalibrated for a class of assets,
963
+ notify ChainPatrol engineering \u2014 the checker likely needs a tuning pass for
964
+ that platform.
833
965
  `;
834
966
  }
835
967
  function getBundledSkillVersion() {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-F4GU6AII.js";
16
+ } from "./chunk-XOXQPUR6.js";
17
17
  import "./chunk-IUZB3DQW.js";
18
18
  import {
19
19
  DateTime
@@ -281,6 +281,31 @@ var HELP = {
281
281
  "--window-hours <n> Hours used for staleness windows"
282
282
  ]
283
283
  },
284
+ orgs: {
285
+ description: "List organizations accessible to you with subscription status and service toggles.",
286
+ usage: "chainpatrol orgs list [filters]",
287
+ examples: [
288
+ "chainpatrol orgs list",
289
+ "chainpatrol orgs list --subscription-status ACTIVE --service-active takedowns --service-manual takedowns"
290
+ ]
291
+ },
292
+ "orgs list": {
293
+ description: "List organizations with their subscription status and per-service active/automated flags. All filters compose with AND.",
294
+ usage: "chainpatrol orgs list [filters]",
295
+ options: [
296
+ "--query <text> Partial name match (substring, case-insensitive)",
297
+ "--subscription-status <l> Comma-separated list (PROSPECT,TRIAL,ACTIVE,INTEGRATION)",
298
+ "--service-active <l> Comma-separated services that must be active",
299
+ "--service-inactive <l> Comma-separated services that must be inactive",
300
+ "--service-automated <l> Services whose automation must be ON (reporting,reviewing,protection,takedowns)",
301
+ "--service-manual <l> Services whose automation must be OFF (reporting,reviewing,protection,takedowns)"
302
+ ],
303
+ examples: [
304
+ "chainpatrol orgs list --subscription-status ACTIVE",
305
+ "chainpatrol orgs list --service-active takedowns --service-manual takedowns",
306
+ "chainpatrol --json orgs list --subscription-status ACTIVE,TRIAL --service-active detection"
307
+ ]
308
+ },
284
309
  healthchecks: {
285
310
  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
311
  usage: "chainpatrol healthchecks <list|run <id>|run --all>",
@@ -369,6 +394,7 @@ function getTopLevelHelp() {
369
394
  " metrics Query organization metrics and breakdowns",
370
395
  " reports Create and list reports from terminal",
371
396
  " queues Snapshot operations review/takedown queues",
397
+ " orgs List organizations and filter by status/services",
372
398
  " presets Run saved workflows for common jobs",
373
399
  " setup Install Claude Code skill and shell completions",
374
400
  " uninstall Remove Claude Code skill and shell completions",
@@ -538,6 +564,7 @@ var COMMANDS = [
538
564
  "metrics",
539
565
  "reports",
540
566
  "queues",
567
+ "orgs",
541
568
  "presets",
542
569
  "setup",
543
570
  "uninstall",
@@ -614,6 +641,12 @@ ${getTopLevelHelp()}
614
641
  contactInfo: { type: "string" },
615
642
  externalSubmissionLink: { type: "string" },
616
643
  payloadFile: { type: "string" },
644
+ subscriptionStatus: { type: "string" },
645
+ serviceActive: { type: "string" },
646
+ serviceInactive: { type: "string" },
647
+ serviceAutomated: { type: "string" },
648
+ serviceManual: { type: "string" },
649
+ query: { type: "string" },
617
650
  help: { type: "boolean", shortFlag: "h" },
618
651
  version: { type: "boolean", shortFlag: "V" },
619
652
  quiet: { type: "boolean", default: false, shortFlag: "q" },
@@ -717,12 +750,12 @@ function parseAttachmentUrls() {
717
750
  }
718
751
  async function handleConfigsList(org) {
719
752
  if (jsonMode) {
720
- const { listConfigsJson } = await import("./list-json-WTMYLZGY.js");
753
+ const { listConfigsJson } = await import("./list-json-LEKCCWQU.js");
721
754
  await listConfigsJson({ org });
722
755
  return;
723
756
  }
724
757
  const { render } = await import("ink");
725
- const { default: ConfigsList } = await import("./list-GEMCFDD5.js");
758
+ const { default: ConfigsList } = await import("./list-5ENZAOFL.js");
726
759
  const { default: React } = await import("react");
727
760
  render(React.createElement(ConfigsList, { org }));
728
761
  }
@@ -820,7 +853,7 @@ async function main() {
820
853
  case "detections": {
821
854
  const org = await resolveOrg();
822
855
  if (subcommand === "healthcheck") {
823
- const { runDetectionsHealthcheck } = await import("./healthcheck-KAONRGSS.js");
856
+ const { runDetectionsHealthcheck } = await import("./healthcheck-AQUXVKAO.js");
824
857
  await runDetectionsHealthcheck({
825
858
  org,
826
859
  source: cli.flags.source,
@@ -835,7 +868,7 @@ async function main() {
835
868
  break;
836
869
  }
837
870
  if (subcommand === "validate") {
838
- const { runDetectionsValidate } = await import("./validate-BJFEKI2N.js");
871
+ const { runDetectionsValidate } = await import("./validate-27RUCN7R.js");
839
872
  await runDetectionsValidate({
840
873
  org,
841
874
  source: cli.flags.source,
@@ -850,7 +883,7 @@ async function main() {
850
883
  break;
851
884
  }
852
885
  if (subcommand === "drift") {
853
- const { runDetectionsDrift } = await import("./drift-DZ6A7JL5.js");
886
+ const { runDetectionsDrift } = await import("./drift-VOKQJ36G.js");
854
887
  await runDetectionsDrift({
855
888
  org,
856
889
  source: cli.flags.source,
@@ -864,7 +897,7 @@ async function main() {
864
897
  break;
865
898
  }
866
899
  if (subcommand === "run") {
867
- const { runDetectionsRun } = await import("./run-43CC5AXR.js");
900
+ const { runDetectionsRun } = await import("./run-OT2X46GT.js");
868
901
  await runDetectionsRun({
869
902
  org,
870
903
  configId: cli.flags.configId,
@@ -883,7 +916,7 @@ async function main() {
883
916
  break;
884
917
  }
885
918
  if (action === "run") {
886
- const { runDetectionsRun } = await import("./run-43CC5AXR.js");
919
+ const { runDetectionsRun } = await import("./run-OT2X46GT.js");
887
920
  await runDetectionsRun({
888
921
  org,
889
922
  configId: cli.flags.configId,
@@ -901,7 +934,7 @@ async function main() {
901
934
  throw new Error("detections configs update requires --config-id");
902
935
  }
903
936
  const configPatch = getConfigPatchFromSetFlags();
904
- const { runDetectionsConfigsUpdate } = await import("./configs-update-RPN32YTL.js");
937
+ const { runDetectionsConfigsUpdate } = await import("./configs-update-VROBC2HI.js");
905
938
  await runDetectionsConfigsUpdate({
906
939
  org,
907
940
  configId: cli.flags.configId,
@@ -928,7 +961,7 @@ async function main() {
928
961
  case "metrics": {
929
962
  const org = await resolveOrg();
930
963
  if (subcommand === "summary") {
931
- const { runMetricsSummary } = await import("./summary-6NCA7PDP.js");
964
+ const { runMetricsSummary } = await import("./summary-JOCABBCO.js");
932
965
  await runMetricsSummary({
933
966
  org,
934
967
  from: cli.flags.from,
@@ -940,7 +973,7 @@ async function main() {
940
973
  break;
941
974
  }
942
975
  if (subcommand === "found") {
943
- const { runMetricsFound } = await import("./found-AOPBSLRD.js");
976
+ const { runMetricsFound } = await import("./found-A5HRTJCJ.js");
944
977
  await runMetricsFound({
945
978
  org,
946
979
  from: cli.flags.from,
@@ -957,7 +990,7 @@ async function main() {
957
990
  if (!by || !["day", "type", "brand"].includes(by)) {
958
991
  throw new Error("metrics breakdown requires --by <day|type|brand>");
959
992
  }
960
- const { runMetricsBreakdown } = await import("./breakdown-EBSACUST.js");
993
+ const { runMetricsBreakdown } = await import("./breakdown-AX6QNTQH.js");
961
994
  await runMetricsBreakdown({
962
995
  org,
963
996
  by,
@@ -977,7 +1010,7 @@ async function main() {
977
1010
  case "reports": {
978
1011
  if (subcommand === "list") {
979
1012
  const org = await resolveOrg();
980
- const { runReportsList } = await import("./list-MWDFCHMJ.js");
1013
+ const { runReportsList } = await import("./list-CVFXTKNX.js");
981
1014
  await runReportsList({
982
1015
  org,
983
1016
  limit: cli.flags.limit,
@@ -993,7 +1026,7 @@ async function main() {
993
1026
  }
994
1027
  if (subcommand === "create") {
995
1028
  const org = await tryResolveOrg();
996
- const { runReportsCreate } = await import("./create-QP3M7EZM.js");
1029
+ const { runReportsCreate } = await import("./create-XTCUNT2C.js");
997
1030
  await runReportsCreate({
998
1031
  org,
999
1032
  title: cli.flags.title,
@@ -1017,7 +1050,7 @@ async function main() {
1017
1050
  }
1018
1051
  case "queues": {
1019
1052
  if (subcommand === "snapshot") {
1020
- const { runQueuesSnapshot } = await import("./snapshot-E3TPZOKT.js");
1053
+ const { runQueuesSnapshot } = await import("./snapshot-4QR4I67P.js");
1021
1054
  await runQueuesSnapshot({
1022
1055
  org: cli.flags.org,
1023
1056
  all: cli.flags.all,
@@ -1033,9 +1066,29 @@ async function main() {
1033
1066
  subcommand ? `Unknown subcommand: queues ${subcommand}${hint ? `. Did you mean "queues ${hint}"?` : ""}` : "Usage: chainpatrol queues snapshot [--org <slug>|--all]"
1034
1067
  );
1035
1068
  }
1069
+ case "orgs": {
1070
+ if (subcommand === "list") {
1071
+ const { runOrgsList } = await import("./list-CGRHTFAS.js");
1072
+ await runOrgsList({
1073
+ query: cli.flags.query,
1074
+ subscriptionStatus: cli.flags.subscriptionStatus,
1075
+ serviceActive: cli.flags.serviceActive,
1076
+ serviceInactive: cli.flags.serviceInactive,
1077
+ serviceAutomated: cli.flags.serviceAutomated,
1078
+ serviceManual: cli.flags.serviceManual,
1079
+ json: jsonMode,
1080
+ outputFormat: cliContext.outputFormat
1081
+ });
1082
+ break;
1083
+ }
1084
+ const hint = subcommand ? suggest(subcommand, ["list"]) : null;
1085
+ throw new Error(
1086
+ subcommand ? `Unknown subcommand: orgs ${subcommand}${hint ? `. Did you mean "orgs ${hint}"?` : ""}` : "Usage: chainpatrol orgs list [--subscription-status <list>] [--service-active <list>] [--service-automated <list>] ..."
1087
+ );
1088
+ }
1036
1089
  case "healthchecks": {
1037
1090
  if (subcommand === "list") {
1038
- const { runHealthchecksList } = await import("./list-UW63DIKX.js");
1091
+ const { runHealthchecksList } = await import("./list-PLZ67PNY.js");
1039
1092
  await runHealthchecksList({
1040
1093
  json: jsonMode,
1041
1094
  outputFormat: cliContext.outputFormat
@@ -1049,7 +1102,7 @@ async function main() {
1049
1102
  thresholds.minResults = cli.flags.minResults;
1050
1103
  if (cli.flags.lookbackHours !== void 0)
1051
1104
  thresholds.lookbackHours = cli.flags.lookbackHours;
1052
- const { runHealthchecksRun } = await import("./run-7THXM7GF.js");
1105
+ const { runHealthchecksRun } = await import("./run-MS5SA5YL.js");
1053
1106
  await runHealthchecksRun({
1054
1107
  org,
1055
1108
  id: action,
@@ -1067,7 +1120,7 @@ async function main() {
1067
1120
  }
1068
1121
  case "presets": {
1069
1122
  if (subcommand === "list") {
1070
- const { runPresetsList } = await import("./list-LN6NOZIJ.js");
1123
+ const { runPresetsList } = await import("./list-EYRN5JYC.js");
1071
1124
  await runPresetsList({ outputFormat: cliContext.outputFormat });
1072
1125
  break;
1073
1126
  }
@@ -1078,7 +1131,7 @@ async function main() {
1078
1131
  );
1079
1132
  }
1080
1133
  const org = await resolveOrg();
1081
- const { runPresetsRun } = await import("./run-MH5RYPWA.js");
1134
+ const { runPresetsRun } = await import("./run-YHDUUP66.js");
1082
1135
  await runPresetsRun({
1083
1136
  presetId: action,
1084
1137
  org,
@@ -1095,12 +1148,12 @@ async function main() {
1095
1148
  case "setup":
1096
1149
  case "install":
1097
1150
  case "i": {
1098
- const { setupSkill } = await import("./setup-skill-2SEVH3M6.js");
1151
+ const { setupSkill } = await import("./setup-skill-BTR2IZ4E.js");
1099
1152
  setupSkill({ json: jsonMode });
1100
1153
  break;
1101
1154
  }
1102
1155
  case "uninstall": {
1103
- const { uninstallSkill } = await import("./setup-skill-2SEVH3M6.js");
1156
+ const { uninstallSkill } = await import("./setup-skill-BTR2IZ4E.js");
1104
1157
  uninstallSkill({ json: jsonMode });
1105
1158
  break;
1106
1159
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-MXUZR2BV.js";
10
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
7
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
7
+ } from "./chunk-LLWKCA3H.js";
8
8
  import {
9
9
  AuthCorruptedError,
10
10
  AuthExpiredError,
@@ -0,0 +1,182 @@
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-LLWKCA3H.js";
12
+ import "./chunk-EEG7T6WT.js";
13
+ import "./chunk-TFCNKBRC.js";
14
+ import "./chunk-U73SABXK.js";
15
+
16
+ // src/commands/orgs/list.ts
17
+ var VALID_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
18
+ "PROSPECT",
19
+ "TRIAL",
20
+ "ACTIVE",
21
+ "INTEGRATION"
22
+ ]);
23
+ var DUAL_SERVICES = ["reporting", "reviewing", "protection", "takedowns"];
24
+ var SINGLE_SERVICES = ["detection", "darkWebMonitoring"];
25
+ var ALL_SERVICES = [...DUAL_SERVICES, ...SINGLE_SERVICES];
26
+ function splitCsv(value) {
27
+ if (!value) return [];
28
+ return value.split(",").map((part) => part.trim()).filter(Boolean);
29
+ }
30
+ function parseSubscriptionStatuses(raw) {
31
+ const parts = splitCsv(raw);
32
+ if (parts.length === 0) return void 0;
33
+ const result = [];
34
+ for (const part of parts) {
35
+ const upper = part.toUpperCase();
36
+ if (!VALID_SUBSCRIPTION_STATUSES.has(upper)) {
37
+ throw new CliExitError(
38
+ `Unknown subscription status: '${part}'. Valid values: ${Array.from(VALID_SUBSCRIPTION_STATUSES).join(", ")}.`,
39
+ ExitCode.USAGE
40
+ );
41
+ }
42
+ result.push(upper);
43
+ }
44
+ return result;
45
+ }
46
+ function parseServiceList(raw, flagName) {
47
+ const parts = splitCsv(raw);
48
+ const result = [];
49
+ for (const part of parts) {
50
+ if (!ALL_SERVICES.includes(part)) {
51
+ throw new CliExitError(
52
+ `Unknown service '${part}' in --${flagName}. Valid services: ${ALL_SERVICES.join(", ")}.`,
53
+ ExitCode.USAGE
54
+ );
55
+ }
56
+ result.push(part);
57
+ }
58
+ return result;
59
+ }
60
+ function requireDualService(service, flagName) {
61
+ if (!DUAL_SERVICES.includes(service)) {
62
+ throw new CliExitError(
63
+ `Service '${service}' does not have an automation toggle and can't be used with --${flagName}. Use --service-active / --service-inactive instead.`,
64
+ ExitCode.USAGE
65
+ );
66
+ }
67
+ return service;
68
+ }
69
+ function buildServicesFilter(options) {
70
+ const filters = {};
71
+ const activeSetBy = /* @__PURE__ */ new Map();
72
+ const automatedSetBy = /* @__PURE__ */ new Map();
73
+ function setActive(service, active, flagName) {
74
+ const previous = activeSetBy.get(service);
75
+ if (previous && previous !== flagName) {
76
+ throw new CliExitError(
77
+ `Conflicting flags for service '${service}': --${previous} and --${flagName} can't both be passed.`,
78
+ ExitCode.USAGE
79
+ );
80
+ }
81
+ activeSetBy.set(service, flagName);
82
+ const existing = filters[service] ?? {};
83
+ filters[service] = { ...existing, active };
84
+ }
85
+ function setAutomated(service, automated, flagName) {
86
+ const previous = automatedSetBy.get(service);
87
+ if (previous && previous !== flagName) {
88
+ throw new CliExitError(
89
+ `Conflicting flags for service '${service}': --${previous} and --${flagName} can't both be passed.`,
90
+ ExitCode.USAGE
91
+ );
92
+ }
93
+ automatedSetBy.set(service, flagName);
94
+ const existing = filters[service] ?? {};
95
+ filters[service] = { ...existing, automated };
96
+ }
97
+ for (const service of parseServiceList(options.serviceActive, "service-active")) {
98
+ setActive(service, true, "service-active");
99
+ }
100
+ for (const service of parseServiceList(options.serviceInactive, "service-inactive")) {
101
+ setActive(service, false, "service-inactive");
102
+ }
103
+ for (const service of parseServiceList(options.serviceAutomated, "service-automated")) {
104
+ setAutomated(
105
+ requireDualService(service, "service-automated"),
106
+ true,
107
+ "service-automated"
108
+ );
109
+ }
110
+ for (const service of parseServiceList(options.serviceManual, "service-manual")) {
111
+ setAutomated(requireDualService(service, "service-manual"), false, "service-manual");
112
+ }
113
+ return Object.keys(filters).length > 0 ? filters : void 0;
114
+ }
115
+ function summarizeServices(org) {
116
+ const parts = [];
117
+ for (const service of DUAL_SERVICES) {
118
+ const entry = org.services[service];
119
+ if (!entry.active) continue;
120
+ parts.push(`${service}=${entry.automated ? "auto" : "manual"}`);
121
+ }
122
+ for (const service of SINGLE_SERVICES) {
123
+ if (org.services[service].active) parts.push(service);
124
+ }
125
+ return parts.length > 0 ? parts.join(" ") : "(no services enabled)";
126
+ }
127
+ async function runOrgsList(options) {
128
+ const client = options.apiClient ?? createApiClient();
129
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
130
+ const input = {
131
+ query: options.query ?? "",
132
+ subscriptionStatus: parseSubscriptionStatuses(options.subscriptionStatus),
133
+ services: buildServicesFilter(options)
134
+ };
135
+ const result = await client.getUserOrgs(input);
136
+ const organizations = result.organizations;
137
+ printOutput({
138
+ outputFormat,
139
+ json: result,
140
+ markdown: [
141
+ `# Organizations (${organizations.length})`,
142
+ "",
143
+ ...organizations.map(
144
+ (org) => `- \`${org.slug}\` \u2014 ${org.name} \u2014 ${org.subscriptionStatus} \u2014 ${summarizeServices(org)}`
145
+ )
146
+ ].join("\n"),
147
+ csv: toCsvRows(
148
+ organizations.map((org) => ({
149
+ slug: org.slug,
150
+ name: org.name,
151
+ subscriptionStatus: org.subscriptionStatus,
152
+ reportingActive: org.services.reporting.active,
153
+ reportingAutomated: org.services.reporting.automated,
154
+ reviewingActive: org.services.reviewing.active,
155
+ reviewingAutomated: org.services.reviewing.automated,
156
+ protectionActive: org.services.protection.active,
157
+ protectionAutomated: org.services.protection.automated,
158
+ takedownsActive: org.services.takedowns.active,
159
+ takedownsAutomated: org.services.takedowns.automated,
160
+ detectionActive: org.services.detection.active,
161
+ darkWebMonitoringActive: org.services.darkWebMonitoring.active
162
+ }))
163
+ ),
164
+ human: () => {
165
+ if (organizations.length === 0) {
166
+ console.log("No organizations matched the filter.");
167
+ return;
168
+ }
169
+ console.log(`Found ${organizations.length} organization(s):`);
170
+ const slugWidth = Math.max(...organizations.map((org) => org.slug.length), 4);
171
+ const nameWidth = Math.max(...organizations.map((org) => org.name.length), 4);
172
+ for (const org of organizations) {
173
+ console.log(
174
+ ` ${org.slug.padEnd(slugWidth)} ${org.name.padEnd(nameWidth)} ${org.subscriptionStatus.padEnd(11)} ${summarizeServices(org)}`
175
+ );
176
+ }
177
+ }
178
+ });
179
+ }
180
+ export {
181
+ runOrgsList
182
+ };
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  PRESETS
3
- } from "./chunk-PIYOWGBZ.js";
3
+ } from "./chunk-AGXMZFUU.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-MXUZR2BV.js";
9
+ import "./chunk-LLWKCA3H.js";
10
10
  import "./chunk-EEG7T6WT.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-MXUZR2BV.js";
7
+ } from "./chunk-LLWKCA3H.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-MXUZR2BV.js";
3
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -27,7 +27,12 @@ function buildPayload(entry, org, thresholds) {
27
27
  "failThreshold",
28
28
  "warnAgeHours",
29
29
  "failAgeHours",
30
- "staleThresholdHours"
30
+ "staleThresholdHours",
31
+ "windowHours",
32
+ "baselineDays",
33
+ "warnMultiplier",
34
+ "failMultiplier",
35
+ "minSpikeCount"
31
36
  ]);
32
37
  for (const [key, value] of Object.entries(thresholds)) {
33
38
  if (allowedKeys.has(key)) {
@@ -150,6 +155,9 @@ async function runHealthchecksRun(options) {
150
155
  if (entry.result.suggestedAction) {
151
156
  lines.push(`- Suggested action: ${entry.result.suggestedAction}`);
152
157
  }
158
+ if (entry.result.appUrl) {
159
+ lines.push(`- View in app: ${entry.result.appUrl}`);
160
+ }
153
161
  return lines.join("\n");
154
162
  }),
155
163
  ...errors.map((entry) => `## ${entry.entry.id} \u2014 ERROR
@@ -194,6 +202,10 @@ async function runHealthchecksRun(options) {
194
202
  console.log(`Suggested action for ${entry.result.id}:`);
195
203
  console.log(` ${entry.result.suggestedAction}`);
196
204
  }
205
+ if (entry.result.appUrl) {
206
+ console.log("");
207
+ console.log(`View in app: ${entry.result.appUrl}`);
208
+ }
197
209
  }
198
210
  }
199
211
  });
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-PIYOWGBZ.js";
4
+ } from "./chunk-AGXMZFUU.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-MXUZR2BV.js";
10
+ import "./chunk-LLWKCA3H.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-F4GU6AII.js";
9
+ } from "./chunk-XOXQPUR6.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
7
+ } from "./chunk-LLWKCA3H.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-MXUZR2BV.js";
11
+ } from "./chunk-LLWKCA3H.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.4.1",
5
+ "version": "0.6.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [