@chainpatrol/cli 0.12.0 → 0.13.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,32 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7cc2783: Default every `chainpatrol metrics …` subcommand
8
+ (`summary` / `found` / `breakdown` / `organization`) to a trailing
9
+ **3-month** window when no `--from`/`--to`/`--this-week` flag is
10
+ supplied, and echo the resolved range in every output format
11
+ (human, markdown, csv, json) so consumers can see exactly what
12
+ window the numbers describe.
13
+
14
+ Previously, omitting the date flags sent `undefined` for `startDate`
15
+ / `endDate`, which caused the metrics handler to fan out ~15 parallel
16
+ unbounded Prisma aggregates per call. On large orgs that routinely
17
+ exceeded the platform's 30s function cap and surfaced to agents as
18
+ flaky `503` responses. The new default keeps the queries bounded
19
+ and produces a stable, labelled result.
20
+
21
+ `--this-week` is now also wired through to `metrics summary`,
22
+ `metrics breakdown`, and `metrics organization` (it was already
23
+ supported on `metrics found`).
24
+
25
+ JSON output now includes a `range: { startDate, endDate, label }`
26
+ field on every metrics subcommand. The label is one of
27
+ `"last 3 months"` (default), `"this week"`, or
28
+ `"custom range (<startIso> → <endIso>)"`.
29
+
3
30
  ## 0.12.0
4
31
 
5
32
  ### Minor Changes
@@ -1,3 +1,6 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
1
4
  import {
2
5
  printOutput,
3
6
  toCsvRows
@@ -11,21 +14,27 @@ 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}`);
@@ -659,20 +659,65 @@ 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
+ #### "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.
676
721
 
677
722
  ### \`presets list | run\` \u2014 Packaged workflows for common jobs
678
723
 
@@ -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
+ };
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-W42ITB5J.js";
16
+ } from "./chunk-BQDMAEQU.js";
17
17
  import "./chunk-NQT77IB7.js";
18
18
  import {
19
19
  DateTime
@@ -263,49 +263,52 @@ 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).",
267
- usage: "chainpatrol metrics organization [--org <slug>] [--from ISO --to ISO]",
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]",
268
268
  options: [
269
269
  "--org <slug> Organization slug (omit when using an API key)",
270
270
  "--brand-slug <slug> Filter by brand slug (requires brands-metrics flag)",
271
271
  "--from <iso> Start date (ISO 8601, e.g. 2026-01-01)",
272
- "--to <iso> End date (ISO 8601)"
272
+ "--to <iso> End date (ISO 8601)",
273
+ "--this-week Use the current ISO week (overrides default)"
273
274
  ],
274
275
  examples: [
275
- "chainpatrol metrics organization --org acme",
276
+ "chainpatrol metrics organization --org acme # defaults to last 3 months",
276
277
  "chainpatrol metrics organization --org acme --from 2026-01-01 --to 2026-02-01",
277
278
  "CHAINPATROL_API_KEY=... chainpatrol metrics organization --json"
278
279
  ]
279
280
  },
280
281
  "metrics summary": {
281
- description: "Summary metrics for an org over a date range.",
282
- usage: "chainpatrol metrics summary --org <slug> [--from ISO --to ISO]",
282
+ description: "Summary metrics for an org over a date range. Defaults to the trailing 3 months if no date range is given.",
283
+ usage: "chainpatrol metrics summary --org <slug> [--from ISO --to ISO|--this-week]",
283
284
  options: [
284
285
  "--org <slug> Organization slug",
285
286
  "--from <iso> Start date (ISO 8601)",
286
287
  "--to <iso> End date (ISO 8601)",
288
+ "--this-week Use the current ISO week (overrides default)",
287
289
  "--brand <ids> Comma-separated brand IDs"
288
290
  ]
289
291
  },
290
292
  "metrics found": {
291
- description: "Count of newly found threats over a date range.",
293
+ description: "Count of newly found threats over a date range. Defaults to the trailing 3 months if no date range is given.",
292
294
  usage: "chainpatrol metrics found --org <slug> [--from ISO --to ISO|--this-week]",
293
295
  options: [
294
296
  "--org <slug> Organization slug",
295
297
  "--from <iso> Start date (ISO 8601)",
296
298
  "--to <iso> End date (ISO 8601)",
297
- "--this-week Use the current week",
299
+ "--this-week Use the current ISO week (overrides default)",
298
300
  "--brand <ids> Comma-separated brand IDs"
299
301
  ]
300
302
  },
301
303
  "metrics breakdown": {
302
- description: "Threat counts grouped by axis (day, type, brand).",
303
- usage: "chainpatrol metrics breakdown --org <slug> --by <day|type|brand>",
304
+ description: "Threat counts grouped by axis (day, type, brand). Defaults to the trailing 3 months if no date range is given.",
305
+ usage: "chainpatrol metrics breakdown --org <slug> --by <day|type|brand> [--from ISO --to ISO|--this-week]",
304
306
  options: [
305
307
  "--org <slug> Organization slug",
306
308
  "--by <axis> day|type|brand (required)",
307
309
  "--from <iso> Start date",
308
310
  "--to <iso> End date",
311
+ "--this-week Use the current ISO week (overrides default)",
309
312
  "--brand <ids> Comma-separated brand IDs"
310
313
  ]
311
314
  },
@@ -1303,12 +1306,13 @@ async function main() {
1303
1306
  case "metrics": {
1304
1307
  if (subcommand === "organization" || subcommand === "org") {
1305
1308
  const org2 = await tryResolveOrg();
1306
- const { runMetricsOrganization } = await import("./organization-IIIOSTHF.js");
1309
+ const { runMetricsOrganization } = await import("./organization-GS5LFIWO.js");
1307
1310
  await runMetricsOrganization({
1308
1311
  org: org2,
1309
1312
  brandSlug: cli.flags.brandSlug,
1310
1313
  from: cli.flags.from,
1311
1314
  to: cli.flags.to,
1315
+ thisWeek: cli.flags.thisWeek,
1312
1316
  json: jsonMode,
1313
1317
  outputFormat: cliContext.outputFormat
1314
1318
  });
@@ -1316,11 +1320,12 @@ async function main() {
1316
1320
  }
1317
1321
  const org = await resolveOrg();
1318
1322
  if (subcommand === "summary") {
1319
- const { runMetricsSummary } = await import("./summary-Y2LG6UJM.js");
1323
+ const { runMetricsSummary } = await import("./summary-CV657J5V.js");
1320
1324
  await runMetricsSummary({
1321
1325
  org,
1322
1326
  from: cli.flags.from,
1323
1327
  to: cli.flags.to,
1328
+ thisWeek: cli.flags.thisWeek,
1324
1329
  brandIds: parseBrandIds(),
1325
1330
  json: jsonMode,
1326
1331
  outputFormat: cliContext.outputFormat
@@ -1328,7 +1333,7 @@ async function main() {
1328
1333
  break;
1329
1334
  }
1330
1335
  if (subcommand === "found") {
1331
- const { runMetricsFound } = await import("./found-EVXJZGYX.js");
1336
+ const { runMetricsFound } = await import("./found-NXAWEIEK.js");
1332
1337
  await runMetricsFound({
1333
1338
  org,
1334
1339
  from: cli.flags.from,
@@ -1345,12 +1350,13 @@ async function main() {
1345
1350
  if (!by || !["day", "type", "brand"].includes(by)) {
1346
1351
  throw new Error("metrics breakdown requires --by <day|type|brand>");
1347
1352
  }
1348
- const { runMetricsBreakdown } = await import("./breakdown-KCTKQNDY.js");
1353
+ const { runMetricsBreakdown } = await import("./breakdown-B6QUGYI6.js");
1349
1354
  await runMetricsBreakdown({
1350
1355
  org,
1351
1356
  by,
1352
1357
  from: cli.flags.from,
1353
1358
  to: cli.flags.to,
1359
+ thisWeek: cli.flags.thisWeek,
1354
1360
  brandIds: parseBrandIds(),
1355
1361
  json: jsonMode,
1356
1362
  outputFormat: cliContext.outputFormat
@@ -1617,12 +1623,12 @@ async function main() {
1617
1623
  case "setup":
1618
1624
  case "install":
1619
1625
  case "i": {
1620
- const { setupSkill } = await import("./setup-skill-FA4STSGA.js");
1626
+ const { setupSkill } = await import("./setup-skill-TPMTULHJ.js");
1621
1627
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1622
1628
  break;
1623
1629
  }
1624
1630
  case "uninstall": {
1625
- const { uninstallSkill } = await import("./setup-skill-FA4STSGA.js");
1631
+ const { uninstallSkill } = await import("./setup-skill-TPMTULHJ.js");
1626
1632
  uninstallSkill({ json: jsonMode });
1627
1633
  break;
1628
1634
  }
@@ -0,0 +1,59 @@
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/found.ts
16
+ async function runMetricsFound(options) {
17
+ const range = resolveDateRange({
18
+ from: options.from,
19
+ to: options.to,
20
+ thisWeek: options.thisWeek
21
+ });
22
+ const client = options.apiClient ?? createApiClient();
23
+ const result = await client.getMetricsFound({
24
+ slug: options.org,
25
+ startDate: range.startDate,
26
+ endDate: range.endDate,
27
+ brandIds: options.brandIds
28
+ });
29
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
30
+ printOutput({
31
+ outputFormat,
32
+ json: { ...result, range },
33
+ markdown: [
34
+ `# Found Threats (${options.org}) \u2014 ${range.label}`,
35
+ "",
36
+ `- Metric: ${result.metric}`,
37
+ `- Found: ${result.found}`,
38
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`
39
+ ].join("\n"),
40
+ csv: toCsvRows([
41
+ {
42
+ rangeLabel: range.label,
43
+ metric: result.metric,
44
+ found: result.found,
45
+ startDate: range.startDate,
46
+ endDate: range.endDate
47
+ }
48
+ ]),
49
+ human: () => {
50
+ console.log(
51
+ `Found threats (${result.metric}) for ${options.org} \u2014 ${range.label}: ${result.found}`
52
+ );
53
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
54
+ }
55
+ });
56
+ }
57
+ export {
58
+ runMetricsFound
59
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
1
4
  import {
2
5
  printOutput,
3
6
  toCsvRows
@@ -13,20 +16,26 @@ import "./chunk-U73SABXK.js";
13
16
  async function runMetricsOrganization(options) {
14
17
  const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
15
18
  const client = options.apiClient ?? createApiClient();
19
+ const range = resolveDateRange({
20
+ from: options.from,
21
+ to: options.to,
22
+ thisWeek: options.thisWeek
23
+ });
16
24
  const input = {
17
25
  organizationSlug: options.org,
18
26
  brandSlug: options.brandSlug,
19
- startDate: options.from,
20
- endDate: options.to
27
+ startDate: range.startDate,
28
+ endDate: range.endDate
21
29
  };
22
30
  const result = await client.getOrganizationMetrics(input);
23
31
  const label = options.org ?? "your organization";
24
32
  printOutput({
25
33
  outputFormat,
26
- json: result,
34
+ json: { ...result, range },
27
35
  markdown: [
28
- `# Organization Metrics (${label})`,
36
+ `# Organization Metrics (${label}) \u2014 ${range.label}`,
29
37
  "",
38
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`,
30
39
  `- Reports: ${result.metrics.reports}`,
31
40
  `- New threats: ${result.metrics.newThreats}`,
32
41
  `- Watchlisted threats: ${result.metrics.threatsWatchlisted}`,
@@ -39,6 +48,9 @@ async function runMetricsOrganization(options) {
39
48
  ].join("\n"),
40
49
  csv: toCsvRows([
41
50
  {
51
+ rangeLabel: range.label,
52
+ startDate: range.startDate,
53
+ endDate: range.endDate,
42
54
  reports: result.metrics.reports,
43
55
  newThreats: result.metrics.newThreats,
44
56
  threatsWatchlisted: result.metrics.threatsWatchlisted,
@@ -51,7 +63,8 @@ async function runMetricsOrganization(options) {
51
63
  }
52
64
  ]),
53
65
  human: () => {
54
- console.log(`Organization metrics for ${label}`);
66
+ console.log(`Organization metrics for ${label} \u2014 ${range.label}`);
67
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
55
68
  console.log(`Reports: ${result.metrics.reports}`);
56
69
  console.log(`New threats: ${result.metrics.newThreats}`);
57
70
  console.log(`Watchlisted threats: ${result.metrics.threatsWatchlisted}`);
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-W42ITB5J.js";
9
+ } from "./chunk-BQDMAEQU.js";
10
10
  import "./chunk-NQT77IB7.js";
11
11
  export {
12
12
  getBundledSkillContent,
@@ -1,3 +1,6 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
1
4
  import {
2
5
  printOutput,
3
6
  toCsvRows
@@ -11,20 +14,26 @@ import "./chunk-U73SABXK.js";
11
14
 
12
15
  // src/commands/metrics/summary.ts
13
16
  async function runMetricsSummary(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.getMetricsSummary({
16
24
  slug: options.org,
17
- startDate: options.from,
18
- endDate: options.to,
25
+ startDate: range.startDate,
26
+ endDate: range.endDate,
19
27
  brandIds: options.brandIds
20
28
  });
21
29
  const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
22
30
  printOutput({
23
31
  outputFormat,
24
- json: result,
32
+ json: { ...result, range },
25
33
  markdown: [
26
- `# Metrics Summary (${options.org})`,
34
+ `# Metrics Summary (${options.org}) \u2014 ${range.label}`,
27
35
  "",
36
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`,
28
37
  `- Reports: ${result.metrics.reports}`,
29
38
  `- New threats: ${result.metrics.newThreats}`,
30
39
  `- Watchlisted threats: ${result.metrics.threatsWatchlisted}`,
@@ -33,6 +42,9 @@ async function runMetricsSummary(options) {
33
42
  ].join("\n"),
34
43
  csv: toCsvRows([
35
44
  {
45
+ rangeLabel: range.label,
46
+ startDate: range.startDate,
47
+ endDate: range.endDate,
36
48
  reports: result.metrics.reports,
37
49
  newThreats: result.metrics.newThreats,
38
50
  threatsWatchlisted: result.metrics.threatsWatchlisted,
@@ -45,7 +57,8 @@ async function runMetricsSummary(options) {
45
57
  }
46
58
  ]),
47
59
  human: () => {
48
- console.log(`Metrics summary for ${options.org}`);
60
+ console.log(`Metrics summary for ${options.org} \u2014 ${range.label}`);
61
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
49
62
  console.log(`Reports: ${result.metrics.reports}`);
50
63
  console.log(`New threats: ${result.metrics.newThreats}`);
51
64
  console.log(`Watchlisted threats: ${result.metrics.threatsWatchlisted}`);
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.12.0",
5
+ "version": "0.13.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -1,95 +0,0 @@
1
- import {
2
- printOutput,
3
- toCsvRows
4
- } from "./chunk-VFT3TD3E.js";
5
- import {
6
- createApiClient
7
- } from "./chunk-OSRFCVBT.js";
8
- import "./chunk-EGWK6SRQ.js";
9
- import {
10
- DateTime
11
- } from "./chunk-TFCNKBRC.js";
12
- import "./chunk-U73SABXK.js";
13
-
14
- // src/lib/date-range.ts
15
- function resolveDateRange({
16
- from,
17
- to,
18
- thisWeek
19
- }) {
20
- if (from && to) {
21
- return {
22
- startDate: DateTime.fromISO(from).toUTC().toISO() ?? from,
23
- endDate: DateTime.fromISO(to).toUTC().toISO() ?? to
24
- };
25
- }
26
- if (from && !to) {
27
- const now2 = DateTime.now().toUTC();
28
- return {
29
- startDate: DateTime.fromISO(from).toUTC().toISO() ?? from,
30
- endDate: now2.toISO() ?? ""
31
- };
32
- }
33
- if (!from && to) {
34
- const end = DateTime.fromISO(to).toUTC();
35
- return {
36
- startDate: end.minus({ days: 7 }).toISO() ?? "",
37
- endDate: end.toISO() ?? to
38
- };
39
- }
40
- if (thisWeek) {
41
- const now2 = DateTime.now().toUTC();
42
- return {
43
- startDate: now2.startOf("week").toISO() ?? now2.minus({ days: 7 }).toISO() ?? "",
44
- endDate: now2.endOf("week").toISO() ?? now2.toISO() ?? ""
45
- };
46
- }
47
- const now = DateTime.now().toUTC();
48
- return {
49
- startDate: now.minus({ days: 7 }).toISO() ?? "",
50
- endDate: now.toISO() ?? ""
51
- };
52
- }
53
-
54
- // src/commands/metrics/found.ts
55
- async function runMetricsFound(options) {
56
- const range = resolveDateRange({
57
- from: options.from,
58
- to: options.to,
59
- thisWeek: options.thisWeek
60
- });
61
- const client = options.apiClient ?? createApiClient();
62
- const result = await client.getMetricsFound({
63
- slug: options.org,
64
- startDate: range.startDate,
65
- endDate: range.endDate,
66
- brandIds: options.brandIds
67
- });
68
- const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
69
- printOutput({
70
- outputFormat,
71
- json: result,
72
- markdown: [
73
- `# Found Threats (${options.org})`,
74
- "",
75
- `- Metric: ${result.metric}`,
76
- `- Found: ${result.found}`,
77
- `- Range: ${range.startDate} -> ${range.endDate}`
78
- ].join("\n"),
79
- csv: toCsvRows([
80
- {
81
- metric: result.metric,
82
- found: result.found,
83
- startDate: range.startDate,
84
- endDate: range.endDate
85
- }
86
- ]),
87
- human: () => {
88
- console.log(`Found threats (${result.metric}) for ${options.org}: ${result.found}`);
89
- console.log(`Range: ${range.startDate} -> ${range.endDate}`);
90
- }
91
- });
92
- }
93
- export {
94
- runMetricsFound
95
- };