@chainpatrol/cli 0.2.2 → 0.3.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.
@@ -4,7 +4,8 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-KMILLX3U.js";
7
+ } from "./chunk-DSOM6TZX.js";
8
+ import "./chunk-TFCNKBRC.js";
8
9
  import "./chunk-EEG7T6WT.js";
9
10
  import "./chunk-U73SABXK.js";
10
11
 
@@ -7,9 +7,11 @@ import {
7
7
  toCsvRows
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
- DateTime,
11
10
  createApiClient
12
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import {
13
+ DateTime
14
+ } from "./chunk-TFCNKBRC.js";
13
15
 
14
16
  // src/presets/index.ts
15
17
  var PRESETS = [
@@ -0,0 +1,150 @@
1
+ import {
2
+ DateTime
3
+ } from "./chunk-TFCNKBRC.js";
4
+ import {
5
+ getValidCredentials
6
+ } from "./chunk-EEG7T6WT.js";
7
+ import {
8
+ getConfig
9
+ } from "./chunk-U73SABXK.js";
10
+
11
+ // src/lib/api-client.ts
12
+ function parseIsoDate(value) {
13
+ if (!value) return void 0;
14
+ const dt = DateTime.fromISO(value, { zone: "utc" });
15
+ if (!dt.isValid) {
16
+ throw new Error(`Invalid ISO date: '${value}'. Use YYYY-MM-DD or full ISO 8601.`);
17
+ }
18
+ return dt.toJSDate();
19
+ }
20
+ var REQUEST_TIMEOUT_MS = 3e4;
21
+ function createApiClient(options) {
22
+ const config = getConfig();
23
+ const apiUrl = options?.apiUrl ?? config.apiUrl;
24
+ const getToken = options?.getToken ?? (() => getValidCredentials().accessToken);
25
+ async function request(path, body) {
26
+ const token = getToken();
27
+ let res;
28
+ try {
29
+ res = await fetch(`${apiUrl}/api/v2${path}`, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ Authorization: `Bearer ${token}`
34
+ },
35
+ body: JSON.stringify(body),
36
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
37
+ });
38
+ } catch (err) {
39
+ if (err instanceof DOMException && err.name === "TimeoutError") {
40
+ throw new Error(
41
+ "Request timed out. Check your network connection and try again."
42
+ );
43
+ }
44
+ if (err instanceof TypeError && err.code === "ECONNREFUSED") {
45
+ throw new Error(`Cannot connect to ${apiUrl}. Is the server running?`);
46
+ }
47
+ throw new Error("Network error. Check your internet connection and try again.");
48
+ }
49
+ if (!res.ok) {
50
+ let errorMessageFromResponse = null;
51
+ try {
52
+ const errorBody = await res.json();
53
+ errorMessageFromResponse = errorBody.message ?? null;
54
+ } catch {
55
+ }
56
+ if (res.status === 401) {
57
+ throw new Error(
58
+ errorMessageFromResponse ?? "Authentication failed. Run `chainpatrol login` to re-authenticate."
59
+ );
60
+ }
61
+ if (res.status === 403) {
62
+ throw new Error(
63
+ errorMessageFromResponse ?? "You don't have permission to access this resource. Check your --org slug and permissions."
64
+ );
65
+ }
66
+ if (res.status === 404) {
67
+ throw new Error(
68
+ errorMessageFromResponse ?? "Organization not found. Check your --org slug."
69
+ );
70
+ }
71
+ if (res.status === 422) {
72
+ throw new Error(errorMessageFromResponse ?? "Request validation failed.");
73
+ }
74
+ if (res.status >= 500) {
75
+ throw new Error(
76
+ "ChainPatrol API is temporarily unavailable. Please try again later."
77
+ );
78
+ }
79
+ throw new Error(
80
+ errorMessageFromResponse ?? `Request failed (${res.status}). Run with --json for details.`
81
+ );
82
+ }
83
+ return await res.json();
84
+ }
85
+ return {
86
+ listDetectionConfigs(slug) {
87
+ return request("/detection/configs/list", { slug });
88
+ },
89
+ updateDetectionConfig(input) {
90
+ return request("/detection/configs/update", input);
91
+ },
92
+ runDetectionConfigs(input) {
93
+ return request("/detection/configs/run", input);
94
+ },
95
+ validateDetectionConfigs(input) {
96
+ return request("/detection/configs/validate", input);
97
+ },
98
+ getDetectionDrift(input) {
99
+ return request("/detection/drift", {
100
+ ...input,
101
+ startDate: parseIsoDate(input.startDate),
102
+ endDate: parseIsoDate(input.endDate)
103
+ });
104
+ },
105
+ getOperationsQueuesSnapshot(input) {
106
+ return request(
107
+ "/operations/queues/snapshot",
108
+ input
109
+ );
110
+ },
111
+ getMetricsSummary(input) {
112
+ return request("/metrics/summary", {
113
+ ...input,
114
+ startDate: parseIsoDate(input.startDate),
115
+ endDate: parseIsoDate(input.endDate)
116
+ });
117
+ },
118
+ getMetricsFound(input) {
119
+ return request("/metrics/found", {
120
+ ...input,
121
+ startDate: parseIsoDate(input.startDate),
122
+ endDate: parseIsoDate(input.endDate)
123
+ });
124
+ },
125
+ getMetricsBreakdown(input) {
126
+ return request("/metrics/breakdown", {
127
+ ...input,
128
+ startDate: parseIsoDate(input.startDate),
129
+ endDate: parseIsoDate(input.endDate)
130
+ });
131
+ },
132
+ createReport(input) {
133
+ return request("/report/create", input);
134
+ },
135
+ listOrganizationReports(input) {
136
+ return request("/public/getOrganizationReports", {
137
+ slug: input.slug,
138
+ limit: input.limit ?? 10,
139
+ cursor: input.cursor,
140
+ status: input.status,
141
+ searchQuery: input.searchQuery,
142
+ reportedByCustomer: input.reportedByCustomer
143
+ });
144
+ }
145
+ };
146
+ }
147
+
148
+ export {
149
+ createApiClient
150
+ };
@@ -4,20 +4,85 @@ import {
4
4
  } from "./chunk-IUZB3DQW.js";
5
5
 
6
6
  // src/commands/setup-skill.ts
7
- import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs";
7
+ import { mkdirSync, writeFileSync, existsSync, readFileSync as readFileSync2, rmSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { homedir } from "os";
10
+
11
+ // src/lib/version.ts
12
+ import { readFileSync } from "fs";
13
+ import { dirname, resolve } from "path";
14
+ import { fileURLToPath } from "url";
15
+ var PACKAGE_NAME = "@chainpatrol/cli";
16
+ var cached;
17
+ function getCliVersion() {
18
+ if (cached !== void 0) return cached;
19
+ cached = resolveCliVersion();
20
+ return cached;
21
+ }
22
+ function resolveCliVersion() {
23
+ try {
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const candidates = [
26
+ resolve(here, "..", "package.json"),
27
+ resolve(here, "..", "..", "package.json")
28
+ ];
29
+ for (const candidate of candidates) {
30
+ const version = tryReadVersion(candidate);
31
+ if (version) return version;
32
+ }
33
+ } catch {
34
+ }
35
+ return "0.0.0";
36
+ }
37
+ function tryReadVersion(path) {
38
+ try {
39
+ const raw = readFileSync(path, "utf-8");
40
+ const pkg = JSON.parse(raw);
41
+ if (pkg.name === PACKAGE_NAME && typeof pkg.version === "string") {
42
+ return pkg.version;
43
+ }
44
+ } catch {
45
+ }
46
+ return void 0;
47
+ }
48
+ function compareVersions(a, b) {
49
+ const parsed = (value) => {
50
+ const hyphenIndex = value.indexOf("-");
51
+ const main = hyphenIndex >= 0 ? value.slice(0, hyphenIndex) : value;
52
+ const pre = hyphenIndex >= 0 ? value.slice(hyphenIndex + 1) : "";
53
+ const parts = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
54
+ while (parts.length < 3) parts.push(0);
55
+ return { parts, pre };
56
+ };
57
+ const va = parsed(a);
58
+ const vb = parsed(b);
59
+ for (let i = 0; i < 3; i += 1) {
60
+ if (va.parts[i] !== vb.parts[i]) return va.parts[i] - vb.parts[i];
61
+ }
62
+ if (va.pre === vb.pre) return 0;
63
+ if (va.pre === "") return 1;
64
+ if (vb.pre === "") return -1;
65
+ return va.pre < vb.pre ? -1 : 1;
66
+ }
67
+
68
+ // src/commands/setup-skill.ts
10
69
  var SKILL_DIR = join(homedir(), ".claude", "skills", "chainpatrol");
11
70
  var SKILL_FILE = join(SKILL_DIR, "SKILL.md");
12
- var SKILL_CONTENT = `---
71
+ function buildSkillContent(version) {
72
+ return `---
13
73
  name: chainpatrol
74
+ version: ${version}
14
75
  description: |
15
76
  ChainPatrol CLI assistant. Helps use the chainpatrol CLI tool: login via device
16
77
  code flow, check auth status, list detection configs, list reports (including
17
- customer-reported ones), and run CLI commands.
78
+ customer-reported ones), run CLI commands, and run an organization
79
+ healthcheck across the detection / reviewing / blocklisting / takedown
80
+ pipeline.
18
81
  Use when: "chainpatrol cli", "login to chainpatrol", "check detection configs",
19
82
  "am I logged in", "list configs", "use the cli", "list reports",
20
- "customer reports", "reports reported by customer", "find detection gaps".
83
+ "customer reports", "reports reported by customer", "find detection gaps",
84
+ "org healthcheck", "organization health check", "audit my org",
85
+ "what's wrong with org", "review org setup".
21
86
  allowed-tools:
22
87
  - Bash
23
88
  - Read
@@ -156,6 +221,22 @@ Config is stored at \`~/.chainpatrol/config.json\`:
156
221
 
157
222
  Override config dir with \`CHAINPATROL_CONFIG_DIR\` env var.
158
223
 
224
+ ## Version Checks
225
+
226
+ The CLI runs two lightweight version checks alongside each command and prints
227
+ a nudge to stderr if anything is out of date:
228
+
229
+ - **Skill freshness**: compares the installed skill (\`~/.claude/skills/chainpatrol/SKILL.md\`)
230
+ to the version bundled with the CLI. If it's missing or older, the user is
231
+ asked to run \`chainpatrol setup\`.
232
+ - **NPM freshness**: compares the running CLI version to the latest published
233
+ on the npm registry. The check is throttled to once per 24 hours and capped
234
+ at a 1.5s timeout, with the result cached at \`<configDir>/version-check.json\`.
235
+
236
+ Set \`CHAINPATROL_NO_UPDATE_CHECK=1\` to silence both checks. JSON mode
237
+ (\`--json\`) and quiet mode (\`-q\` / \`--quiet\`) also suppress the nudges so
238
+ machine-readable output is never polluted.
239
+
159
240
  ## Global Flags
160
241
 
161
242
  | Flag | Description |
@@ -182,7 +263,129 @@ The \`configs list\` command shows detection sources grouped by:
182
263
  - **Not Configured** \u2014 Sources available but not yet set up for the org
183
264
 
184
265
  Each config entry includes: title, status, cron schedule, and configuration parameters.
266
+
267
+ ## Organization HealthCheck Guide
268
+
269
+ This guide explains how to look for things that may be wrong for a given org
270
+ across the full pipeline: **detection \u2192 reviewing \u2192 blocklisting \u2192 takedowns**.
271
+ When the user asks for a "health check", "audit", "what's wrong with org X",
272
+ "review org X's setup", or similar, walk through each section below and surface
273
+ findings.
274
+
275
+ In each section there are things you can look for that may be wrong.
276
+
277
+ ### Detection
278
+
279
+ #### Enable Config That May Be Turned Off for Current Threats
280
+
281
+ Blocked threats exist in an asset type but the matching detection source is
282
+ turned off. For example: lots of Twitter assets are on the blocklist, but
283
+ detection sources like "Twitter / X User Search" or "Twitter Post Search" are
284
+ disabled. Those should be turned on.
285
+
286
+ #### Spike in Detections
287
+
288
+ A spike in recent detections is worth investigating. It could be a bad config
289
+ change, or it could be a legitimate new attack push in this area \u2014 useful
290
+ intel to surface to the security team as a targeted spike.
291
+
292
+ #### Drop in Detections
293
+
294
+ A drop is also worth looking into. It may be a bad config change, or it may
295
+ mean the config is not really relevant and can be safely turned off (not all
296
+ default-on configs are relevant to every org).
297
+
298
+ ### Reviewing
299
+
300
+ #### Pile Up / Backlog of Unreviewed Proposals
301
+
302
+ Too many proposals waiting in review. For most organizations this is over 100
303
+ reports, but really the threshold is relative to the average number of
304
+ confirmed threats per week. Example: if an org only adds 5 blocked threats per
305
+ week, then a 7-day backlog of even 10 proposals is a really big deal.
306
+
307
+ #### Really Old Proposals
308
+
309
+ Any proposal waiting for review longer than 14 days is a sign something has
310
+ gone wrong. Even complex investigations rarely take longer than this. Except
311
+ for rare cases, these should be rejected or approved to prevent a backlog
312
+ from building.
313
+
314
+ #### Spike in Auto Approved Reports
315
+
316
+ If suddenly a lot of proposals in an org are being approved by automation,
317
+ that can be a sign of a bad rule approving too much, or a break in auto
318
+ confidences. Sometimes detection is spamming things that uniquely combine
319
+ with a weakness of a rule \u2014 which is effectively a bad rule. In all these
320
+ cases, notify an engineer at ChainPatrol and check any detection configs you
321
+ adjusted recently, since those may be the cause of spam combined with a weak
322
+ rule.
323
+
324
+ ### Blocklisting
325
+
326
+ #### Google Safe Browsing (Coming Soon)
327
+
328
+ (Needs new public API added before this works.)
329
+
330
+ High error rate in Google Safe Browsing submission tracker. Each submission
331
+ has a status. If too many are in \`CANCELLED\`, that means Google's engine
332
+ denied our submission. Contact ChainPatrol's eng team to investigate why, and
333
+ also take a look at the org's custom detection sources \u2014 it's possible there
334
+ are too many false positives landing on the blocklist, indicating issues with
335
+ detection and reviewing rules.
336
+
337
+ ### Takedowns
338
+
339
+ #### Too Many Takedowns in ToDo
340
+
341
+ Can mean a gap in automated takedowns not being implemented for some new area
342
+ of threats. It can also mean the areas that require manual takedowns are
343
+ being missed by the takedown team.
344
+
345
+ #### Too Many Takedowns In Progress
346
+
347
+ Typically means something is wrong with the submission itself. The takedown
348
+ may need to be resubmitted, the vendor asked for more evidence, or we may
349
+ have submitted it in the wrong place.
350
+
351
+ #### Too Many Cancelled Takedowns
352
+
353
+ Takedowns should rarely be cancelled. A cancelled takedown means "we will not
354
+ do this takedown" for some reason. Cases like adding an item to the blocklist
355
+ when it's already taken down are treated as completed, not cancelled. So even
356
+ 3 cancelled takedowns in a 7-day period is too many.
357
+
358
+ #### Automated Takedowns Turned Off for Over 30 Days
359
+
360
+ Automated takedowns should be on by default for nearly every organization.
361
+ Any issue that would make you want to turn off automated takedowns should be
362
+ resolved within 30 days.
185
363
  `;
364
+ }
365
+ function getBundledSkillVersion() {
366
+ return getCliVersion();
367
+ }
368
+ function getBundledSkillContent() {
369
+ return buildSkillContent(getCliVersion());
370
+ }
371
+ function readInstalledSkillVersion() {
372
+ if (!existsSync(SKILL_FILE)) return void 0;
373
+ try {
374
+ const raw = readFileSync2(SKILL_FILE, "utf-8");
375
+ return parseSkillVersion(raw);
376
+ } catch {
377
+ return void 0;
378
+ }
379
+ }
380
+ function isSkillInstalled() {
381
+ return existsSync(SKILL_FILE);
382
+ }
383
+ function parseSkillVersion(content) {
384
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
385
+ if (!fmMatch) return void 0;
386
+ const versionMatch = fmMatch[1].match(/^version:\s*(.+?)\s*$/m);
387
+ return versionMatch ? versionMatch[1].trim() : void 0;
388
+ }
186
389
  var LOGO = `
187
390
  \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
188
391
  \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
@@ -195,10 +398,11 @@ var LOGO = `
195
398
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
196
399
  `;
197
400
  function setupSkill(options) {
401
+ const skillContent = buildSkillContent(getCliVersion());
198
402
  const alreadyExists = existsSync(SKILL_FILE);
199
403
  if (alreadyExists) {
200
- const existing = readFileSync(SKILL_FILE, "utf-8");
201
- if (existing === SKILL_CONTENT) {
404
+ const existing = readFileSync2(SKILL_FILE, "utf-8");
405
+ if (existing === skillContent) {
202
406
  if (options.json) {
203
407
  console.log(JSON.stringify({ status: "up-to-date", path: SKILL_FILE }));
204
408
  } else {
@@ -208,7 +412,7 @@ function setupSkill(options) {
208
412
  }
209
413
  }
210
414
  mkdirSync(SKILL_DIR, { recursive: true });
211
- writeFileSync(SKILL_FILE, SKILL_CONTENT, { mode: 420 });
415
+ writeFileSync(SKILL_FILE, skillContent, { mode: 420 });
212
416
  const completionResult = installCompletions();
213
417
  if (options.json) {
214
418
  console.log(
@@ -257,7 +461,15 @@ function uninstallSkill(options) {
257
461
  }
258
462
  }
259
463
  }
464
+
260
465
  export {
466
+ getCliVersion,
467
+ compareVersions,
468
+ getBundledSkillVersion,
469
+ getBundledSkillContent,
470
+ readInstalledSkillVersion,
471
+ isSkillInstalled,
472
+ parseSkillVersion,
261
473
  setupSkill,
262
474
  uninstallSkill
263
475
  };
@@ -1,10 +1,3 @@
1
- import {
2
- getValidCredentials
3
- } from "./chunk-EEG7T6WT.js";
4
- import {
5
- getConfig
6
- } from "./chunk-U73SABXK.js";
7
-
8
1
  // ../../node_modules/luxon/src/errors.js
9
2
  var LuxonError = class extends Error {
10
3
  };
@@ -6430,144 +6423,6 @@ function friendlyDateTime(dateTimeish) {
6430
6423
  }
6431
6424
  }
6432
6425
 
6433
- // src/lib/api-client.ts
6434
- function parseIsoDate(value) {
6435
- if (!value) return void 0;
6436
- const dt = DateTime.fromISO(value, { zone: "utc" });
6437
- if (!dt.isValid) {
6438
- throw new Error(`Invalid ISO date: '${value}'. Use YYYY-MM-DD or full ISO 8601.`);
6439
- }
6440
- return dt.toJSDate();
6441
- }
6442
- var REQUEST_TIMEOUT_MS = 3e4;
6443
- function createApiClient(options) {
6444
- const config = getConfig();
6445
- const apiUrl = options?.apiUrl ?? config.apiUrl;
6446
- const getToken = options?.getToken ?? (() => getValidCredentials().accessToken);
6447
- async function request(path, body) {
6448
- const token = getToken();
6449
- let res;
6450
- try {
6451
- res = await fetch(`${apiUrl}/api/v2${path}`, {
6452
- method: "POST",
6453
- headers: {
6454
- "Content-Type": "application/json",
6455
- Authorization: `Bearer ${token}`
6456
- },
6457
- body: JSON.stringify(body),
6458
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
6459
- });
6460
- } catch (err) {
6461
- if (err instanceof DOMException && err.name === "TimeoutError") {
6462
- throw new Error(
6463
- "Request timed out. Check your network connection and try again."
6464
- );
6465
- }
6466
- if (err instanceof TypeError && err.code === "ECONNREFUSED") {
6467
- throw new Error(`Cannot connect to ${apiUrl}. Is the server running?`);
6468
- }
6469
- throw new Error("Network error. Check your internet connection and try again.");
6470
- }
6471
- if (!res.ok) {
6472
- let errorMessageFromResponse = null;
6473
- try {
6474
- const errorBody = await res.json();
6475
- errorMessageFromResponse = errorBody.message ?? null;
6476
- } catch {
6477
- }
6478
- if (res.status === 401) {
6479
- throw new Error(
6480
- errorMessageFromResponse ?? "Authentication failed. Run `chainpatrol login` to re-authenticate."
6481
- );
6482
- }
6483
- if (res.status === 403) {
6484
- throw new Error(
6485
- errorMessageFromResponse ?? "You don't have permission to access this resource. Check your --org slug and permissions."
6486
- );
6487
- }
6488
- if (res.status === 404) {
6489
- throw new Error(
6490
- errorMessageFromResponse ?? "Organization not found. Check your --org slug."
6491
- );
6492
- }
6493
- if (res.status === 422) {
6494
- throw new Error(errorMessageFromResponse ?? "Request validation failed.");
6495
- }
6496
- if (res.status >= 500) {
6497
- throw new Error(
6498
- "ChainPatrol API is temporarily unavailable. Please try again later."
6499
- );
6500
- }
6501
- throw new Error(
6502
- errorMessageFromResponse ?? `Request failed (${res.status}). Run with --json for details.`
6503
- );
6504
- }
6505
- return await res.json();
6506
- }
6507
- return {
6508
- listDetectionConfigs(slug) {
6509
- return request("/detection/configs/list", { slug });
6510
- },
6511
- updateDetectionConfig(input) {
6512
- return request("/detection/configs/update", input);
6513
- },
6514
- runDetectionConfigs(input) {
6515
- return request("/detection/configs/run", input);
6516
- },
6517
- validateDetectionConfigs(input) {
6518
- return request("/detection/configs/validate", input);
6519
- },
6520
- getDetectionDrift(input) {
6521
- return request("/detection/drift", {
6522
- ...input,
6523
- startDate: parseIsoDate(input.startDate),
6524
- endDate: parseIsoDate(input.endDate)
6525
- });
6526
- },
6527
- getOperationsQueuesSnapshot(input) {
6528
- return request(
6529
- "/operations/queues/snapshot",
6530
- input
6531
- );
6532
- },
6533
- getMetricsSummary(input) {
6534
- return request("/metrics/summary", {
6535
- ...input,
6536
- startDate: parseIsoDate(input.startDate),
6537
- endDate: parseIsoDate(input.endDate)
6538
- });
6539
- },
6540
- getMetricsFound(input) {
6541
- return request("/metrics/found", {
6542
- ...input,
6543
- startDate: parseIsoDate(input.startDate),
6544
- endDate: parseIsoDate(input.endDate)
6545
- });
6546
- },
6547
- getMetricsBreakdown(input) {
6548
- return request("/metrics/breakdown", {
6549
- ...input,
6550
- startDate: parseIsoDate(input.startDate),
6551
- endDate: parseIsoDate(input.endDate)
6552
- });
6553
- },
6554
- createReport(input) {
6555
- return request("/report/create", input);
6556
- },
6557
- listOrganizationReports(input) {
6558
- return request("/public/getOrganizationReports", {
6559
- slug: input.slug,
6560
- limit: input.limit ?? 10,
6561
- cursor: input.cursor,
6562
- status: input.status,
6563
- searchQuery: input.searchQuery,
6564
- reportedByCustomer: input.reportedByCustomer
6565
- });
6566
- }
6567
- };
6568
- }
6569
-
6570
6426
  export {
6571
- DateTime,
6572
- createApiClient
6427
+ DateTime
6573
6428
  };
package/dist/cli.js CHANGED
@@ -7,6 +7,20 @@ import {
7
7
  import {
8
8
  resolveOutputFormat
9
9
  } from "./chunk-VFT3TD3E.js";
10
+ import {
11
+ compareVersions,
12
+ getBundledSkillVersion,
13
+ getCliVersion,
14
+ isSkillInstalled,
15
+ readInstalledSkillVersion
16
+ } from "./chunk-T4DYUWUD.js";
17
+ import "./chunk-IUZB3DQW.js";
18
+ import {
19
+ DateTime
20
+ } from "./chunk-TFCNKBRC.js";
21
+ import {
22
+ getConfigDir
23
+ } from "./chunk-U73SABXK.js";
10
24
 
11
25
  // src/cli.tsx
12
26
  import meow from "meow";
@@ -345,6 +359,145 @@ function getTopLevelHelp() {
345
359
  ].join("\n");
346
360
  }
347
361
 
362
+ // src/lib/version-check.ts
363
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
364
+ import { join } from "path";
365
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@chainpatrol/cli/latest";
366
+ var CHECK_FILE_NAME = "version-check.json";
367
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
368
+ var NPM_REQUEST_TIMEOUT_MS = 1500;
369
+ function checkSkillStatus() {
370
+ const bundled = getBundledSkillVersion();
371
+ if (!isSkillInstalled()) {
372
+ return { state: "missing", bundled };
373
+ }
374
+ const installed = readInstalledSkillVersion();
375
+ if (!installed) {
376
+ return { state: "outdated", installed: void 0, bundled };
377
+ }
378
+ if (compareVersions(installed, bundled) < 0) {
379
+ return { state: "outdated", installed, bundled };
380
+ }
381
+ return { state: "ok", installed, bundled };
382
+ }
383
+ function getCheckFilePath() {
384
+ return join(getConfigDir(), CHECK_FILE_NAME);
385
+ }
386
+ function readCheckCache() {
387
+ const path = getCheckFilePath();
388
+ if (!existsSync(path)) return {};
389
+ try {
390
+ return JSON.parse(readFileSync(path, "utf-8"));
391
+ } catch {
392
+ return {};
393
+ }
394
+ }
395
+ function writeCheckCache(cache) {
396
+ try {
397
+ mkdirSync(getConfigDir(), { recursive: true });
398
+ writeFileSync(getCheckFilePath(), JSON.stringify(cache, null, 2), { mode: 420 });
399
+ } catch {
400
+ }
401
+ }
402
+ async function fetchNpmLatestVersion(timeoutMs) {
403
+ const controller = new AbortController();
404
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
405
+ try {
406
+ const res = await fetch(NPM_REGISTRY_URL, {
407
+ signal: controller.signal,
408
+ headers: { accept: "application/json" }
409
+ });
410
+ if (!res.ok) return void 0;
411
+ const data = await res.json();
412
+ return typeof data.version === "string" ? data.version : void 0;
413
+ } catch {
414
+ return void 0;
415
+ } finally {
416
+ clearTimeout(timer);
417
+ }
418
+ }
419
+ async function checkNpmStatus(options) {
420
+ const current = getCliVersion();
421
+ if (process.env.CHAINPATROL_NO_UPDATE_CHECK === "1") {
422
+ return { state: "unknown", current };
423
+ }
424
+ const now = options?.now ?? DateTime.now().toUTC().toMillis();
425
+ const force = options?.force ?? false;
426
+ const timeoutMs = options?.timeoutMs ?? NPM_REQUEST_TIMEOUT_MS;
427
+ const cache = readCheckCache();
428
+ const fresh = cache.lastNpmCheckAt !== void 0 && now - cache.lastNpmCheckAt < CHECK_INTERVAL_MS;
429
+ let latest;
430
+ if (!force && fresh && cache.lastSeenLatest) {
431
+ latest = cache.lastSeenLatest;
432
+ } else {
433
+ latest = await fetchNpmLatestVersion(timeoutMs);
434
+ if (latest) {
435
+ writeCheckCache({ lastNpmCheckAt: now, lastSeenLatest: latest });
436
+ }
437
+ }
438
+ if (!latest) return { state: "unknown", current };
439
+ if (compareVersions(current, latest) < 0) {
440
+ return { state: "outdated", current, latest };
441
+ }
442
+ return { state: "ok", current, latest };
443
+ }
444
+ function formatSkillNudge(status) {
445
+ if (status.state === "missing") {
446
+ return [
447
+ "Tip: the ChainPatrol skill for Claude Code is not installed.",
448
+ " Run `chainpatrol setup` to install it (one-time setup)."
449
+ ].join("\n");
450
+ }
451
+ if (status.state === "outdated") {
452
+ const from = status.installed ?? "unknown";
453
+ return [
454
+ `Tip: your installed ChainPatrol skill (${from}) is out of date (latest: ${status.bundled}).`,
455
+ " Run `chainpatrol setup` to update."
456
+ ].join("\n");
457
+ }
458
+ return void 0;
459
+ }
460
+ function formatNpmNudge(status) {
461
+ if (status.state === "outdated") {
462
+ return [
463
+ `Tip: a new version of @chainpatrol/cli is available: ${status.latest} (you have ${status.current}).`,
464
+ " Update: npm i -g @chainpatrol/cli"
465
+ ].join("\n");
466
+ }
467
+ return void 0;
468
+ }
469
+ async function runVersionChecks(options) {
470
+ const skill = checkSkillStatus();
471
+ const npm = await checkNpmStatus(options);
472
+ const messages = [];
473
+ const skillMsg = formatSkillNudge(skill);
474
+ if (skillMsg) messages.push(skillMsg);
475
+ const npmMsg = formatNpmNudge(npm);
476
+ if (npmMsg) messages.push(npmMsg);
477
+ return { skill, npm, messages };
478
+ }
479
+ function shouldRunVersionChecks(opts) {
480
+ if (opts.jsonMode || opts.quiet || opts.helpOrVersionFlag) return false;
481
+ if (process.env.CHAINPATROL_NO_UPDATE_CHECK === "1") return false;
482
+ if (!opts.command) return false;
483
+ const skipCommands = /* @__PURE__ */ new Set([
484
+ "setup",
485
+ "install",
486
+ "i",
487
+ "uninstall",
488
+ "completions",
489
+ "help"
490
+ ]);
491
+ return !skipCommands.has(opts.command);
492
+ }
493
+ function printVersionMessages(messages) {
494
+ if (messages.length === 0) return;
495
+ for (const msg of messages) {
496
+ console.error("");
497
+ console.error(msg);
498
+ }
499
+ }
500
+
348
501
  // src/cli.tsx
349
502
  var COMMANDS = [
350
503
  "login",
@@ -389,7 +542,7 @@ var cli = meow(`
389
542
  ${getTopLevelHelp()}
390
543
  `, {
391
544
  importMeta: import.meta,
392
- version: "0.1.0",
545
+ version: getCliVersion(),
393
546
  // Boolean flags without an explicit `default` should resolve to `undefined`
394
547
  // when not passed (rather than meow's default of `false`), so handlers can
395
548
  // distinguish "flag omitted" from "flag explicitly set to false". Flags that
@@ -533,12 +686,12 @@ function parseAttachmentUrls() {
533
686
  }
534
687
  async function handleConfigsList(org) {
535
688
  if (jsonMode) {
536
- const { listConfigsJson } = await import("./list-json-GTST5CRN.js");
689
+ const { listConfigsJson } = await import("./list-json-GANUT7SB.js");
537
690
  await listConfigsJson({ org });
538
691
  return;
539
692
  }
540
693
  const { render } = await import("ink");
541
- const { default: ConfigsList } = await import("./list-IZT5P7II.js");
694
+ const { default: ConfigsList } = await import("./list-56BS25X4.js");
542
695
  const { default: React } = await import("react");
543
696
  render(React.createElement(ConfigsList, { org }));
544
697
  }
@@ -564,6 +717,14 @@ function maybeShowScopedHelp() {
564
717
  }
565
718
  return false;
566
719
  }
720
+ var helpOrVersionFlag = cli.flags.help === true || cli.flags.version === true;
721
+ var versionChecksEnabled = shouldRunVersionChecks({
722
+ command,
723
+ jsonMode,
724
+ quiet,
725
+ helpOrVersionFlag
726
+ });
727
+ var versionChecksPromise = versionChecksEnabled ? runVersionChecks().catch(() => ({ messages: [] })) : null;
567
728
  async function main() {
568
729
  if (maybeShowScopedHelp()) {
569
730
  return;
@@ -625,7 +786,7 @@ async function main() {
625
786
  case "detections": {
626
787
  const org = await resolveOrg();
627
788
  if (subcommand === "healthcheck") {
628
- const { runDetectionsHealthcheck } = await import("./healthcheck-URZRXICK.js");
789
+ const { runDetectionsHealthcheck } = await import("./healthcheck-6RHZBEVE.js");
629
790
  await runDetectionsHealthcheck({
630
791
  org,
631
792
  source: cli.flags.source,
@@ -640,7 +801,7 @@ async function main() {
640
801
  break;
641
802
  }
642
803
  if (subcommand === "validate") {
643
- const { runDetectionsValidate } = await import("./validate-VQKKFNBO.js");
804
+ const { runDetectionsValidate } = await import("./validate-NIBVL7X5.js");
644
805
  await runDetectionsValidate({
645
806
  org,
646
807
  source: cli.flags.source,
@@ -655,7 +816,7 @@ async function main() {
655
816
  break;
656
817
  }
657
818
  if (subcommand === "drift") {
658
- const { runDetectionsDrift } = await import("./drift-UDNSOP2S.js");
819
+ const { runDetectionsDrift } = await import("./drift-2JRZK2MC.js");
659
820
  await runDetectionsDrift({
660
821
  org,
661
822
  source: cli.flags.source,
@@ -669,7 +830,7 @@ async function main() {
669
830
  break;
670
831
  }
671
832
  if (subcommand === "run") {
672
- const { runDetectionsRun } = await import("./run-V66OGZ3W.js");
833
+ const { runDetectionsRun } = await import("./run-JZNZSCDJ.js");
673
834
  await runDetectionsRun({
674
835
  org,
675
836
  configId: cli.flags.configId,
@@ -688,7 +849,7 @@ async function main() {
688
849
  break;
689
850
  }
690
851
  if (action === "run") {
691
- const { runDetectionsRun } = await import("./run-V66OGZ3W.js");
852
+ const { runDetectionsRun } = await import("./run-JZNZSCDJ.js");
692
853
  await runDetectionsRun({
693
854
  org,
694
855
  configId: cli.flags.configId,
@@ -706,7 +867,7 @@ async function main() {
706
867
  throw new Error("detections configs update requires --config-id");
707
868
  }
708
869
  const configPatch = getConfigPatchFromSetFlags();
709
- const { runDetectionsConfigsUpdate } = await import("./configs-update-OVEREFCP.js");
870
+ const { runDetectionsConfigsUpdate } = await import("./configs-update-AHI7T6S4.js");
710
871
  await runDetectionsConfigsUpdate({
711
872
  org,
712
873
  configId: cli.flags.configId,
@@ -733,7 +894,7 @@ async function main() {
733
894
  case "metrics": {
734
895
  const org = await resolveOrg();
735
896
  if (subcommand === "summary") {
736
- const { runMetricsSummary } = await import("./summary-SUWVXO7Y.js");
897
+ const { runMetricsSummary } = await import("./summary-PA2CCZVO.js");
737
898
  await runMetricsSummary({
738
899
  org,
739
900
  from: cli.flags.from,
@@ -745,7 +906,7 @@ async function main() {
745
906
  break;
746
907
  }
747
908
  if (subcommand === "found") {
748
- const { runMetricsFound } = await import("./found-SJVDAINM.js");
909
+ const { runMetricsFound } = await import("./found-YYYFF4FD.js");
749
910
  await runMetricsFound({
750
911
  org,
751
912
  from: cli.flags.from,
@@ -762,7 +923,7 @@ async function main() {
762
923
  if (!by || !["day", "type", "brand"].includes(by)) {
763
924
  throw new Error("metrics breakdown requires --by <day|type|brand>");
764
925
  }
765
- const { runMetricsBreakdown } = await import("./breakdown-PBH5NX6P.js");
926
+ const { runMetricsBreakdown } = await import("./breakdown-BECXDJIS.js");
766
927
  await runMetricsBreakdown({
767
928
  org,
768
929
  by,
@@ -782,7 +943,7 @@ async function main() {
782
943
  case "reports": {
783
944
  if (subcommand === "list") {
784
945
  const org = await resolveOrg();
785
- const { runReportsList } = await import("./list-V5OVJ5NG.js");
946
+ const { runReportsList } = await import("./list-MHEIY7LQ.js");
786
947
  await runReportsList({
787
948
  org,
788
949
  limit: cli.flags.limit,
@@ -798,7 +959,7 @@ async function main() {
798
959
  }
799
960
  if (subcommand === "create") {
800
961
  const org = await tryResolveOrg();
801
- const { runReportsCreate } = await import("./create-C7H3L7TU.js");
962
+ const { runReportsCreate } = await import("./create-RS37JAMM.js");
802
963
  await runReportsCreate({
803
964
  org,
804
965
  title: cli.flags.title,
@@ -822,7 +983,7 @@ async function main() {
822
983
  }
823
984
  case "queues": {
824
985
  if (subcommand === "snapshot") {
825
- const { runQueuesSnapshot } = await import("./snapshot-TZHS7XQX.js");
986
+ const { runQueuesSnapshot } = await import("./snapshot-KGMO44YB.js");
826
987
  await runQueuesSnapshot({
827
988
  org: cli.flags.org,
828
989
  all: cli.flags.all,
@@ -840,7 +1001,7 @@ async function main() {
840
1001
  }
841
1002
  case "presets": {
842
1003
  if (subcommand === "list") {
843
- const { runPresetsList } = await import("./list-3JBC4637.js");
1004
+ const { runPresetsList } = await import("./list-M2UQCXIO.js");
844
1005
  await runPresetsList({ outputFormat: cliContext.outputFormat });
845
1006
  break;
846
1007
  }
@@ -851,7 +1012,7 @@ async function main() {
851
1012
  );
852
1013
  }
853
1014
  const org = await resolveOrg();
854
- const { runPresetsRun } = await import("./run-MUMSMG4V.js");
1015
+ const { runPresetsRun } = await import("./run-7NTCD7JI.js");
855
1016
  await runPresetsRun({
856
1017
  presetId: action,
857
1018
  org,
@@ -868,12 +1029,12 @@ async function main() {
868
1029
  case "setup":
869
1030
  case "install":
870
1031
  case "i": {
871
- const { setupSkill } = await import("./setup-skill-SIWP3KFS.js");
1032
+ const { setupSkill } = await import("./setup-skill-ZUZ5MYLI.js");
872
1033
  setupSkill({ json: jsonMode });
873
1034
  break;
874
1035
  }
875
1036
  case "uninstall": {
876
- const { uninstallSkill } = await import("./setup-skill-SIWP3KFS.js");
1037
+ const { uninstallSkill } = await import("./setup-skill-ZUZ5MYLI.js");
877
1038
  uninstallSkill({ json: jsonMode });
878
1039
  break;
879
1040
  }
@@ -893,12 +1054,26 @@ async function main() {
893
1054
  }
894
1055
  }
895
1056
  }
896
- main().catch((err) => {
1057
+ async function flushVersionChecks() {
1058
+ if (!versionChecksPromise) return;
1059
+ let timer;
1060
+ const timeoutPromise = new Promise((resolve) => {
1061
+ timer = setTimeout(() => resolve({ messages: [] }), 500);
1062
+ });
1063
+ try {
1064
+ const result = await Promise.race([versionChecksPromise, timeoutPromise]);
1065
+ printVersionMessages(result.messages);
1066
+ } finally {
1067
+ if (timer) clearTimeout(timer);
1068
+ }
1069
+ }
1070
+ main().then(flushVersionChecks).catch(async (err) => {
897
1071
  const message = err instanceof Error ? err.message : String(err);
898
1072
  const exitCode = mapErrorToExitCode(err);
899
1073
  if (jsonMode) {
900
1074
  jsonError(message, exitCode);
901
1075
  }
902
1076
  console.error(message);
1077
+ await flushVersionChecks();
903
1078
  process.exit(exitCode);
904
1079
  });
@@ -7,7 +7,8 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-KMILLX3U.js";
10
+ } from "./chunk-DSOM6TZX.js";
11
+ import "./chunk-TFCNKBRC.js";
11
12
  import "./chunk-EEG7T6WT.js";
12
13
  import "./chunk-U73SABXK.js";
13
14
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -3,9 +3,11 @@ import {
3
3
  toCsvRows
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
- DateTime,
7
6
  createApiClient
8
- } from "./chunk-KMILLX3U.js";
7
+ } from "./chunk-DSOM6TZX.js";
8
+ import {
9
+ DateTime
10
+ } from "./chunk-TFCNKBRC.js";
9
11
  import "./chunk-EEG7T6WT.js";
10
12
  import "./chunk-U73SABXK.js";
11
13
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-KMILLX3U.js";
3
+ } from "./chunk-DSOM6TZX.js";
4
+ import "./chunk-TFCNKBRC.js";
4
5
  import {
5
6
  ErrorDisplay,
6
7
  Spinner
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  PRESETS
3
- } from "./chunk-6FIJ4NU4.js";
3
+ } from "./chunk-B3CCMSG2.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-KMILLX3U.js";
9
+ import "./chunk-DSOM6TZX.js";
10
+ import "./chunk-TFCNKBRC.js";
10
11
  import "./chunk-EEG7T6WT.js";
11
12
  import "./chunk-U73SABXK.js";
12
13
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-KMILLX3U.js";
3
+ } from "./chunk-DSOM6TZX.js";
4
+ import "./chunk-TFCNKBRC.js";
4
5
  import "./chunk-EEG7T6WT.js";
5
6
  import "./chunk-U73SABXK.js";
6
7
 
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-6FIJ4NU4.js";
4
+ } from "./chunk-B3CCMSG2.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-KMILLX3U.js";
10
+ import "./chunk-DSOM6TZX.js";
11
+ import "./chunk-TFCNKBRC.js";
11
12
  import "./chunk-EEG7T6WT.js";
12
13
  import "./chunk-U73SABXK.js";
13
14
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -0,0 +1,19 @@
1
+ import {
2
+ getBundledSkillContent,
3
+ getBundledSkillVersion,
4
+ isSkillInstalled,
5
+ parseSkillVersion,
6
+ readInstalledSkillVersion,
7
+ setupSkill,
8
+ uninstallSkill
9
+ } from "./chunk-T4DYUWUD.js";
10
+ import "./chunk-IUZB3DQW.js";
11
+ export {
12
+ getBundledSkillContent,
13
+ getBundledSkillVersion,
14
+ isSkillInstalled,
15
+ parseSkillVersion,
16
+ readInstalledSkillVersion,
17
+ setupSkill,
18
+ uninstallSkill
19
+ };
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
@@ -4,7 +4,8 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-KMILLX3U.js";
7
+ } from "./chunk-DSOM6TZX.js";
8
+ import "./chunk-TFCNKBRC.js";
8
9
  import "./chunk-EEG7T6WT.js";
9
10
  import "./chunk-U73SABXK.js";
10
11
 
@@ -8,7 +8,8 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-KMILLX3U.js";
11
+ } from "./chunk-DSOM6TZX.js";
12
+ import "./chunk-TFCNKBRC.js";
12
13
  import "./chunk-EEG7T6WT.js";
13
14
  import "./chunk-U73SABXK.js";
14
15
 
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.2.2",
5
+ "version": "0.3.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [