@chainpatrol/cli 0.12.0 → 0.14.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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/{breakdown-KCTKQNDY.js → breakdown-K5Q6TDKE.js} +21 -6
  3. package/dist/{check-P5JJJNEX.js → check-NJH7BUIM.js} +1 -1
  4. package/dist/{chunk-W42ITB5J.js → chunk-6X5BFFXA.js} +77 -5
  5. package/dist/{chunk-DJVB73FL.js → chunk-BIKTBRER.js} +1 -1
  6. package/dist/chunk-S37VFIZY.js +66 -0
  7. package/dist/{chunk-OSRFCVBT.js → chunk-YZEMTNCX.js} +26 -2
  8. package/dist/cli.js +53 -43
  9. package/dist/{configs-update-3ZVXGADV.js → configs-update-ON67NMXA.js} +1 -1
  10. package/dist/{create-DFH2G7RH.js → create-W5S2ZPVC.js} +1 -1
  11. package/dist/{drift-T74V2C3J.js → drift-ZOLDE24A.js} +1 -1
  12. package/dist/found-M5MX4VXK.js +59 -0
  13. package/dist/{healthcheck-HZYEIIOR.js → healthcheck-PFYP6PNW.js} +1 -1
  14. package/dist/{list-PBAXQSL5.js → list-5TMGSHFC.js} +1 -1
  15. package/dist/{list-QUFY4IP7.js → list-AHWEE3YZ.js} +1 -1
  16. package/dist/{list-IYJBKE2F.js → list-B5KLEFHA.js} +1 -1
  17. package/dist/{list-ZFPML3ZR.js → list-FV5IYCG4.js} +1 -1
  18. package/dist/{list-23QQYAV5.js → list-GKC7PEU5.js} +1 -1
  19. package/dist/{list-YCT6DYDI.js → list-HGYBEYOV.js} +1 -1
  20. package/dist/{list-YRJPN4PI.js → list-ISP5JL4M.js} +1 -1
  21. package/dist/{list-WX5G3IOF.js → list-LNNIEVB4.js} +2 -2
  22. package/dist/{list-WJQT2XLE.js → list-TUMVDWIZ.js} +1 -1
  23. package/dist/{list-I7SBGA52.js → list-VP5IY2SZ.js} +1 -1
  24. package/dist/{list-EIVPGMWJ.js → list-Z7RJWZH5.js} +1 -1
  25. package/dist/{list-json-T2Z7NV7G.js → list-json-QC6X25LI.js} +1 -1
  26. package/dist/organization-VSQQAGBS.js +105 -0
  27. package/dist/{run-CJGN6TX4.js → run-HRFJORDS.js} +1 -1
  28. package/dist/{run-BHEXWBAS.js → run-KDNQIR5E.js} +2 -2
  29. package/dist/{run-MSO6IXEV.js → run-VHFXOCGS.js} +1 -1
  30. package/dist/{search-WPSKQGUS.js → search-NOSQW75S.js} +1 -1
  31. package/dist/{setup-skill-FA4STSGA.js → setup-skill-Q6PCFIDE.js} +1 -1
  32. package/dist/{snapshot-ZWAHEG64.js → snapshot-55UV26NQ.js} +1 -1
  33. package/dist/{summary-Y2LG6UJM.js → summary-QUCLZYX6.js} +19 -6
  34. package/dist/{validate-SM5DD7I2.js → validate-TPG47SYS.js} +1 -1
  35. package/dist/{whoami-DH5HVLFN.js → whoami-Y3ZJGN4O.js} +1 -1
  36. package/package.json +1 -1
  37. package/dist/found-EVXJZGYX.js +0 -95
  38. package/dist/organization-IIIOSTHF.js +0 -75
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 339729c: Add an `--include` filter to `chainpatrol metrics organization` (backed
8
+ by a new optional `include` field on `GET /organization/metrics`) so
9
+ callers can ask for just the metrics they care about instead of paying
10
+ for the full ~11-aggregate fan-out.
11
+
12
+ `--include` (or `include: [...]` in the JSON body) accepts a comma list
13
+ of any of: `reports`, `newThreats`, `threatsWatchlisted`,
14
+ `takedownsFiled`, `takedownsCompleted`, `domainThreats`,
15
+ `twitterThreats`, `telegramThreats`, `otherThreats`, `blockedByType`,
16
+ `blockedByDay`. The server runs only those Prisma aggregates;
17
+ unrequested fields come back as `null`. Omitting `--include` (or
18
+ passing an empty list) preserves the existing "compute everything"
19
+ behavior.
20
+
21
+ This is the targeted follow-up to the 3-month-default change for the
22
+ same 503 issue: agents that only need takedown counts can now skip the
23
+ nine other counts entirely, keeping each call well inside the platform
24
+ function cap and cheap enough to run repeatedly per org.
25
+
26
+ The response schema's metric fields and `blockedByType`/`blockedByDay`
27
+ arrays are now nullable to express "not included" — TypeScript callers
28
+ of the CLI's `OrganizationMetricsResult` that previously read
29
+ `result.metrics.reports` as `number` will need `?? 0` (or to opt in to
30
+ `--include` and check for `null` explicitly).
31
+
32
+ ## 0.13.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 7cc2783: Default every `chainpatrol metrics …` subcommand
37
+ (`summary` / `found` / `breakdown` / `organization`) to a trailing
38
+ **3-month** window when no `--from`/`--to`/`--this-week` flag is
39
+ supplied, and echo the resolved range in every output format
40
+ (human, markdown, csv, json) so consumers can see exactly what
41
+ window the numbers describe.
42
+
43
+ Previously, omitting the date flags sent `undefined` for `startDate`
44
+ / `endDate`, which caused the metrics handler to fan out ~15 parallel
45
+ unbounded Prisma aggregates per call. On large orgs that routinely
46
+ exceeded the platform's 30s function cap and surfaced to agents as
47
+ flaky `503` responses. The new default keeps the queries bounded
48
+ and produces a stable, labelled result.
49
+
50
+ `--this-week` is now also wired through to `metrics summary`,
51
+ `metrics breakdown`, and `metrics organization` (it was already
52
+ supported on `metrics found`).
53
+
54
+ JSON output now includes a `range: { startDate, endDate, label }`
55
+ field on every metrics subcommand. The label is one of
56
+ `"last 3 months"` (default), `"this week"`, or
57
+ `"custom range (<startIso> → <endIso>)"`.
58
+
3
59
  ## 0.12.0
4
60
 
5
61
  ### Minor Changes
@@ -1,31 +1,40 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
1
4
  import {
2
5
  printOutput,
3
6
  toCsvRows
4
7
  } from "./chunk-VFT3TD3E.js";
5
8
  import {
6
9
  createApiClient
7
- } from "./chunk-OSRFCVBT.js";
10
+ } from "./chunk-YZEMTNCX.js";
8
11
  import "./chunk-EGWK6SRQ.js";
9
12
  import "./chunk-TFCNKBRC.js";
10
13
  import "./chunk-U73SABXK.js";
11
14
 
12
15
  // src/commands/metrics/breakdown.ts
13
16
  async function runMetricsBreakdown(options) {
17
+ const range = resolveDateRange({
18
+ from: options.from,
19
+ to: options.to,
20
+ thisWeek: options.thisWeek
21
+ });
14
22
  const client = options.apiClient ?? createApiClient();
15
23
  const result = await client.getMetricsBreakdown({
16
24
  slug: options.org,
17
25
  by: options.by,
18
- startDate: options.from,
19
- endDate: options.to,
26
+ startDate: range.startDate,
27
+ endDate: range.endDate,
20
28
  brandIds: options.brandIds
21
29
  });
22
30
  const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
23
31
  printOutput({
24
32
  outputFormat,
25
- json: result,
33
+ json: { ...result, range },
26
34
  markdown: [
27
- `# Metrics Breakdown (${options.org})`,
35
+ `# Metrics Breakdown (${options.org}) \u2014 ${range.label}`,
28
36
  "",
37
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`,
29
38
  `- Grouped by: ${result.by}`,
30
39
  "",
31
40
  ...result.points.map((point) => {
@@ -40,6 +49,9 @@ async function runMetricsBreakdown(options) {
40
49
  ].join("\n"),
41
50
  csv: toCsvRows(
42
51
  result.points.map((point) => ({
52
+ rangeLabel: range.label,
53
+ startDate: range.startDate,
54
+ endDate: range.endDate,
43
55
  key: point.key,
44
56
  count: point.count,
45
57
  date: point.date ?? null,
@@ -50,7 +62,10 @@ async function runMetricsBreakdown(options) {
50
62
  }))
51
63
  ),
52
64
  human: () => {
53
- console.log(`Metrics breakdown for ${options.org} by ${result.by}`);
65
+ console.log(
66
+ `Metrics breakdown for ${options.org} by ${result.by} \u2014 ${range.label}`
67
+ );
68
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
54
69
  for (const point of result.points) {
55
70
  if (result.by === "day") {
56
71
  console.log(`${point.date ?? point.key}: ${point.count}`);
@@ -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-YZEMTNCX.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -659,20 +659,92 @@ takedowns?", reach for \`orgs list\` \u2014 it's the only command that exposes
659
659
  service flags across multiple orgs in one call. Run it in \`--json\` mode
660
660
  and summarize patterns by service or by subscription tier.
661
661
 
662
- ### \`metrics summary | found | breakdown\` \u2014 Org metrics for spike/drop analysis
662
+ ### \`metrics summary | found | breakdown | organization\` \u2014 Org metrics for spike/drop analysis
663
663
 
664
664
  \`\`\`bash
665
- chainpatrol --json metrics summary --org <slug> --this-week
666
- chainpatrol --json metrics breakdown --org <slug> --by day --this-week
667
- chainpatrol --json metrics found --org <slug> --from <YYYY-MM-DD> --to <YYYY-MM-DD>
665
+ chainpatrol --json metrics summary --org <slug> # defaults to last 3 months
666
+ chainpatrol --json metrics summary --org <slug> --this-week
667
+ chainpatrol --json metrics breakdown --org <slug> --by day --this-week
668
+ chainpatrol --json metrics found --org <slug> --from <YYYY-MM-DD> --to <YYYY-MM-DD>
669
+ chainpatrol --json metrics organization --org <slug> # defaults to last 3 months
668
670
  \`\`\`
669
671
 
672
+ **Always operate on a bounded window.** When no \`--from\`/\`--to\`/\`--this-week\`
673
+ is passed, every metrics subcommand defaults to the trailing **last 3 months**
674
+ ending now. The default exists because unbounded org-wide aggregates over
675
+ multi-year history routinely time out at the platform layer (Vercel's
676
+ 30s function cap), which surfaces to clients as a 503. Stick to the default,
677
+ or pass an explicit narrower window \u2014 don't try to bypass the default by
678
+ guessing wide \`--from\` values.
679
+
680
+ The resolved range is echoed back in every output format so the user can
681
+ see exactly what they got:
682
+
683
+ - JSON: \`{ "range": { "startDate": "...", "endDate": "...", "label": "last 3 months" } }\`
684
+ - Markdown / human: the header line includes the label, e.g.
685
+ \`Organization metrics for acme \u2014 last 3 months\`, followed by
686
+ \`Range: <startDate> \u2192 <endDate>\`.
687
+
688
+ When reporting numbers to the user (especially averages and rates),
689
+ **state the window explicitly** \u2014 say "averaged over the last 3 months"
690
+ rather than just quoting a number, since the same prompt phrased
691
+ differently can pick a different window.
692
+
670
693
  \`breakdown\` is the one you usually want for healthchecks: it returns a
671
694
  time series (by day or week) of reports, new threats, watchlisted threats,
672
695
  and takedowns filed/completed. Compare the latest period against a prior
673
696
  window to spot the **spike** or **drop** signals described in the manual
674
697
  HealthCheck Guide. \`summary\` returns a single window total; \`found\` is
675
- oriented around when threats were first discovered.
698
+ oriented around when threats were first discovered; \`organization\` is
699
+ the full customer-facing dashboard slice (reports, new threats,
700
+ watchlisted, takedowns filed/completed, plus per-type and per-day
701
+ breakdowns).
702
+
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
+ #### "Across all orgs" via an API key \u2014 what's actually possible
731
+
732
+ A ChainPatrol API key is scoped to a single organization. The
733
+ \`/organization/metrics\` endpoint derives the org from the key, so
734
+ **there is no way to fetch metrics for multiple orgs with one API key**.
735
+ If the user asks for "averages across all orgs":
736
+
737
+ 1. If they're authenticated with a user session, list accessible orgs
738
+ first (\`chainpatrol --json orgs list\`) and then call
739
+ \`metrics organization\` once per org **serially** (concurrent
740
+ fan-out across many orgs is what triggers 503/throttling on the
741
+ metrics endpoint).
742
+ 2. If they're using a single API key, explain that the key only
743
+ covers one org \u2014 they need either session auth or one key per
744
+ org, not a loop with the same key.
745
+ 3. Whatever window you choose, **make it explicit** in your reply
746
+ ("last 3 months", "last 7 days", etc) \u2014 never report an average
747
+ without naming the window it was computed over.
676
748
 
677
749
  ### \`presets list | run\` \u2014 Packaged workflows for common jobs
678
750
 
@@ -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-YZEMTNCX.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -0,0 +1,66 @@
1
+ import {
2
+ DateTime
3
+ } from "./chunk-TFCNKBRC.js";
4
+
5
+ // src/lib/date-range.ts
6
+ var DEFAULT_RANGE_MONTHS = 3;
7
+ var DEFAULT_RANGE_LABEL = `last ${DEFAULT_RANGE_MONTHS} months`;
8
+ function customLabel(start, end) {
9
+ const startIso = start.toUTC().toISODate();
10
+ const endIso = end.toUTC().toISODate();
11
+ return `custom range (${startIso} \u2192 ${endIso})`;
12
+ }
13
+ function resolveDateRange({
14
+ from,
15
+ to,
16
+ thisWeek
17
+ }) {
18
+ if (from && to) {
19
+ const start2 = DateTime.fromISO(from).toUTC();
20
+ const end = DateTime.fromISO(to).toUTC();
21
+ return {
22
+ startDate: start2.toISO() ?? from,
23
+ endDate: end.toISO() ?? to,
24
+ label: customLabel(start2, end)
25
+ };
26
+ }
27
+ if (from && !to) {
28
+ const now2 = DateTime.now().toUTC();
29
+ const start2 = DateTime.fromISO(from).toUTC();
30
+ return {
31
+ startDate: start2.toISO() ?? from,
32
+ endDate: now2.toISO() ?? "",
33
+ label: customLabel(start2, now2)
34
+ };
35
+ }
36
+ if (!from && to) {
37
+ const end = DateTime.fromISO(to).toUTC();
38
+ const start2 = end.minus({ days: 7 });
39
+ return {
40
+ startDate: start2.toISO() ?? "",
41
+ endDate: end.toISO() ?? to,
42
+ label: customLabel(start2, end)
43
+ };
44
+ }
45
+ if (thisWeek) {
46
+ const now2 = DateTime.now().toUTC();
47
+ const start2 = now2.startOf("week");
48
+ const end = now2.endOf("week");
49
+ return {
50
+ startDate: start2.toISO() ?? now2.minus({ days: 7 }).toISO() ?? "",
51
+ endDate: end.toISO() ?? now2.toISO() ?? "",
52
+ label: "this week"
53
+ };
54
+ }
55
+ const now = DateTime.now().toUTC();
56
+ const start = now.minus({ months: DEFAULT_RANGE_MONTHS });
57
+ return {
58
+ startDate: start.toISO() ?? "",
59
+ endDate: now.toISO() ?? "",
60
+ label: DEFAULT_RANGE_LABEL
61
+ };
62
+ }
63
+
64
+ export {
65
+ resolveDateRange
66
+ };
@@ -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
  }
@@ -258,7 +280,8 @@ function createApiClient(options) {
258
280
  organizationSlug: input.organizationSlug,
259
281
  brandSlug: input.brandSlug,
260
282
  startDate: parseIsoDateString(input.startDate),
261
- endDate: parseIsoDateString(input.endDate)
283
+ endDate: parseIsoDateString(input.endDate),
284
+ include: input.include
262
285
  });
263
286
  },
264
287
  listHealthchecks() {
@@ -378,5 +401,6 @@ export {
378
401
  TAKEDOWN_STATUSES,
379
402
  LIVENESS_STATUSES,
380
403
  TAKEDOWN_SORT_KEYS,
404
+ ORGANIZATION_METRICS_INCLUDE_FIELDS,
381
405
  createApiClient
382
406
  };