@chainpatrol/cli 0.13.0 → 0.15.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/{breakdown-B6QUGYI6.js → breakdown-27PMFEWQ.js} +1 -1
  3. package/dist/{check-P5JJJNEX.js → check-M5U2CSWK.js} +1 -1
  4. package/dist/{chunk-DJVB73FL.js → chunk-FOTFWUOX.js} +1 -1
  5. package/dist/{chunk-BQDMAEQU.js → chunk-QGKR7EPH.js} +102 -18
  6. package/dist/{chunk-OSRFCVBT.js → chunk-UZDSNWGL.js} +27 -2
  7. package/dist/cli.js +49 -40
  8. package/dist/{configs-update-3ZVXGADV.js → configs-update-YMPRYXKZ.js} +1 -1
  9. package/dist/{create-DFH2G7RH.js → create-DQYNYD6G.js} +1 -1
  10. package/dist/{drift-T74V2C3J.js → drift-T4AF6Y3X.js} +1 -1
  11. package/dist/{found-NXAWEIEK.js → found-XEGMO6YY.js} +1 -1
  12. package/dist/{healthcheck-HZYEIIOR.js → healthcheck-FORGIJWP.js} +1 -1
  13. package/dist/{list-EIVPGMWJ.js → list-63HPS2RE.js} +1 -1
  14. package/dist/{list-WX5G3IOF.js → list-BJMP3QEV.js} +2 -2
  15. package/dist/{list-YRJPN4PI.js → list-COJ5BLQV.js} +1 -1
  16. package/dist/{list-WJQT2XLE.js → list-DVPEMSVH.js} +1 -1
  17. package/dist/{list-ZFPML3ZR.js → list-N4TTNR6N.js} +1 -1
  18. package/dist/{list-23QQYAV5.js → list-OWJLAWMP.js} +1 -1
  19. package/dist/{list-QUFY4IP7.js → list-RMD4NKVB.js} +1 -1
  20. package/dist/{list-PBAXQSL5.js → list-VOWXWXHY.js} +1 -1
  21. package/dist/{list-I7SBGA52.js → list-WPXNCBT3.js} +1 -1
  22. package/dist/{list-YCT6DYDI.js → list-XHBQMP6P.js} +1 -1
  23. package/dist/{list-IYJBKE2F.js → list-ZTMCEG56.js} +1 -1
  24. package/dist/{list-json-T2Z7NV7G.js → list-json-X6QHIRE5.js} +1 -1
  25. package/dist/organization-HD6LRAZP.js +251 -0
  26. package/dist/{run-BHEXWBAS.js → run-BHAH7DUC.js} +2 -2
  27. package/dist/{run-CJGN6TX4.js → run-G5J77GZD.js} +1 -1
  28. package/dist/{run-MSO6IXEV.js → run-V7QJBNLF.js} +1 -1
  29. package/dist/{search-WPSKQGUS.js → search-ME6KFCEV.js} +1 -1
  30. package/dist/{setup-skill-TPMTULHJ.js → setup-skill-ENJSVGLR.js} +1 -1
  31. package/dist/{snapshot-ZWAHEG64.js → snapshot-B6TQCFQ3.js} +1 -1
  32. package/dist/{summary-CV657J5V.js → summary-NYHIQ5DP.js} +1 -1
  33. package/dist/{validate-SM5DD7I2.js → validate-I3YHZDZR.js} +1 -1
  34. package/dist/{whoami-DH5HVLFN.js → whoami-6KU4LOJS.js} +1 -1
  35. package/package.json +1 -1
  36. package/dist/organization-GS5LFIWO.js +0 -88
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e24afdc: `GET /organization/metrics` (and the matching `chainpatrol metrics
8
+ organization --slugs` CLI flag) now accept a list of org slugs and
9
+ return a single rolled-up response: `metrics` becomes the sum across
10
+ the queried orgs, `perOrg` holds the per-org breakdown, and a new
11
+ `averages` field provides `perDay` (total / windowDays) plus
12
+ `perOrgPerDay` (total / numOrgs / windowDays) so callers don't have
13
+ to do the math themselves. The single-org form is unchanged on the
14
+ wire except for the new `averages.perDay` and `scope: { mode, orgs }`
15
+ additions, and `averages.perOrgPerDay` is `null` for single-org
16
+ callers.
17
+
18
+ Lets an agent answer "how many threats did we take down across all
19
+ clients in the last 7 days?" — or "what's our average takedowns per
20
+ customer per day this month?" — in **one** HTTP call with one
21
+ `--include`-narrowed Prisma aggregate per org (run server-side with
22
+ bounded concurrency), instead of the N-round-trip fan-out that was
23
+ previously the only path. `slugs` is capped at 50 per call, mutually
24
+ exclusive with `organizationSlug`, and disallowed with `brandSlug`.
25
+
26
+ Auth: org-scoped API keys remain single-org and are rejected if
27
+ `slugs` contains any foreign org; user API keys (which inherit the
28
+ owning user's memberships) and Bearer sessions can pass any orgs
29
+ the principal has access to. Staff users can query any org, as
30
+ before.
31
+
32
+ The CLI's `metrics organization` command picks up a `--slugs`
33
+ flag, prints totals + per-day / per-org-per-day averages + a
34
+ per-org breakdown in human/markdown/csv output, and surfaces a
35
+ `scope` and `averages` block in the JSON envelope.
36
+
37
+ ## 0.14.0
38
+
39
+ ### Minor Changes
40
+
41
+ - 339729c: Add an `--include` filter to `chainpatrol metrics organization` (backed
42
+ by a new optional `include` field on `GET /organization/metrics`) so
43
+ callers can ask for just the metrics they care about instead of paying
44
+ for the full ~11-aggregate fan-out.
45
+
46
+ `--include` (or `include: [...]` in the JSON body) accepts a comma list
47
+ of any of: `reports`, `newThreats`, `threatsWatchlisted`,
48
+ `takedownsFiled`, `takedownsCompleted`, `domainThreats`,
49
+ `twitterThreats`, `telegramThreats`, `otherThreats`, `blockedByType`,
50
+ `blockedByDay`. The server runs only those Prisma aggregates;
51
+ unrequested fields come back as `null`. Omitting `--include` (or
52
+ passing an empty list) preserves the existing "compute everything"
53
+ behavior.
54
+
55
+ This is the targeted follow-up to the 3-month-default change for the
56
+ same 503 issue: agents that only need takedown counts can now skip the
57
+ nine other counts entirely, keeping each call well inside the platform
58
+ function cap and cheap enough to run repeatedly per org.
59
+
60
+ The response schema's metric fields and `blockedByType`/`blockedByDay`
61
+ arrays are now nullable to express "not included" — TypeScript callers
62
+ of the CLI's `OrganizationMetricsResult` that previously read
63
+ `result.metrics.reports` as `number` will need `?? 0` (or to opt in to
64
+ `--include` and check for `null` explicitly).
65
+
3
66
  ## 0.13.0
4
67
 
5
68
  ### Minor Changes
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-OSRFCVBT.js";
10
+ } from "./chunk-UZDSNWGL.js";
11
11
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -700,24 +700,108 @@ the full customer-facing dashboard slice (reports, new threats,
700
700
  watchlisted, takedowns filed/completed, plus per-type and per-day
701
701
  breakdowns).
702
702
 
703
- #### "Across all orgs" via an API key \u2014 what's actually possible
704
-
705
- A ChainPatrol API key is scoped to a single organization. The
706
- \`/organization/metrics\` endpoint derives the org from the key, so
707
- **there is no way to fetch metrics for multiple orgs with one API key**.
708
- If the user asks for "averages across all orgs":
709
-
710
- 1. If they're authenticated with a user session, list accessible orgs
711
- first (\`chainpatrol --json orgs list\`) and then call
712
- \`metrics organization\` once per org **serially** (concurrent
713
- fan-out across many orgs is what triggers 503/throttling on the
714
- metrics endpoint).
715
- 2. If they're using a single API key, explain that the key only
716
- covers one org \u2014 they need either session auth or one key per
717
- org, not a loop with the same key.
718
- 3. Whatever window you choose, **make it explicit** in your reply
719
- ("last 3 months", "last 7 days", etc) \u2014 never report an average
720
- without naming the window it was computed over.
703
+ #### \`--include\` \u2014 only compute the metrics you actually need
704
+
705
+ \`metrics organization\` runs one Prisma aggregate per requested field.
706
+ By default all 11 are computed in parallel. Pass \`--include\` (or
707
+ \`include: [\u2026]\` in the JSON body) with the comma-separated subset you
708
+ care about to skip the rest \u2014 the unrequested fields come back as
709
+ \`null\` instead of a number, and the server never runs those queries:
710
+
711
+ \`\`\`bash
712
+ # Only takedowns \u2014 one of the cheap shapes
713
+ chainpatrol --json metrics organization --org <slug> \\
714
+ --include takedownsFiled,takedownsCompleted
715
+
716
+ # Time series only, no scalar counts
717
+ chainpatrol --json metrics organization --org <slug> --include blockedByDay
718
+ \`\`\`
719
+
720
+ Allowed values: \`reports\`, \`newThreats\`, \`threatsWatchlisted\`,
721
+ \`takedownsFiled\`, \`takedownsCompleted\`, \`domainThreats\`,
722
+ \`twitterThreats\`, \`telegramThreats\`, \`otherThreats\`,
723
+ \`blockedByType\`, \`blockedByDay\`.
724
+
725
+ Use this whenever the user's question is specific ("how many takedowns
726
+ did we file last month?"). It is the lowest-effort way to make a
727
+ metrics call cheap enough to run repeatedly. Pair it with an explicit
728
+ date window for the best behavior.
729
+
730
+ #### \`--slugs\` \u2014 multi-org rollups in one call (use this for "across all customers/clients")
731
+
732
+ When the user asks for a total or an average **across more than one
733
+ org** \u2014 e.g. "how many takedowns did we complete across all clients
734
+ in the last 7 days?", "average reports per org per day this month",
735
+ "top 5 customers by new threats" \u2014 reach for \`--slugs\` instead of
736
+ looping. The server fans out the per-org aggregates internally,
737
+ sums into totals, computes per-day and per-org-per-day averages,
738
+ and returns a per-org breakdown in a **single HTTP round trip**:
739
+
740
+ \`\`\`bash
741
+ # Total takedowns completed across three customers, last 7 days
742
+ chainpatrol --json metrics organization \\
743
+ --slugs acme,beta,gamma \\
744
+ --include takedownsCompleted \\
745
+ --from 2026-05-12 --to 2026-05-19
746
+ \`\`\`
747
+
748
+ The response shape switches to multi-org:
749
+
750
+ \`\`\`json
751
+ {
752
+ "scope": { "mode": "multi", "orgs": ["acme", "beta", "gamma"] },
753
+ "metrics": { "takedownsCompleted": 117, ... }, // sum across orgs
754
+ "averages": {
755
+ "perDay": { "takedownsCompleted": 16.71, ... }, // total / windowDays
756
+ "perOrgPerDay": { "takedownsCompleted": 5.57, ... }, // total / numOrgs / windowDays
757
+ "windowDays": 7
758
+ },
759
+ "perOrg": { "acme": { "metrics": { ... } }, "beta": { ... }, "gamma": { ... } }
760
+ }
761
+ \`\`\`
762
+
763
+ To answer **"across all of our customers"**, list the orgs first:
764
+
765
+ \`\`\`bash
766
+ chainpatrol --json orgs list --subscription-status ACTIVE # \u2190 get slugs
767
+ chainpatrol --json metrics organization \\
768
+ --slugs <comma-list of all the slugs from above> \\
769
+ --include takedownsCompleted \\
770
+ --from 2026-05-12 --to 2026-05-19
771
+ \`\`\`
772
+
773
+ The endpoint caps \`slugs\` at 50 per call; if you need more, split
774
+ the list and sum the totals client-side. **Do not loop \`metrics
775
+ organization\` once per org** when \`--slugs\` will do \u2014 N HTTP
776
+ round-trips is what made the old code 503.
777
+
778
+ Always pair \`--slugs\` with \`--include\` to narrow the metric set to
779
+ what the user actually asked about. "How many takedowns did we
780
+ complete?" \u2192 \`--include takedownsCompleted\`; "average reports per
781
+ day per customer?" \u2192 \`--include reports\`. Skipping \`--include\` runs
782
+ ~11 aggregates per org which is rarely necessary.
783
+
784
+ #### Auth: which credential can use \`--slugs\`
785
+
786
+ - **Org-scoped API key** (the most common production key): can only
787
+ query its own org. If \`--slugs\` contains anything else the server
788
+ returns 403. Use the \`--org\` form (or omit both) instead.
789
+ - **User API key**: inherits the user's org memberships, so it can
790
+ query any org the user is a member of (or any org for staff
791
+ users). This is the credential you want for cross-org rollups
792
+ from automated agents.
793
+ - **Session auth** (Bearer token, e.g. \`chainpatrol login\`): same
794
+ membership rules as the user. Staff users can query any org.
795
+
796
+ #### Reporting numbers to the user
797
+
798
+ Whatever window and scope you choose, **make both explicit in your
799
+ reply** \u2014 say "averaged across 12 customers over the last 3 months"
800
+ or "summed across all paying orgs for the last 7 days." The same
801
+ prompt phrased slightly differently can pick a different window or
802
+ org set, and the response already echoes
803
+ \`scope.mode\` / \`scope.orgs\` / \`averages.windowDays\` /
804
+ \`range.label\` so you don't have to guess.
721
805
 
722
806
  ### \`presets list | run\` \u2014 Packaged workflows for common jobs
723
807
 
@@ -45,6 +45,19 @@ var TAKEDOWN_SORT_KEYS = [
45
45
  "assigneeId",
46
46
  "brandId"
47
47
  ];
48
+ var ORGANIZATION_METRICS_INCLUDE_FIELDS = [
49
+ "reports",
50
+ "newThreats",
51
+ "threatsWatchlisted",
52
+ "takedownsFiled",
53
+ "takedownsCompleted",
54
+ "domainThreats",
55
+ "twitterThreats",
56
+ "telegramThreats",
57
+ "otherThreats",
58
+ "blockedByType",
59
+ "blockedByDay"
60
+ ];
48
61
  var REQUEST_TIMEOUT_MS = 3e4;
49
62
  function defaultGetCredential() {
50
63
  const envKey = process.env.CHAINPATROL_API_KEY;
@@ -71,7 +84,16 @@ function createApiClient(options) {
71
84
  const headers = buildAuthHeaders();
72
85
  const search = new URLSearchParams();
73
86
  for (const [key, value] of Object.entries(query)) {
74
- if (value !== void 0 && value !== "") {
87
+ if (value === void 0) continue;
88
+ if (Array.isArray(value)) {
89
+ for (const item of value) {
90
+ if (item !== void 0 && item !== "") {
91
+ search.append(key, item);
92
+ }
93
+ }
94
+ continue;
95
+ }
96
+ if (value !== "") {
75
97
  search.set(key, value);
76
98
  }
77
99
  }
@@ -256,9 +278,11 @@ function createApiClient(options) {
256
278
  getOrganizationMetrics(input) {
257
279
  return requestGet("/organization/metrics", {
258
280
  organizationSlug: input.organizationSlug,
281
+ slugs: input.slugs,
259
282
  brandSlug: input.brandSlug,
260
283
  startDate: parseIsoDateString(input.startDate),
261
- endDate: parseIsoDateString(input.endDate)
284
+ endDate: parseIsoDateString(input.endDate),
285
+ include: input.include
262
286
  });
263
287
  },
264
288
  listHealthchecks() {
@@ -378,5 +402,6 @@ export {
378
402
  TAKEDOWN_STATUSES,
379
403
  LIVENESS_STATUSES,
380
404
  TAKEDOWN_SORT_KEYS,
405
+ ORGANIZATION_METRICS_INCLUDE_FIELDS,
381
406
  createApiClient
382
407
  };
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-BQDMAEQU.js";
16
+ } from "./chunk-QGKR7EPH.js";
17
17
  import "./chunk-NQT77IB7.js";
18
18
  import {
19
19
  DateTime
@@ -263,19 +263,23 @@ var HELP = {
263
263
  ]
264
264
  },
265
265
  "metrics organization": {
266
- description: "Get organization metrics via the documented GET /organization/metrics endpoint. Works with both API key auth (org derived from key) and session auth (pass --org). Defaults to the trailing 3 months if no date range is given.",
267
- usage: "chainpatrol metrics organization [--org <slug>] [--from ISO --to ISO|--this-week]",
266
+ description: "Get organization metrics via the documented GET /organization/metrics endpoint. Works with org-scoped API keys (org derived from key, single-org only), user API keys (inherits user permissions, multi-org allowed), and session auth. Defaults to the trailing 3 months if no date range is given. Use --include to skip aggregates the caller doesn't need; use --slugs to query several orgs in one call and get totals + per-org breakdown + per-day / per-org-per-day averages. Fields not included come back as null.",
267
+ usage: "chainpatrol metrics organization [--org <slug>|--slugs <list>] [--from ISO --to ISO|--this-week] [--include <list>]",
268
268
  options: [
269
- "--org <slug> Organization slug (omit when using an API key)",
270
- "--brand-slug <slug> Filter by brand slug (requires brands-metrics flag)",
269
+ "--org <slug> Single organization slug (omit when using an org-scoped API key). Mutually exclusive with --slugs.",
270
+ "--slugs <list> Comma list of org slugs (max 50). Response includes totals across these orgs and a per-org breakdown plus per-day / per-org-per-day averages. Not allowed for org-scoped API keys with foreign slugs.",
271
+ "--brand-slug <slug> Filter by brand slug (requires brands-metrics flag; only valid with a single org)",
271
272
  "--from <iso> Start date (ISO 8601, e.g. 2026-01-01)",
272
273
  "--to <iso> End date (ISO 8601)",
273
- "--this-week Use the current ISO week (overrides default)"
274
+ "--this-week Use the current ISO week (overrides default)",
275
+ "--include <list> Comma list of fields to compute: reports,newThreats,threatsWatchlisted,takedownsFiled,takedownsCompleted,domainThreats,twitterThreats,telegramThreats,otherThreats,blockedByType,blockedByDay (omit for all)"
274
276
  ],
275
277
  examples: [
276
- "chainpatrol metrics organization --org acme # defaults to last 3 months",
278
+ "chainpatrol metrics organization --org acme # defaults to last 3 months, all metrics",
279
+ "chainpatrol metrics organization --org acme --include takedownsFiled,takedownsCompleted",
277
280
  "chainpatrol metrics organization --org acme --from 2026-01-01 --to 2026-02-01",
278
- "CHAINPATROL_API_KEY=... chainpatrol metrics organization --json"
281
+ "chainpatrol metrics organization --slugs acme,beta,gamma --include takedownsCompleted --from 2026-05-12 --to 2026-05-19 # 7-day takedown total + per-day / per-org-per-day averages",
282
+ "CHAINPATROL_API_KEY=... chainpatrol metrics organization --json --include reports"
279
283
  ]
280
284
  },
281
285
  "metrics summary": {
@@ -858,6 +862,8 @@ ${getTopLevelHelp()}
858
862
  to: { type: "string" },
859
863
  thisWeek: { type: "boolean", default: false },
860
864
  by: { type: "string" },
865
+ include: { type: "string" },
866
+ slugs: { type: "string" },
861
867
  brand: { type: "string" },
862
868
  enable: { type: "boolean", default: false },
863
869
  disable: { type: "boolean", default: false },
@@ -1031,12 +1037,12 @@ function parseAttachmentUrls() {
1031
1037
  }
1032
1038
  async function handleConfigsList(org) {
1033
1039
  if (jsonMode) {
1034
- const { listConfigsJson } = await import("./list-json-T2Z7NV7G.js");
1040
+ const { listConfigsJson } = await import("./list-json-X6QHIRE5.js");
1035
1041
  await listConfigsJson({ org });
1036
1042
  return;
1037
1043
  }
1038
1044
  const { render } = await import("ink");
1039
- const { default: ConfigsList } = await import("./list-IYJBKE2F.js");
1045
+ const { default: ConfigsList } = await import("./list-ZTMCEG56.js");
1040
1046
  const { default: React } = await import("react");
1041
1047
  render(React.createElement(ConfigsList, { org }));
1042
1048
  }
@@ -1136,7 +1142,7 @@ async function main() {
1136
1142
  const positionals = cli.input.slice(2);
1137
1143
  const flagAssets = parseAssetInputs();
1138
1144
  const contents = [...positionals, ...flagAssets];
1139
- const { runAssetCheck } = await import("./check-P5JJJNEX.js");
1145
+ const { runAssetCheck } = await import("./check-M5U2CSWK.js");
1140
1146
  await runAssetCheck({
1141
1147
  contents,
1142
1148
  json: jsonMode,
@@ -1146,7 +1152,7 @@ async function main() {
1146
1152
  break;
1147
1153
  }
1148
1154
  if (subcommand === "list") {
1149
- const { runAssetList } = await import("./list-PBAXQSL5.js");
1155
+ const { runAssetList } = await import("./list-VOWXWXHY.js");
1150
1156
  await runAssetList({
1151
1157
  type: cli.flags.type ?? cli.flags.assetType,
1152
1158
  status: cli.flags.assetStatus ?? cli.flags.status,
@@ -1167,7 +1173,7 @@ async function main() {
1167
1173
  case "detections": {
1168
1174
  const org = await resolveOrg();
1169
1175
  if (subcommand === "healthcheck") {
1170
- const { runDetectionsHealthcheck } = await import("./healthcheck-HZYEIIOR.js");
1176
+ const { runDetectionsHealthcheck } = await import("./healthcheck-FORGIJWP.js");
1171
1177
  await runDetectionsHealthcheck({
1172
1178
  org,
1173
1179
  source: cli.flags.source,
@@ -1182,7 +1188,7 @@ async function main() {
1182
1188
  break;
1183
1189
  }
1184
1190
  if (subcommand === "validate") {
1185
- const { runDetectionsValidate } = await import("./validate-SM5DD7I2.js");
1191
+ const { runDetectionsValidate } = await import("./validate-I3YHZDZR.js");
1186
1192
  await runDetectionsValidate({
1187
1193
  org,
1188
1194
  source: cli.flags.source,
@@ -1197,7 +1203,7 @@ async function main() {
1197
1203
  break;
1198
1204
  }
1199
1205
  if (subcommand === "drift") {
1200
- const { runDetectionsDrift } = await import("./drift-T74V2C3J.js");
1206
+ const { runDetectionsDrift } = await import("./drift-T4AF6Y3X.js");
1201
1207
  await runDetectionsDrift({
1202
1208
  org,
1203
1209
  source: cli.flags.source,
@@ -1211,7 +1217,7 @@ async function main() {
1211
1217
  break;
1212
1218
  }
1213
1219
  if (subcommand === "run") {
1214
- const { runDetectionsRun } = await import("./run-MSO6IXEV.js");
1220
+ const { runDetectionsRun } = await import("./run-V7QJBNLF.js");
1215
1221
  await runDetectionsRun({
1216
1222
  org,
1217
1223
  configId: cli.flags.configId,
@@ -1225,7 +1231,7 @@ async function main() {
1225
1231
  break;
1226
1232
  }
1227
1233
  if (subcommand === "list") {
1228
- const { runDetectionsList } = await import("./list-QUFY4IP7.js");
1234
+ const { runDetectionsList } = await import("./list-RMD4NKVB.js");
1229
1235
  await runDetectionsList({
1230
1236
  org,
1231
1237
  cursor: cli.flags.cursor,
@@ -1254,7 +1260,7 @@ async function main() {
1254
1260
  break;
1255
1261
  }
1256
1262
  if (action === "run") {
1257
- const { runDetectionsRun } = await import("./run-MSO6IXEV.js");
1263
+ const { runDetectionsRun } = await import("./run-V7QJBNLF.js");
1258
1264
  await runDetectionsRun({
1259
1265
  org,
1260
1266
  configId: cli.flags.configId,
@@ -1272,7 +1278,7 @@ async function main() {
1272
1278
  throw new Error("detections configs update requires --config-id");
1273
1279
  }
1274
1280
  const configPatch = getConfigPatchFromSetFlags();
1275
- const { runDetectionsConfigsUpdate } = await import("./configs-update-3ZVXGADV.js");
1281
+ const { runDetectionsConfigsUpdate } = await import("./configs-update-YMPRYXKZ.js");
1276
1282
  await runDetectionsConfigsUpdate({
1277
1283
  org,
1278
1284
  configId: cli.flags.configId,
@@ -1305,14 +1311,17 @@ async function main() {
1305
1311
  }
1306
1312
  case "metrics": {
1307
1313
  if (subcommand === "organization" || subcommand === "org") {
1308
- const org2 = await tryResolveOrg();
1309
- const { runMetricsOrganization } = await import("./organization-GS5LFIWO.js");
1314
+ const slugs = parseCsvList(cli.flags.slugs);
1315
+ const org2 = slugs && slugs.length > 0 ? void 0 : await tryResolveOrg();
1316
+ const { runMetricsOrganization } = await import("./organization-HD6LRAZP.js");
1310
1317
  await runMetricsOrganization({
1311
1318
  org: org2,
1319
+ slugs,
1312
1320
  brandSlug: cli.flags.brandSlug,
1313
1321
  from: cli.flags.from,
1314
1322
  to: cli.flags.to,
1315
1323
  thisWeek: cli.flags.thisWeek,
1324
+ include: parseCsvList(cli.flags.include),
1316
1325
  json: jsonMode,
1317
1326
  outputFormat: cliContext.outputFormat
1318
1327
  });
@@ -1320,7 +1329,7 @@ async function main() {
1320
1329
  }
1321
1330
  const org = await resolveOrg();
1322
1331
  if (subcommand === "summary") {
1323
- const { runMetricsSummary } = await import("./summary-CV657J5V.js");
1332
+ const { runMetricsSummary } = await import("./summary-NYHIQ5DP.js");
1324
1333
  await runMetricsSummary({
1325
1334
  org,
1326
1335
  from: cli.flags.from,
@@ -1333,7 +1342,7 @@ async function main() {
1333
1342
  break;
1334
1343
  }
1335
1344
  if (subcommand === "found") {
1336
- const { runMetricsFound } = await import("./found-NXAWEIEK.js");
1345
+ const { runMetricsFound } = await import("./found-XEGMO6YY.js");
1337
1346
  await runMetricsFound({
1338
1347
  org,
1339
1348
  from: cli.flags.from,
@@ -1350,7 +1359,7 @@ async function main() {
1350
1359
  if (!by || !["day", "type", "brand"].includes(by)) {
1351
1360
  throw new Error("metrics breakdown requires --by <day|type|brand>");
1352
1361
  }
1353
- const { runMetricsBreakdown } = await import("./breakdown-B6QUGYI6.js");
1362
+ const { runMetricsBreakdown } = await import("./breakdown-27PMFEWQ.js");
1354
1363
  await runMetricsBreakdown({
1355
1364
  org,
1356
1365
  by,
@@ -1371,7 +1380,7 @@ async function main() {
1371
1380
  case "reports": {
1372
1381
  if (subcommand === "list") {
1373
1382
  const org = await resolveOrg();
1374
- const { runReportsList } = await import("./list-ZFPML3ZR.js");
1383
+ const { runReportsList } = await import("./list-N4TTNR6N.js");
1375
1384
  await runReportsList({
1376
1385
  org,
1377
1386
  limit: cli.flags.limit,
@@ -1399,7 +1408,7 @@ async function main() {
1399
1408
  }
1400
1409
  if (subcommand === "create") {
1401
1410
  const org = await tryResolveOrg();
1402
- const { runReportsCreate } = await import("./create-DFH2G7RH.js");
1411
+ const { runReportsCreate } = await import("./create-DQYNYD6G.js");
1403
1412
  await runReportsCreate({
1404
1413
  org,
1405
1414
  title: cli.flags.title,
@@ -1420,7 +1429,7 @@ async function main() {
1420
1429
  const positionals = cli.input.slice(2);
1421
1430
  const flagAssets = parseAssetInputs();
1422
1431
  const assetContents = [...positionals, ...flagAssets];
1423
- const { runReportsSearch } = await import("./search-WPSKQGUS.js");
1432
+ const { runReportsSearch } = await import("./search-ME6KFCEV.js");
1424
1433
  await runReportsSearch({
1425
1434
  assetContents,
1426
1435
  reportedByCustomer: cli.flags.reportedByCustomer,
@@ -1436,7 +1445,7 @@ async function main() {
1436
1445
  }
1437
1446
  case "threats": {
1438
1447
  if (subcommand === "list") {
1439
- const { runThreatsList } = await import("./list-YCT6DYDI.js");
1448
+ const { runThreatsList } = await import("./list-XHBQMP6P.js");
1440
1449
  await runThreatsList({
1441
1450
  query: cli.flags.query,
1442
1451
  from: cli.flags.from,
@@ -1459,7 +1468,7 @@ async function main() {
1459
1468
  case "takedowns": {
1460
1469
  if (subcommand === "list") {
1461
1470
  const org = await tryResolveOrg();
1462
- const { runTakedownsList } = await import("./list-WJQT2XLE.js");
1471
+ const { runTakedownsList } = await import("./list-DVPEMSVH.js");
1463
1472
  await runTakedownsList({
1464
1473
  org,
1465
1474
  query: cli.flags.query,
@@ -1490,7 +1499,7 @@ async function main() {
1490
1499
  }
1491
1500
  case "queues": {
1492
1501
  if (subcommand === "snapshot") {
1493
- const { runQueuesSnapshot } = await import("./snapshot-ZWAHEG64.js");
1502
+ const { runQueuesSnapshot } = await import("./snapshot-B6TQCFQ3.js");
1494
1503
  await runQueuesSnapshot({
1495
1504
  org: cli.flags.org,
1496
1505
  all: cli.flags.all,
@@ -1508,7 +1517,7 @@ async function main() {
1508
1517
  }
1509
1518
  case "orgs": {
1510
1519
  if (subcommand === "list") {
1511
- const { runOrgsList } = await import("./list-YRJPN4PI.js");
1520
+ const { runOrgsList } = await import("./list-COJ5BLQV.js");
1512
1521
  await runOrgsList({
1513
1522
  query: cli.flags.query,
1514
1523
  subscriptionStatus: cli.flags.subscriptionStatus,
@@ -1523,7 +1532,7 @@ async function main() {
1523
1532
  }
1524
1533
  if (subcommand === "assets") {
1525
1534
  if (action === "list") {
1526
- const { runOrgsAssetsList } = await import("./list-EIVPGMWJ.js");
1535
+ const { runOrgsAssetsList } = await import("./list-63HPS2RE.js");
1527
1536
  await runOrgsAssetsList({
1528
1537
  type: cli.flags.type ?? cli.flags.assetType,
1529
1538
  groupId: cli.flags.groupId,
@@ -1539,7 +1548,7 @@ async function main() {
1539
1548
  }
1540
1549
  if (subcommand === "asset-groups") {
1541
1550
  if (action === "list") {
1542
- const { runOrgsAssetGroupsList } = await import("./list-I7SBGA52.js");
1551
+ const { runOrgsAssetGroupsList } = await import("./list-WPXNCBT3.js");
1543
1552
  await runOrgsAssetGroupsList({
1544
1553
  json: jsonMode,
1545
1554
  outputFormat: cliContext.outputFormat
@@ -1554,7 +1563,7 @@ async function main() {
1554
1563
  );
1555
1564
  }
1556
1565
  case "whoami": {
1557
- const { runWhoami } = await import("./whoami-DH5HVLFN.js");
1566
+ const { runWhoami } = await import("./whoami-6KU4LOJS.js");
1558
1567
  await runWhoami({
1559
1568
  json: jsonMode,
1560
1569
  outputFormat: cliContext.outputFormat
@@ -1563,7 +1572,7 @@ async function main() {
1563
1572
  }
1564
1573
  case "healthchecks": {
1565
1574
  if (subcommand === "list") {
1566
- const { runHealthchecksList } = await import("./list-23QQYAV5.js");
1575
+ const { runHealthchecksList } = await import("./list-OWJLAWMP.js");
1567
1576
  await runHealthchecksList({
1568
1577
  json: jsonMode,
1569
1578
  outputFormat: cliContext.outputFormat
@@ -1577,7 +1586,7 @@ async function main() {
1577
1586
  thresholds.minResults = cli.flags.minResults;
1578
1587
  if (cli.flags.lookbackHours !== void 0)
1579
1588
  thresholds.lookbackHours = cli.flags.lookbackHours;
1580
- const { runHealthchecksRun } = await import("./run-CJGN6TX4.js");
1589
+ const { runHealthchecksRun } = await import("./run-G5J77GZD.js");
1581
1590
  await runHealthchecksRun({
1582
1591
  org,
1583
1592
  id: action,
@@ -1595,7 +1604,7 @@ async function main() {
1595
1604
  }
1596
1605
  case "presets": {
1597
1606
  if (subcommand === "list") {
1598
- const { runPresetsList } = await import("./list-WX5G3IOF.js");
1607
+ const { runPresetsList } = await import("./list-BJMP3QEV.js");
1599
1608
  await runPresetsList({ outputFormat: cliContext.outputFormat });
1600
1609
  break;
1601
1610
  }
@@ -1606,7 +1615,7 @@ async function main() {
1606
1615
  );
1607
1616
  }
1608
1617
  const org = await resolveOrg();
1609
- const { runPresetsRun } = await import("./run-BHEXWBAS.js");
1618
+ const { runPresetsRun } = await import("./run-BHAH7DUC.js");
1610
1619
  await runPresetsRun({
1611
1620
  presetId: action,
1612
1621
  org,
@@ -1623,12 +1632,12 @@ async function main() {
1623
1632
  case "setup":
1624
1633
  case "install":
1625
1634
  case "i": {
1626
- const { setupSkill } = await import("./setup-skill-TPMTULHJ.js");
1635
+ const { setupSkill } = await import("./setup-skill-ENJSVGLR.js");
1627
1636
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1628
1637
  break;
1629
1638
  }
1630
1639
  case "uninstall": {
1631
- const { uninstallSkill } = await import("./setup-skill-TPMTULHJ.js");
1640
+ const { uninstallSkill } = await import("./setup-skill-ENJSVGLR.js");
1632
1641
  uninstallSkill({ json: jsonMode });
1633
1642
  break;
1634
1643
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-OSRFCVBT.js";
10
+ } from "./chunk-UZDSNWGL.js";
11
11
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-OSRFCVBT.js";
10
+ } from "./chunk-UZDSNWGL.js";
11
11
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-DJVB73FL.js";
3
+ } from "./chunk-FOTFWUOX.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-OSRFCVBT.js";
9
+ import "./chunk-UZDSNWGL.js";
10
10
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -11,7 +11,7 @@ import {
11
11
  TAKEDOWN_SORT_KEYS,
12
12
  TAKEDOWN_STATUSES,
13
13
  createApiClient
14
- } from "./chunk-OSRFCVBT.js";
14
+ } from "./chunk-UZDSNWGL.js";
15
15
  import "./chunk-EGWK6SRQ.js";
16
16
  import "./chunk-TFCNKBRC.js";
17
17
  import "./chunk-U73SABXK.js";
@@ -10,7 +10,7 @@ import {
10
10
  REPORT_REVIEW_DECISIONS,
11
11
  REPORT_STATUSES,
12
12
  createApiClient
13
- } from "./chunk-OSRFCVBT.js";
13
+ } from "./chunk-UZDSNWGL.js";
14
14
  import "./chunk-EGWK6SRQ.js";
15
15
  import "./chunk-TFCNKBRC.js";
16
16
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-OSRFCVBT.js";
7
+ } from "./chunk-UZDSNWGL.js";
8
8
  import "./chunk-EGWK6SRQ.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -12,7 +12,7 @@ import {
12
12
  LIVENESS_FILTER_VALUES,
13
13
  WATCHLIST_FILTER_VALUES,
14
14
  createApiClient
15
- } from "./chunk-OSRFCVBT.js";
15
+ } from "./chunk-UZDSNWGL.js";
16
16
  import "./chunk-EGWK6SRQ.js";
17
17
  import "./chunk-TFCNKBRC.js";
18
18
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
7
+ } from "./chunk-UZDSNWGL.js";
8
8
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
7
+ } from "./chunk-UZDSNWGL.js";
8
8
  import {
9
9
  AuthCorruptedError,
10
10
  AuthExpiredError,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-OSRFCVBT.js";
3
+ } from "./chunk-UZDSNWGL.js";
4
4
  import "./chunk-EGWK6SRQ.js";
5
5
  import "./chunk-TFCNKBRC.js";
6
6
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,251 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
4
+ import {
5
+ printOutput,
6
+ toCsvRows
7
+ } from "./chunk-VFT3TD3E.js";
8
+ import {
9
+ ORGANIZATION_METRICS_INCLUDE_FIELDS,
10
+ createApiClient
11
+ } from "./chunk-UZDSNWGL.js";
12
+ import "./chunk-EGWK6SRQ.js";
13
+ import "./chunk-TFCNKBRC.js";
14
+ import "./chunk-U73SABXK.js";
15
+
16
+ // src/commands/metrics/organization.ts
17
+ var INCLUDE_VALUES = new Set(ORGANIZATION_METRICS_INCLUDE_FIELDS);
18
+ var METRIC_LABELS = [
19
+ ["reports", "Reports"],
20
+ ["newThreats", "New threats"],
21
+ ["threatsWatchlisted", "Watchlisted threats"],
22
+ ["takedownsFiled", "Takedowns filed"],
23
+ ["takedownsCompleted", "Takedowns completed"],
24
+ ["domainThreats", "Domain threats"],
25
+ ["twitterThreats", "Twitter threats"],
26
+ ["telegramThreats", "Telegram threats"],
27
+ ["otherThreats", "Other threats"]
28
+ ];
29
+ function parseIncludeList(raw) {
30
+ if (!raw || raw.length === 0) return void 0;
31
+ const fields = [];
32
+ for (const value of raw) {
33
+ if (!INCLUDE_VALUES.has(value)) {
34
+ throw new Error(
35
+ `Unknown --include value: '${value}'. Allowed: ${ORGANIZATION_METRICS_INCLUDE_FIELDS.join(", ")}`
36
+ );
37
+ }
38
+ fields.push(value);
39
+ }
40
+ return fields;
41
+ }
42
+ function formatAverage(value) {
43
+ if (value === null) return "n/a";
44
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
45
+ }
46
+ function populatedMetricRows(metrics, format = (v) => String(v)) {
47
+ return METRIC_LABELS.filter(([key]) => metrics[key] !== null).map(([key, label]) => [
48
+ label,
49
+ format(metrics[key])
50
+ ]);
51
+ }
52
+ async function runMetricsOrganization(options) {
53
+ if (options.org && options.slugs && options.slugs.length > 0) {
54
+ throw new Error("Pass either --org or --slugs, not both.");
55
+ }
56
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
57
+ const client = options.apiClient ?? createApiClient();
58
+ const range = resolveDateRange({
59
+ from: options.from,
60
+ to: options.to,
61
+ thisWeek: options.thisWeek
62
+ });
63
+ const include = parseIncludeList(options.include);
64
+ const input = {
65
+ organizationSlug: options.org,
66
+ slugs: options.slugs && options.slugs.length > 0 ? options.slugs : void 0,
67
+ brandSlug: options.brandSlug,
68
+ startDate: range.startDate,
69
+ endDate: range.endDate,
70
+ include
71
+ };
72
+ const result = await client.getOrganizationMetrics(input);
73
+ const isMulti = result.scope.mode === "multi";
74
+ const scopeLabel = isMulti ? `${result.scope.orgs.length} orgs (${result.scope.orgs.join(", ")})` : result.scope.orgs[0] ?? options.org ?? "your organization";
75
+ const includeLabel = include ? ` (include: ${include.join(", ")})` : "";
76
+ const totalsRows = populatedMetricRows(result.metrics);
77
+ const perDayRows = result.averages.perDay ? populatedMetricRows(result.averages.perDay, formatAverage) : [];
78
+ const perOrgPerDayRows = result.averages.perOrgPerDay ? populatedMetricRows(result.averages.perOrgPerDay, formatAverage) : [];
79
+ printOutput({
80
+ outputFormat,
81
+ json: { ...result, range, include: include ?? null },
82
+ markdown: buildMarkdown({
83
+ scopeLabel,
84
+ rangeLabel: range.label,
85
+ includeLabel,
86
+ isMulti,
87
+ range,
88
+ result,
89
+ totalsRows,
90
+ perDayRows,
91
+ perOrgPerDayRows
92
+ }),
93
+ csv: toCsvRows(buildCsvRows({ result, range, isMulti })),
94
+ human: () => {
95
+ const headline = isMulti ? `Multi-org metrics \u2014 ${scopeLabel} \u2014 ${range.label}${includeLabel}` : `Organization metrics for ${scopeLabel} \u2014 ${range.label}${includeLabel}`;
96
+ console.log(headline);
97
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
98
+ const totalsHeader = isMulti ? "Totals (sum across orgs):" : "Totals:";
99
+ console.log(totalsHeader);
100
+ for (const [name, value] of totalsRows) {
101
+ console.log(` ${name}: ${value}`);
102
+ }
103
+ if (result.averages.windowDays !== null && perDayRows.length > 0) {
104
+ console.log("");
105
+ console.log(`Averages per day (window = ${result.averages.windowDays} days):`);
106
+ for (const [name, value] of perDayRows) {
107
+ console.log(` ${name}: ${value}`);
108
+ }
109
+ }
110
+ if (perOrgPerDayRows.length > 0) {
111
+ console.log("");
112
+ console.log(`Averages per org per day:`);
113
+ for (const [name, value] of perOrgPerDayRows) {
114
+ console.log(` ${name}: ${value}`);
115
+ }
116
+ }
117
+ if (isMulti && result.perOrg) {
118
+ console.log("");
119
+ console.log("Per-org breakdown:");
120
+ for (const slug of result.scope.orgs) {
121
+ const entry = result.perOrg[slug];
122
+ if (!entry) continue;
123
+ const rows = populatedMetricRows(entry.metrics);
124
+ if (rows.length === 0) continue;
125
+ console.log(` ${slug}:`);
126
+ for (const [name, value] of rows) {
127
+ console.log(` ${name}: ${value}`);
128
+ }
129
+ }
130
+ }
131
+ if (!isMulti && result.blockedByDay && result.blockedByDay.length > 0) {
132
+ console.log("");
133
+ console.log(`Blocked by day (${result.blockedByDay.length} entries):`);
134
+ for (const entry of result.blockedByDay) {
135
+ console.log(` ${entry.date}: ${entry.count}`);
136
+ }
137
+ }
138
+ }
139
+ });
140
+ }
141
+ function buildMarkdown(args) {
142
+ const {
143
+ scopeLabel,
144
+ rangeLabel,
145
+ includeLabel,
146
+ isMulti,
147
+ range,
148
+ result,
149
+ totalsRows,
150
+ perDayRows,
151
+ perOrgPerDayRows
152
+ } = args;
153
+ const lines = [];
154
+ const heading = isMulti ? "Multi-Org Metrics" : "Organization Metrics";
155
+ lines.push(`# ${heading} (${scopeLabel}) \u2014 ${rangeLabel}${includeLabel}`);
156
+ lines.push("");
157
+ lines.push(`- Range: ${range.startDate} \u2192 ${range.endDate}`);
158
+ lines.push("");
159
+ lines.push(isMulti ? `## Totals (sum across orgs)` : `## Totals`);
160
+ for (const [name, value] of totalsRows) {
161
+ lines.push(`- ${name}: ${value}`);
162
+ }
163
+ if (result.averages.windowDays !== null && perDayRows.length > 0) {
164
+ lines.push("");
165
+ lines.push(`## Averages per day (window = ${result.averages.windowDays} days)`);
166
+ for (const [name, value] of perDayRows) {
167
+ lines.push(`- ${name}: ${value}`);
168
+ }
169
+ }
170
+ if (perOrgPerDayRows.length > 0) {
171
+ lines.push("");
172
+ lines.push(`## Averages per org per day`);
173
+ for (const [name, value] of perOrgPerDayRows) {
174
+ lines.push(`- ${name}: ${value}`);
175
+ }
176
+ }
177
+ if (isMulti && result.perOrg) {
178
+ lines.push("");
179
+ lines.push(`## Per-org breakdown`);
180
+ for (const slug of result.scope.orgs) {
181
+ const entry = result.perOrg[slug];
182
+ if (!entry) continue;
183
+ const rows = populatedMetricRows(entry.metrics);
184
+ if (rows.length === 0) continue;
185
+ lines.push(`### ${slug}`);
186
+ for (const [name, value] of rows) {
187
+ lines.push(`- ${name}: ${value}`);
188
+ }
189
+ }
190
+ }
191
+ return lines.join("\n");
192
+ }
193
+ function buildCsvRows({
194
+ result,
195
+ range,
196
+ isMulti
197
+ }) {
198
+ const baseFields = (metrics) => ({
199
+ reports: metrics.reports,
200
+ newThreats: metrics.newThreats,
201
+ threatsWatchlisted: metrics.threatsWatchlisted,
202
+ takedownsFiled: metrics.takedownsFiled,
203
+ takedownsCompleted: metrics.takedownsCompleted,
204
+ domainThreats: metrics.domainThreats,
205
+ twitterThreats: metrics.twitterThreats,
206
+ telegramThreats: metrics.telegramThreats,
207
+ otherThreats: metrics.otherThreats
208
+ });
209
+ if (!isMulti) {
210
+ return [
211
+ {
212
+ scope: "single",
213
+ org: result.scope.orgs[0] ?? "",
214
+ rangeLabel: range.label,
215
+ startDate: range.startDate,
216
+ endDate: range.endDate,
217
+ windowDays: result.averages.windowDays,
218
+ ...baseFields(result.metrics)
219
+ }
220
+ ];
221
+ }
222
+ const rows = [];
223
+ rows.push({
224
+ scope: "totals",
225
+ org: "",
226
+ rangeLabel: range.label,
227
+ startDate: range.startDate,
228
+ endDate: range.endDate,
229
+ windowDays: result.averages.windowDays,
230
+ ...baseFields(result.metrics)
231
+ });
232
+ if (result.perOrg) {
233
+ for (const slug of result.scope.orgs) {
234
+ const entry = result.perOrg[slug];
235
+ if (!entry) continue;
236
+ rows.push({
237
+ scope: "per-org",
238
+ org: slug,
239
+ rangeLabel: range.label,
240
+ startDate: range.startDate,
241
+ endDate: range.endDate,
242
+ windowDays: result.averages.windowDays,
243
+ ...baseFields(entry.metrics)
244
+ });
245
+ }
246
+ }
247
+ return rows;
248
+ }
249
+ export {
250
+ runMetricsOrganization
251
+ };
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-DJVB73FL.js";
4
+ } from "./chunk-FOTFWUOX.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-OSRFCVBT.js";
10
+ import "./chunk-UZDSNWGL.js";
11
11
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-BQDMAEQU.js";
9
+ } from "./chunk-QGKR7EPH.js";
10
10
  import "./chunk-NQT77IB7.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-OSRFCVBT.js";
10
+ } from "./chunk-UZDSNWGL.js";
11
11
  import "./chunk-EGWK6SRQ.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-OSRFCVBT.js";
11
+ } from "./chunk-UZDSNWGL.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-VFT3TD3E.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-OSRFCVBT.js";
6
+ } from "./chunk-UZDSNWGL.js";
7
7
  import "./chunk-EGWK6SRQ.js";
8
8
  import "./chunk-TFCNKBRC.js";
9
9
  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.13.0",
5
+ "version": "0.15.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -1,88 +0,0 @@
1
- import {
2
- resolveDateRange
3
- } from "./chunk-S37VFIZY.js";
4
- import {
5
- printOutput,
6
- toCsvRows
7
- } from "./chunk-VFT3TD3E.js";
8
- import {
9
- createApiClient
10
- } from "./chunk-OSRFCVBT.js";
11
- import "./chunk-EGWK6SRQ.js";
12
- import "./chunk-TFCNKBRC.js";
13
- import "./chunk-U73SABXK.js";
14
-
15
- // src/commands/metrics/organization.ts
16
- async function runMetricsOrganization(options) {
17
- const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
18
- const client = options.apiClient ?? createApiClient();
19
- const range = resolveDateRange({
20
- from: options.from,
21
- to: options.to,
22
- thisWeek: options.thisWeek
23
- });
24
- const input = {
25
- organizationSlug: options.org,
26
- brandSlug: options.brandSlug,
27
- startDate: range.startDate,
28
- endDate: range.endDate
29
- };
30
- const result = await client.getOrganizationMetrics(input);
31
- const label = options.org ?? "your organization";
32
- printOutput({
33
- outputFormat,
34
- json: { ...result, range },
35
- markdown: [
36
- `# Organization Metrics (${label}) \u2014 ${range.label}`,
37
- "",
38
- `- Range: ${range.startDate} \u2192 ${range.endDate}`,
39
- `- Reports: ${result.metrics.reports}`,
40
- `- New threats: ${result.metrics.newThreats}`,
41
- `- Watchlisted threats: ${result.metrics.threatsWatchlisted}`,
42
- `- Takedowns filed: ${result.metrics.takedownsFiled}`,
43
- `- Takedowns completed: ${result.metrics.takedownsCompleted}`,
44
- `- Domain threats: ${result.metrics.domainThreats}`,
45
- `- Twitter threats: ${result.metrics.twitterThreats}`,
46
- `- Telegram threats: ${result.metrics.telegramThreats}`,
47
- `- Other threats: ${result.metrics.otherThreats}`
48
- ].join("\n"),
49
- csv: toCsvRows([
50
- {
51
- rangeLabel: range.label,
52
- startDate: range.startDate,
53
- endDate: range.endDate,
54
- reports: result.metrics.reports,
55
- newThreats: result.metrics.newThreats,
56
- threatsWatchlisted: result.metrics.threatsWatchlisted,
57
- takedownsFiled: result.metrics.takedownsFiled,
58
- takedownsCompleted: result.metrics.takedownsCompleted,
59
- domainThreats: result.metrics.domainThreats,
60
- twitterThreats: result.metrics.twitterThreats,
61
- telegramThreats: result.metrics.telegramThreats,
62
- otherThreats: result.metrics.otherThreats
63
- }
64
- ]),
65
- human: () => {
66
- console.log(`Organization metrics for ${label} \u2014 ${range.label}`);
67
- console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
68
- console.log(`Reports: ${result.metrics.reports}`);
69
- console.log(`New threats: ${result.metrics.newThreats}`);
70
- console.log(`Watchlisted threats: ${result.metrics.threatsWatchlisted}`);
71
- console.log(`Takedowns filed: ${result.metrics.takedownsFiled}`);
72
- console.log(`Takedowns completed: ${result.metrics.takedownsCompleted}`);
73
- console.log(
74
- `Threat breakdown: domains=${result.metrics.domainThreats}, twitter=${result.metrics.twitterThreats}, telegram=${result.metrics.telegramThreats}, other=${result.metrics.otherThreats}`
75
- );
76
- if (result.blockedByDay.length > 0) {
77
- console.log("");
78
- console.log(`Blocked by day (${result.blockedByDay.length} entries):`);
79
- for (const entry of result.blockedByDay) {
80
- console.log(` ${entry.date}: ${entry.count}`);
81
- }
82
- }
83
- }
84
- });
85
- }
86
- export {
87
- runMetricsOrganization
88
- };