@chainpatrol/cli 1.5.0 → 1.7.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 (43) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +6 -1
  3. package/dist/{breakdown-5DJMXRK4.js → breakdown-2HAZ2MX5.js} +1 -1
  4. package/dist/{check-5P3EXVSN.js → check-5LJI7N5I.js} +1 -1
  5. package/dist/{chunk-OFUIYMPD.js → chunk-322AVMUH.js} +4 -0
  6. package/dist/{chunk-6VTWWGNV.js → chunk-7BZUQSRO.js} +12 -1
  7. package/dist/{chunk-PEX2A3IL.js → chunk-FPCN4SNK.js} +1 -1
  8. package/dist/{chunk-MM5OAY2U.js → chunk-YMO4QK6Q.js} +1 -1
  9. package/dist/cli.js +153 -44
  10. package/dist/{completions-ILC76OX5.js → completions-3ESPMO65.js} +1 -1
  11. package/dist/{configs-update-TAYT4JTU.js → configs-update-N7ZIWWKQ.js} +1 -1
  12. package/dist/{create-UXRULUR7.js → create-4ZJ7DXHX.js} +1 -1
  13. package/dist/{drift-TCZELVHO.js → drift-BGWNH6QE.js} +1 -1
  14. package/dist/{found-NCP6H2SD.js → found-QOARB5G2.js} +1 -1
  15. package/dist/{get-SM5KGJVA.js → get-ON5JOCV7.js} +1 -1
  16. package/dist/{healthcheck-5YEZCTXC.js → healthcheck-34LQTSVZ.js} +1 -1
  17. package/dist/{list-MPSB2DYU.js → list-5TKOWBC4.js} +2 -1
  18. package/dist/{list-2RHEVI2T.js → list-6CAKS43I.js} +1 -1
  19. package/dist/{list-WQBXC3QS.js → list-AG6LWNMN.js} +1 -1
  20. package/dist/{list-W6JZUNS2.js → list-DJFIKDY6.js} +1 -1
  21. package/dist/{list-4YBWU2JZ.js → list-IVFZGLKN.js} +1 -1
  22. package/dist/{list-23C72EH4.js → list-JG5ARDTR.js} +2 -2
  23. package/dist/{list-BAPTZNTM.js → list-MHOHKFXT.js} +1 -1
  24. package/dist/{list-TA4HYD66.js → list-PAJBOVN2.js} +1 -1
  25. package/dist/{list-QLMFMYU6.js → list-SJFJT4V3.js} +1 -1
  26. package/dist/{list-SCA73PLA.js → list-UPY6PHY4.js} +1 -1
  27. package/dist/{list-GEJQ6N2Y.js → list-UXYGISAK.js} +1 -1
  28. package/dist/{list-QJYRXPXM.js → list-W6CF25ZS.js} +1 -1
  29. package/dist/{list-json-NJ5IFRDE.js → list-json-ZR3XOQPJ.js} +1 -1
  30. package/dist/{organization-N6RMWKJM.js → organization-YAID36GZ.js} +1 -1
  31. package/dist/review-7A7WV73T.js +270 -0
  32. package/dist/{run-RRG7D4SN.js → run-AIYPHWFJ.js} +1 -1
  33. package/dist/{run-KOZXEYTL.js → run-LZZOV4HG.js} +1 -1
  34. package/dist/{run-TUADYLU7.js → run-YP4O5VXZ.js} +2 -2
  35. package/dist/{search-Y37B2JVP.js → search-IWS42COG.js} +2 -2
  36. package/dist/{search-M6EKPO4D.js → search-OGGKNPUJ.js} +1 -1
  37. package/dist/{setup-skill-RRTDEOR4.js → setup-skill-LP5Z3Z5T.js} +2 -2
  38. package/dist/{snapshot-RFGSAHO7.js → snapshot-TLSOPK7C.js} +1 -1
  39. package/dist/{summary-XCN3DRYP.js → summary-XA3PVJIW.js} +1 -1
  40. package/dist/takedown-time-ARGVSPL6.js +87 -0
  41. package/dist/{validate-OYTNB2ZM.js → validate-WB5367QN.js} +1 -1
  42. package/dist/{whoami-5WCW3FOZ.js → whoami-YE3V6I65.js} +1 -1
  43. package/package.json +2 -2
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -11,7 +11,7 @@ import {
11
11
  TAKEDOWN_SORT_KEYS,
12
12
  TAKEDOWN_STATUSES,
13
13
  createApiClient
14
- } from "./chunk-OFUIYMPD.js";
14
+ } from "./chunk-322AVMUH.js";
15
15
  import "./chunk-EGWK6SRQ.js";
16
16
  import "./chunk-U73SABXK.js";
17
17
 
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-OFUIYMPD.js";
7
+ } from "./chunk-322AVMUH.js";
8
8
  import "./chunk-EGWK6SRQ.js";
9
9
  import "./chunk-U73SABXK.js";
10
10
 
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-OFUIYMPD.js";
7
+ } from "./chunk-322AVMUH.js";
8
8
  import "./chunk-EGWK6SRQ.js";
9
9
  import "./chunk-U73SABXK.js";
10
10
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-OFUIYMPD.js";
3
+ } from "./chunk-322AVMUH.js";
4
4
  import "./chunk-EGWK6SRQ.js";
5
5
  import "./chunk-U73SABXK.js";
6
6
 
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  ORGANIZATION_METRICS_INCLUDE_FIELDS,
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-TFCNKBRC.js";
13
13
  import "./chunk-EGWK6SRQ.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,270 @@
1
+ import {
2
+ CliExitError,
3
+ ExitCode
4
+ } from "./chunk-E2LAMILJ.js";
5
+ import {
6
+ printOutput,
7
+ toCsvRows
8
+ } from "./chunk-VFT3TD3E.js";
9
+ import {
10
+ PROPOSAL_REJECT_REASONS,
11
+ PROPOSAL_WATCHLIST_REASONS,
12
+ createApiClient
13
+ } from "./chunk-322AVMUH.js";
14
+ import "./chunk-EGWK6SRQ.js";
15
+ import "./chunk-U73SABXK.js";
16
+
17
+ // src/commands/proposals/review.ts
18
+ var BRAND_REQUIRED_LABELS = ["brand impersonation", "employee impersonation"];
19
+ var DECISIONS = ["APPROVE", "REJECT", "WATCHLIST"];
20
+ function parsePositiveId(value, flag) {
21
+ if (value === void 0) {
22
+ throw new CliExitError(`proposals review requires ${flag} <id>.`, ExitCode.USAGE);
23
+ }
24
+ if (!Number.isInteger(value) || value < 1) {
25
+ throw new CliExitError(`${flag} must be a positive integer.`, ExitCode.USAGE);
26
+ }
27
+ return value;
28
+ }
29
+ function parseDecision(value) {
30
+ if (!value) {
31
+ throw new CliExitError(
32
+ "proposals review requires --decision <approve|reject|watchlist>.",
33
+ ExitCode.USAGE
34
+ );
35
+ }
36
+ const normalized = value.trim().toUpperCase();
37
+ if (!DECISIONS.includes(normalized)) {
38
+ throw new CliExitError(
39
+ `Invalid --decision '${value}'. Valid: approve, reject, watchlist.`,
40
+ ExitCode.USAGE
41
+ );
42
+ }
43
+ return normalized;
44
+ }
45
+ function parseRejectReason(value) {
46
+ if (!value) {
47
+ throw new CliExitError(
48
+ `--reject-reason is required when --decision reject. Valid: ${PROPOSAL_REJECT_REASONS.join(
49
+ ", "
50
+ )}`,
51
+ ExitCode.USAGE
52
+ );
53
+ }
54
+ const normalized = value.trim().toLowerCase();
55
+ if (!PROPOSAL_REJECT_REASONS.includes(normalized)) {
56
+ throw new CliExitError(
57
+ `Invalid --reject-reason '${value}'. Valid: ${PROPOSAL_REJECT_REASONS.join(", ")}`,
58
+ ExitCode.USAGE
59
+ );
60
+ }
61
+ return normalized;
62
+ }
63
+ function parseWatchlistReason(value) {
64
+ if (!value) {
65
+ throw new CliExitError(
66
+ `--watchlist-reason is required when --decision watchlist. Valid: ${PROPOSAL_WATCHLIST_REASONS.join(
67
+ ", "
68
+ )}`,
69
+ ExitCode.USAGE
70
+ );
71
+ }
72
+ const normalized = value.trim().toUpperCase();
73
+ if (!PROPOSAL_WATCHLIST_REASONS.includes(normalized)) {
74
+ throw new CliExitError(
75
+ `Invalid --watchlist-reason '${value}'. Valid: ${PROPOSAL_WATCHLIST_REASONS.join(
76
+ ", "
77
+ )}`,
78
+ ExitCode.USAGE
79
+ );
80
+ }
81
+ return normalized;
82
+ }
83
+ function buildReviewInput(options) {
84
+ const proposalId = parsePositiveId(options.proposalId, "--proposal-id");
85
+ const assetId = parsePositiveId(options.assetId, "--asset-id");
86
+ const decision = parseDecision(options.decision);
87
+ const input = {
88
+ proposalId,
89
+ assetId,
90
+ slug: options.org,
91
+ decision
92
+ };
93
+ if (decision === "APPROVE") {
94
+ const label = options.label?.trim();
95
+ if (!label) {
96
+ throw new CliExitError(
97
+ "--label is required when --decision approve.",
98
+ ExitCode.USAGE
99
+ );
100
+ }
101
+ if (BRAND_REQUIRED_LABELS.includes(label.toLowerCase()) && options.brandId === void 0) {
102
+ throw new CliExitError(
103
+ `--brand-id is required when --label is '${label}'. List your brands with \`chainpatrol brands list\`.`,
104
+ ExitCode.USAGE
105
+ );
106
+ }
107
+ input.label = label;
108
+ if (options.brandId !== void 0) {
109
+ input.brandId = parsePositiveId(options.brandId, "--brand-id");
110
+ }
111
+ }
112
+ if (decision === "REJECT") {
113
+ input.rejectReason = parseRejectReason(options.rejectReason);
114
+ }
115
+ if (decision === "WATCHLIST") {
116
+ const watchlistReason = parseWatchlistReason(options.watchlistReason);
117
+ if (watchlistReason === "OTHER" && !options.note?.trim()) {
118
+ throw new CliExitError(
119
+ "--note is required when --watchlist-reason OTHER, since the note becomes the reason.",
120
+ ExitCode.USAGE
121
+ );
122
+ }
123
+ input.watchlistReason = watchlistReason;
124
+ }
125
+ const note = options.note?.trim();
126
+ if (note) {
127
+ input.note = note;
128
+ }
129
+ return input;
130
+ }
131
+ function describeReview(input) {
132
+ const lines = [
133
+ `Decision: ${input.decision}`,
134
+ `Proposal: #${input.proposalId}`,
135
+ `Asset: #${input.assetId}`,
136
+ `Organization: ${input.slug ?? "from credential context"}`
137
+ ];
138
+ if (input.label) {
139
+ lines.push(`Label: ${input.label}`);
140
+ }
141
+ if (input.brandId !== void 0) {
142
+ lines.push(`Brand ID: ${input.brandId}`);
143
+ }
144
+ if (input.rejectReason) {
145
+ lines.push(`Reject reason: ${input.rejectReason}`);
146
+ }
147
+ if (input.watchlistReason) {
148
+ lines.push(`Watchlist reason: ${input.watchlistReason}`);
149
+ }
150
+ if (input.note) {
151
+ lines.push(`Note: ${input.note}`);
152
+ }
153
+ return lines;
154
+ }
155
+ async function promptForConfirmation(summary) {
156
+ const { createInterface } = await import("readline/promises");
157
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
158
+ try {
159
+ const answer = await rl.question(`${summary}
160
+ Type "yes" to continue: `);
161
+ const normalized = answer.trim().toLowerCase();
162
+ return normalized === "y" || normalized === "yes";
163
+ } finally {
164
+ rl.close();
165
+ }
166
+ }
167
+ async function runProposalsReview(options) {
168
+ const input = buildReviewInput(options);
169
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
170
+ const details = describeReview(input);
171
+ if (options.dryRun) {
172
+ printOutput({
173
+ outputFormat,
174
+ json: {
175
+ dryRun: true,
176
+ mutation: "organization.proposals.review",
177
+ payload: input,
178
+ explanation: options.explain ? "This command records a customer review on the proposal and, for APPROVE, blocks the asset." : void 0
179
+ },
180
+ markdown: ["# Dry Run: Proposal Review", "", ...details.map((d) => `- ${d}`)].join(
181
+ "\n"
182
+ ),
183
+ csv: toCsvRows([
184
+ {
185
+ proposalId: input.proposalId,
186
+ assetId: input.assetId,
187
+ decision: input.decision,
188
+ label: input.label ?? "",
189
+ rejectReason: input.rejectReason ?? "",
190
+ watchlistReason: input.watchlistReason ?? ""
191
+ }
192
+ ]),
193
+ human: () => {
194
+ console.log("Dry run only. No review was submitted.");
195
+ for (const line of details) {
196
+ console.log(line);
197
+ }
198
+ }
199
+ });
200
+ return;
201
+ }
202
+ if (!options.yes) {
203
+ const summary = [
204
+ "About to submit a review that cannot be undone over the API:",
205
+ ...details.map((line) => ` ${line}`)
206
+ ].join("\n");
207
+ if (options.confirm) {
208
+ if (!await options.confirm(summary)) {
209
+ console.log("Aborted. No review was submitted.");
210
+ return;
211
+ }
212
+ } else if (options.noInput || outputFormat !== "human" || !process.stdin.isTTY) {
213
+ throw new CliExitError(
214
+ "proposals review cannot be undone and needs confirmation. Re-run with --yes, or run it in an interactive terminal.",
215
+ ExitCode.USAGE
216
+ );
217
+ } else if (!await promptForConfirmation(summary)) {
218
+ console.log("Aborted. No review was submitted.");
219
+ return;
220
+ }
221
+ }
222
+ const client = options.apiClient ?? createApiClient();
223
+ const result = await client.reviewOrganizationProposal(input);
224
+ const row = {
225
+ proposalId: result.proposal.id,
226
+ reviewStatus: result.proposal.reviewStatus,
227
+ assetId: result.proposal.asset.id,
228
+ assetType: result.proposal.asset.type,
229
+ assetContent: result.proposal.asset.content,
230
+ assetStatus: result.proposal.asset.status,
231
+ reviewId: result.review.id,
232
+ decision: result.review.decision,
233
+ label: result.review.label,
234
+ watchlisted: result.watchlisted
235
+ };
236
+ printOutput({
237
+ outputFormat,
238
+ json: {
239
+ ...result,
240
+ explanation: options.explain ? "A WATCHLIST request is recorded as a REJECT review with the asset parked for re-evaluation." : void 0
241
+ },
242
+ markdown: [
243
+ "# Proposal Reviewed",
244
+ "",
245
+ `- Proposal: #${row.proposalId} (${row.reviewStatus})`,
246
+ `- Asset: #${row.assetId} ${row.assetType} ${row.assetContent} \u2192 ${row.assetStatus}`,
247
+ `- Review: #${row.reviewId} ${row.decision}${row.label ? ` (${row.label})` : ""}`,
248
+ `- Watchlisted: ${row.watchlisted ? "yes" : "no"}`
249
+ ].join("\n"),
250
+ csv: toCsvRows([row]),
251
+ human: () => {
252
+ console.log(`Reviewed proposal #${row.proposalId} as ${row.decision}`);
253
+ console.log(`Asset: ${row.assetType} ${row.assetContent} \u2192 ${row.assetStatus}`);
254
+ console.log(`Review: #${row.reviewId}${row.label ? ` label=${row.label}` : ""}`);
255
+ if (row.watchlisted) {
256
+ console.log("Asset parked on the watchlist for re-evaluation.");
257
+ }
258
+ if (options.explain) {
259
+ console.log(
260
+ "Reviews over the API are limited to 10 per organization per day and are shared across all of the org's API credentials."
261
+ );
262
+ }
263
+ }
264
+ });
265
+ }
266
+ export {
267
+ buildReviewInput,
268
+ describeReview,
269
+ runProposalsReview
270
+ };
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-PEX2A3IL.js";
4
+ } from "./chunk-FPCN4SNK.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-OFUIYMPD.js";
10
+ import "./chunk-322AVMUH.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-EGWK6SRQ.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-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -105,7 +105,7 @@ async function runAssetSearch(options) {
105
105
  console.error(
106
106
  `Error searching ${contents[i]}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
107
107
  );
108
- return { asset: null, reports: [], takedown: null, blockedBy: [] };
108
+ return { asset: null, reports: [], scans: [], takedown: null, blockedBy: [] };
109
109
  });
110
110
  if (errored === contents.length) {
111
111
  throw new CliExitError(
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -6,8 +6,8 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-MM5OAY2U.js";
10
- import "./chunk-6VTWWGNV.js";
9
+ } from "./chunk-YMO4QK6Q.js";
10
+ import "./chunk-7BZUQSRO.js";
11
11
  export {
12
12
  getBundledSkillContent,
13
13
  getBundledSkillVersion,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-OFUIYMPD.js";
10
+ } from "./chunk-322AVMUH.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,87 @@
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-322AVMUH.js";
11
+ import "./chunk-TFCNKBRC.js";
12
+ import "./chunk-EGWK6SRQ.js";
13
+ import "./chunk-U73SABXK.js";
14
+
15
+ // src/commands/metrics/takedown-time.ts
16
+ function formatDuration(hours) {
17
+ if (hours < 1 / 60) {
18
+ return `${Math.round(hours * 3600)}s`;
19
+ }
20
+ if (hours < 1) {
21
+ return `${Math.round(hours * 60)}m`;
22
+ }
23
+ if (hours < 24) {
24
+ return `${hours.toFixed(1)}h`;
25
+ }
26
+ return `${(hours / 24).toFixed(1)}d`;
27
+ }
28
+ async function runMetricsTakedownTime(options) {
29
+ const range = resolveDateRange({
30
+ from: options.from,
31
+ to: options.to,
32
+ thisWeek: options.thisWeek
33
+ });
34
+ const client = options.apiClient ?? createApiClient();
35
+ const result = await client.getMetricsTakedownTime({
36
+ slug: options.org,
37
+ // The caller's own --from/--to go through untouched when given: the API
38
+ // reads a date-only value as the whole day, while `resolveDateRange` has
39
+ // already expanded it to a midnight timestamp, which would cut the final
40
+ // day off.
41
+ startDate: options.from ?? range.startDate,
42
+ endDate: options.to ?? range.endDate,
43
+ brandIds: options.brandIds
44
+ });
45
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
46
+ printOutput({
47
+ outputFormat,
48
+ json: { ...result, range },
49
+ markdown: [
50
+ `# Median Time to Takedown (${options.org}) \u2014 ${range.label}`,
51
+ "",
52
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`,
53
+ "",
54
+ ...result.byAssetType.map(
55
+ (row) => `- ${row.assetType}: ${formatDuration(row.medianTakedownHours)} (${row.count} takedowns)`
56
+ )
57
+ ].join("\n"),
58
+ csv: toCsvRows(
59
+ result.byAssetType.map((row) => ({
60
+ rangeLabel: range.label,
61
+ startDate: range.startDate,
62
+ endDate: range.endDate,
63
+ assetType: row.assetType,
64
+ medianTakedownMinutes: row.medianTakedownMinutes,
65
+ medianTakedownHours: row.medianTakedownHours,
66
+ medianTakedownDays: row.medianTakedownDays,
67
+ count: row.count
68
+ }))
69
+ ),
70
+ human: () => {
71
+ console.log(`Median time to takedown for ${options.org} \u2014 ${range.label}`);
72
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
73
+ if (result.byAssetType.length === 0) {
74
+ console.log("No completed takedowns in this range.");
75
+ return;
76
+ }
77
+ for (const row of result.byAssetType) {
78
+ console.log(
79
+ `${row.assetType}: ${formatDuration(row.medianTakedownHours)} (${row.count} takedowns)`
80
+ );
81
+ }
82
+ }
83
+ });
84
+ }
85
+ export {
86
+ runMetricsTakedownTime
87
+ };
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-OFUIYMPD.js";
11
+ } from "./chunk-322AVMUH.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-U73SABXK.js";
14
14
 
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-VFT3TD3E.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-OFUIYMPD.js";
6
+ } from "./chunk-322AVMUH.js";
7
7
  import "./chunk-EGWK6SRQ.js";
8
8
  import "./chunk-U73SABXK.js";
9
9
 
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": "1.5.0",
5
+ "version": "1.7.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "lint": "npx oxlint ."
34
34
  },
35
35
  "dependencies": {
36
- "@chainpatrol/sdk": "1.5.0",
36
+ "@chainpatrol/sdk": "1.7.0",
37
37
  "ink": "^7.0.1",
38
38
  "meow": "^14.1.0",
39
39
  "open": "^11.0.0",