@chainpatrol/cli 0.23.1 → 0.25.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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -0
  3. package/dist/{breakdown-XA6UUD3T.js → breakdown-5DJMXRK4.js} +2 -2
  4. package/dist/{check-NQZPNAUF.js → check-5P3EXVSN.js} +1 -2
  5. package/dist/chunk-MM5OAY2U.js +317 -0
  6. package/dist/chunk-OFUIYMPD.js +64 -0
  7. package/dist/{chunk-DVNTYW7L.js → chunk-PEX2A3IL.js} +1 -1
  8. package/dist/cli.js +38 -38
  9. package/dist/{configs-update-VDZVBTIF.js → configs-update-TAYT4JTU.js} +1 -2
  10. package/dist/{create-DGCWT2YJ.js → create-UXRULUR7.js} +1 -2
  11. package/dist/{drift-YZHCIQ5O.js → drift-TCZELVHO.js} +1 -2
  12. package/dist/{found-6JAOLM6M.js → found-NCP6H2SD.js} +2 -2
  13. package/dist/{get-RC75EJFC.js → get-4YHQ7ZE2.js} +1 -2
  14. package/dist/{healthcheck-7YWSOY6Y.js → healthcheck-5YEZCTXC.js} +1 -2
  15. package/dist/{list-IDR7WH5J.js → list-23C72EH4.js} +3 -3
  16. package/dist/{list-4HX2R7HU.js → list-2RHEVI2T.js} +1 -2
  17. package/dist/{list-ZARZ72DG.js → list-33ILIIXB.js} +1 -2
  18. package/dist/{list-4KIZLIVN.js → list-GEJQ6N2Y.js} +1 -2
  19. package/dist/{list-WQFDRXHD.js → list-MPSB2DYU.js} +1 -2
  20. package/dist/{list-EJDWUF6W.js → list-QJYRXPXM.js} +1 -2
  21. package/dist/{list-UDT5MDQZ.js → list-QLMFMYU6.js} +1 -2
  22. package/dist/{list-YNQ6VSYV.js → list-SCA73PLA.js} +1 -2
  23. package/dist/{list-SRVNEUHC.js → list-T2YIGGM6.js} +1 -2
  24. package/dist/{list-ZBC53OYA.js → list-TA4HYD66.js} +1 -2
  25. package/dist/{list-H7NVUX42.js → list-TULFPPG3.js} +5 -3
  26. package/dist/{list-Q3MMBL4Y.js → list-W6JZUNS2.js} +1 -2
  27. package/dist/{list-json-7FDHNRYP.js → list-json-NJ5IFRDE.js} +1 -2
  28. package/dist/{login-AMEXCOGT.js → login-2NRI3ECY.js} +2 -2
  29. package/dist/{login-json-MPLXCSP4.js → login-json-HOCNSQQ7.js} +2 -2
  30. package/dist/{login-plain-NIJDS3D2.js → login-plain-6ZJF5KA7.js} +2 -2
  31. package/dist/{organization-NEZA6JX2.js → organization-N6RMWKJM.js} +2 -2
  32. package/dist/{run-CCVJIK7L.js → run-KOZXEYTL.js} +1 -2
  33. package/dist/{run-5QV3YD7V.js → run-SE2SUE64.js} +1 -2
  34. package/dist/{run-JLTNFOJK.js → run-TUADYLU7.js} +3 -3
  35. package/dist/{search-JXJLBV43.js → search-5DOXBWPB.js} +1 -2
  36. package/dist/{search-ZXTJPVM6.js → search-Y37B2JVP.js} +1 -2
  37. package/dist/{setup-skill-V3HDWN7T.js → setup-skill-RRTDEOR4.js} +1 -1
  38. package/dist/{snapshot-NYOLPGRW.js → snapshot-RFGSAHO7.js} +1 -2
  39. package/dist/{summary-FE6ROSOO.js → summary-XCN3DRYP.js} +2 -2
  40. package/dist/{validate-RPQEEQ36.js → validate-OYTNB2ZM.js} +1 -2
  41. package/dist/{whoami-ZIWZCBGI.js → whoami-5WCW3FOZ.js} +1 -2
  42. package/package.json +2 -1
  43. package/dist/chunk-GOQL7UCG.js +0 -422
  44. package/dist/chunk-LKITFBIG.js +0 -1950
  45. package/dist/{chunk-PZV55KAR.js → chunk-GGBG2R63.js} +3 -3
@@ -1,1950 +0,0 @@
1
- import {
2
- installCompletions,
3
- uninstallCompletions
4
- } from "./chunk-6VTWWGNV.js";
5
-
6
- // src/commands/setup-skill.ts
7
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
8
- import { join as join2 } from "path";
9
- import { homedir as homedir2 } from "os";
10
-
11
- // src/commands/login-hook.ts
12
- import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
13
- import { homedir } from "os";
14
- import { join, dirname } from "path";
15
- var CLAUDE_DIR = join(homedir(), ".claude");
16
- var HOOK_DIR = join(CLAUDE_DIR, "hooks");
17
- var HOOK_FILE = join(HOOK_DIR, "chainpatrol-login.sh");
18
- var SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
19
- var HOOK_SCRIPT = `#!/bin/bash
20
- # chainpatrol auto-login hook \u2014 installed by \`chainpatrol setup\`.
21
- # On Claude Code session start, if the user has no chainpatrol credentials,
22
- # emit additionalContext so Claude surfaces the device-code login URL on
23
- # the first user turn. Safe to run repeatedly: no-ops once authenticated.
24
- set -uo pipefail
25
-
26
- CONFIG_DIR="\${CHAINPATROL_CONFIG_DIR:-$HOME/.chainpatrol}"
27
- if [ -s "$CONFIG_DIR/credentials.json" ]; then
28
- exit 0
29
- fi
30
-
31
- # Only nudge when the CLI is reachable \u2014 otherwise we'd ask Claude to run
32
- # a command that isn't installed.
33
- if ! command -v chainpatrol >/dev/null 2>&1 \\
34
- && [ ! -x /usr/local/bin/chainpatrol ] \\
35
- && ! ls /opt/node*/bin/chainpatrol >/dev/null 2>&1; then
36
- exit 0
37
- fi
38
-
39
- cat <<'JSON'
40
- {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"chainpatrol auto-login: the user is not authenticated with ChainPatrol (no credentials.json found). If the user's task involves ChainPatrol or the chainpatrol CLI, start \`chainpatrol login\` in the background per the /chainpatrol skill's headless runbook (background + tail /tmp/cp-login.out) and surface the verification URL before doing anything else. If the user's task is unrelated, ignore this notice."}}
41
- JSON
42
- exit 0
43
- `;
44
- function installLoginHook() {
45
- mkdirSync(HOOK_DIR, { recursive: true });
46
- const hookWritten = !existsSync(HOOK_FILE) || readFileSync(HOOK_FILE, "utf-8") !== HOOK_SCRIPT;
47
- if (hookWritten) {
48
- writeFileSync(HOOK_FILE, HOOK_SCRIPT, { mode: 493 });
49
- }
50
- const settingsUpdated = registerHookInSettings();
51
- return {
52
- hookPath: HOOK_FILE,
53
- hookWritten,
54
- settingsPath: SETTINGS_FILE,
55
- settingsUpdated
56
- };
57
- }
58
- function uninstallLoginHook() {
59
- let hookRemoved = false;
60
- if (existsSync(HOOK_FILE)) {
61
- unlinkSync(HOOK_FILE);
62
- hookRemoved = true;
63
- }
64
- const settingsUpdated = unregisterHookFromSettings();
65
- return { hookRemoved, settingsUpdated };
66
- }
67
- function readSettings() {
68
- if (!existsSync(SETTINGS_FILE)) return {};
69
- const raw = readFileSync(SETTINGS_FILE, "utf-8");
70
- if (raw.trim() === "") return {};
71
- return JSON.parse(raw);
72
- }
73
- function writeSettings(settings) {
74
- mkdirSync(dirname(SETTINGS_FILE), { recursive: true });
75
- writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n", {
76
- mode: 420
77
- });
78
- }
79
- function hasHookEntry(settings) {
80
- const matchers = settings.hooks?.SessionStart;
81
- if (!Array.isArray(matchers)) return false;
82
- return matchers.some(
83
- (matcher) => Array.isArray(matcher?.hooks) && matcher.hooks.some((h) => h?.command === HOOK_FILE)
84
- );
85
- }
86
- function registerHookInSettings() {
87
- const settings = readSettings();
88
- if (hasHookEntry(settings)) return false;
89
- const hooks = settings.hooks ??= {};
90
- const sessionStart = hooks.SessionStart ??= [];
91
- sessionStart.push({
92
- hooks: [{ type: "command", command: HOOK_FILE }]
93
- });
94
- writeSettings(settings);
95
- return true;
96
- }
97
- function unregisterHookFromSettings() {
98
- if (!existsSync(SETTINGS_FILE)) return false;
99
- const settings = readSettings();
100
- const sessionStart = settings.hooks?.SessionStart;
101
- if (!Array.isArray(sessionStart)) return false;
102
- let changed = false;
103
- const filtered = sessionStart.map((matcher) => {
104
- if (!Array.isArray(matcher?.hooks)) return matcher;
105
- const remaining = matcher.hooks.filter((h) => h?.command !== HOOK_FILE);
106
- if (remaining.length === matcher.hooks.length) return matcher;
107
- changed = true;
108
- return remaining.length > 0 ? { ...matcher, hooks: remaining } : null;
109
- }).filter((m) => m !== null);
110
- if (!changed) return false;
111
- if (filtered.length === 0) {
112
- delete settings.hooks.SessionStart;
113
- if (Object.keys(settings.hooks).length === 0) {
114
- delete settings.hooks;
115
- }
116
- } else {
117
- settings.hooks.SessionStart = filtered;
118
- }
119
- writeSettings(settings);
120
- return true;
121
- }
122
-
123
- // src/lib/version.ts
124
- import { readFileSync as readFileSync2 } from "fs";
125
- import { dirname as dirname2, resolve } from "path";
126
- import { fileURLToPath } from "url";
127
- var PACKAGE_NAME = "@chainpatrol/cli";
128
- var cached;
129
- function getCliVersion() {
130
- if (cached !== void 0) return cached;
131
- cached = resolveCliVersion();
132
- return cached;
133
- }
134
- function resolveCliVersion() {
135
- try {
136
- const here = dirname2(fileURLToPath(import.meta.url));
137
- const candidates = [
138
- resolve(here, "..", "package.json"),
139
- resolve(here, "..", "..", "package.json")
140
- ];
141
- for (const candidate of candidates) {
142
- const version = tryReadVersion(candidate);
143
- if (version) return version;
144
- }
145
- } catch {
146
- }
147
- return "0.0.0";
148
- }
149
- function tryReadVersion(path) {
150
- try {
151
- const raw = readFileSync2(path, "utf-8");
152
- const pkg = JSON.parse(raw);
153
- if (pkg.name === PACKAGE_NAME && typeof pkg.version === "string") {
154
- return pkg.version;
155
- }
156
- } catch {
157
- }
158
- return void 0;
159
- }
160
- function compareVersions(a, b) {
161
- const parsed = (value) => {
162
- const hyphenIndex = value.indexOf("-");
163
- const main = hyphenIndex >= 0 ? value.slice(0, hyphenIndex) : value;
164
- const pre = hyphenIndex >= 0 ? value.slice(hyphenIndex + 1) : "";
165
- const parts = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
166
- while (parts.length < 3) parts.push(0);
167
- return { parts, pre };
168
- };
169
- const va = parsed(a);
170
- const vb = parsed(b);
171
- for (let i = 0; i < 3; i += 1) {
172
- if (va.parts[i] !== vb.parts[i]) return va.parts[i] - vb.parts[i];
173
- }
174
- if (va.pre === vb.pre) return 0;
175
- if (va.pre === "") return 1;
176
- if (vb.pre === "") return -1;
177
- return va.pre < vb.pre ? -1 : 1;
178
- }
179
-
180
- // src/commands/setup-skill.ts
181
- var SKILL_DIR = join2(homedir2(), ".claude", "skills", "chainpatrol");
182
- var SKILL_FILE = join2(SKILL_DIR, "SKILL.md");
183
- function buildSkillContent(version) {
184
- return `---
185
- name: chainpatrol
186
- version: ${version}
187
- description: |
188
- ChainPatrol CLI assistant. Helps use the chainpatrol CLI tool: login via device
189
- code flow, check auth status, list detection configs, list reports (including
190
- customer-reported ones), run CLI commands, and run an organization
191
- healthcheck across the detection / reviewing / blocklisting / takedown
192
- pipeline.
193
- Use when: "chainpatrol cli", "login to chainpatrol", "check detection configs",
194
- "am I logged in", "list configs", "use the cli", "list reports",
195
- "customer reports", "reports reported by customer", "find detection gaps",
196
- "org healthcheck", "organization health check", "audit my org",
197
- "what's wrong with org", "review org setup", "list orgs", "list organizations",
198
- "get org", "get organization", "show org details", "org by slug",
199
- "fetch org details", "org info",
200
- "orgs with takedowns off", "automation off across orgs",
201
- "which customers have X enabled", "service toggles by org",
202
- "obligatory admin approval", "obligatory organization admin approval",
203
- "obligatory approval", "admin approval enabled", "admin approval orgs",
204
- "requires customer review", "requires admin approval",
205
- "orgs that require admin approval", "orgs requiring approval",
206
- "which orgs require approval for twitter", "approval scope",
207
- "admin approval asset types", "approval per asset type",
208
- "pending approval", "pending approvals", "pending service approval",
209
- "orgs with pending approval", "pending wallet blocking approval",
210
- "pending takedown approval", "awaiting approval", "waiting for approval",
211
- "is protection active approval", "wallet blocking approval",
212
- "who needs to approve wallet blocking", "service change request",
213
- "track orgs with pending approval", "orgs awaiting sign-off",
214
- "is this URL blocked", "is this domain blocked", "is this address blocked",
215
- "check this asset", "asset check", "lookup asset status",
216
- "what is ARCHIVE_ORG", "what does PAGE mean", "list asset types",
217
- "supported asset types", "asset type mapping", "asset type enum",
218
- "what asset types are there", "human readable asset type",
219
- "how many takedowns", "takedowns in the last", "threats taken down",
220
- "across all clients", "across all customers", "across all orgs",
221
- "across all brands", "company-wide", "total takedowns", "total threats",
222
- "total reports", "average takedowns", "average threats", "average per day",
223
- "average per customer", "average per org", "rollup across customers",
224
- "sum across orgs", "sum across customers",
225
- "trends in org", "search for trends", "trend search", "look for trends",
226
- "any trends", "trending threats", "spike in asset type",
227
- "spike in threat volume", "coordinated attack", "spike check",
228
- "anything unusual", "anything new for", "what's new for",
229
- "employee being targeted", "spike on a sub-brand", "spike on a brand",
230
- "sub-brand spike",
231
- "list brands", "brands for org", "list sub-brands", "employee brands",
232
- "individual brands", "brand list", "all brands in org".
233
- allowed-tools:
234
- - Bash
235
- - Read
236
- - Grep
237
- - Glob
238
- ---
239
-
240
- # ChainPatrol CLI Skill
241
-
242
- You are a ChainPatrol CLI assistant. Help the user interact with the ChainPatrol
243
- platform using the CLI tool.
244
-
245
- ## Running the CLI
246
-
247
- IMPORTANT: Claude Code's sandbox shell often has a minimal PATH
248
- (\`/usr/bin:/bin:/usr/sbin:/sbin\`) that may not include the directory where
249
- \`chainpatrol\` is installed, so bare \`chainpatrol\` calls may fail with
250
- "command not found". Always invoke the CLI by its full path.
251
-
252
- The install location depends on the environment:
253
-
254
- - **Local installs** typically land at \`/usr/local/bin/chainpatrol\` (when
255
- installed globally via \`npm install -g @chainpatrol/cli\`).
256
- - **Cloud / sandboxed environments** (e.g. Claude Code on the web, Cursor
257
- Cloud) often install Node into \`/opt\` and the binary ends up under a
258
- Node-version-specific path like \`/opt/node22/bin/chainpatrol\`. Variants
259
- such as \`/opt/node20/bin/chainpatrol\` or \`/opt/node21/bin/chainpatrol\`
260
- are also possible depending on which Node version is active.
261
-
262
- To find the binary, try (in order):
263
-
264
- \`\`\`bash
265
- command -v chainpatrol \\
266
- || ls /usr/local/bin/chainpatrol /opt/node*/bin/chainpatrol 2>/dev/null \\
267
- | head -n 1
268
- \`\`\`
269
-
270
- Then use that full path for every subsequent command, e.g.:
271
-
272
- \`\`\`bash
273
- /opt/node22/bin/chainpatrol <command> [options]
274
- # or
275
- /usr/local/bin/chainpatrol <command> [options]
276
- \`\`\`
277
-
278
- All examples below use the short name \`chainpatrol\` for readability, but you
279
- MUST substitute the full resolved path in your Bash commands.
280
-
281
- ## Available Commands
282
-
283
- ### \`login\` \u2014 Authenticate with ChainPatrol
284
-
285
- Uses the OAuth Device Code flow (RFC 8628):
286
- 1. CLI requests a device code from the server
287
- 2. User is shown a code and a URL to visit
288
- 3. User authorizes in the browser
289
- 4. CLI polls for the token
290
-
291
- \`\`\`bash
292
- chainpatrol login
293
- \`\`\`
294
-
295
- JSON mode (for automation):
296
- \`\`\`bash
297
- chainpatrol --json login
298
- \`\`\`
299
-
300
- #### Running \`login\` from an agent (headless / non-TTY)
301
-
302
- The login flow blocks for up to 30 minutes while polling for the user to
303
- authorize in their browser. If you (the agent) run it as a foreground
304
- command and wait for it to exit before reading output, you will appear
305
- stuck \u2014 the verification URL will never be shown because the process is
306
- still polling.
307
-
308
- **Always run \`login\` in the background and stream its output**, then
309
- surface the verification URL to the user as soon as it appears:
310
-
311
- \`\`\`bash
312
- # 1. Kick off login in the background (do NOT wait for it to exit)
313
- chainpatrol --json login > /tmp/cp-login.out 2>&1 &
314
-
315
- # 2. Wait briefly for the first line, which contains the URL
316
- for _ in 1 2 3 4 5 6 7 8 9 10; do
317
- test -s /tmp/cp-login.out && break
318
- sleep 1
319
- done
320
- cat /tmp/cp-login.out
321
- \`\`\`
322
-
323
- The first line emitted is JSON describing the device code, e.g.:
324
-
325
- \`\`\`json
326
- {"action":"open_url","user_code":"ABCD-1234","verification_uri":"https://app.chainpatrol.io/auth/verify-device","verification_uri_complete":"https://app.chainpatrol.io/auth/verify-device?user_code=ABCD-1234","expires_in":1800,"headless":true}
327
- \`\`\`
328
-
329
- Show the user the \`verification_uri_complete\` link (or
330
- \`verification_uri\` + \`user_code\` as a fallback) and explain that the
331
- CLI will pick up the token automatically once they authorize. Then keep
332
- the background process running and tail it for the final
333
- \`{"status":"success",...}\` or \`{"error":...}\` line.
334
-
335
- CLI v0.3.3+ also auto-detects non-TTY stdout and prints the URL as plain
336
- text immediately when you run \`chainpatrol login\` without \`--json\`,
337
- so the same background+tail pattern works without \`--json\`. Prefer
338
- \`--json\` so the output is structured and machine-parseable.
339
-
340
- ### \`logout\` \u2014 Clear stored credentials
341
-
342
- \`\`\`bash
343
- chainpatrol logout
344
- \`\`\`
345
-
346
- ### \`asset check\` \u2014 Check one or many assets against the blocklist
347
-
348
- Look up a URL, domain, or crypto address and return its aggregated status
349
- (\`BLOCKED\`, \`ALLOWED\`, or \`UNKNOWN\`) plus a per-source breakdown
350
- (ChainPatrol + external feeds like eth-phishing-detect, phishfort, seal,
351
- polkadot-phishing). Works whether you're authenticated via device-code
352
- login or via a \`CHAINPATROL_API_KEY\` env var.
353
-
354
- Single asset:
355
-
356
- \`\`\`bash
357
- chainpatrol asset check https://phish.example
358
- chainpatrol asset check 0xabc123...
359
- \`\`\`
360
-
361
- #### Bulk checks (preferred for >1 asset)
362
-
363
- Pass multiple assets in a single invocation \u2014 the CLI runs them in
364
- parallel (concurrency 10) and returns one row per asset. **Do this
365
- instead of looping the CLI in a shell \`for\` loop**: one process, one
366
- auth handshake, parallel HTTP. Use either positional args or repeated
367
- \`--asset\`:
368
-
369
- \`\`\`bash
370
- # positional form
371
- chainpatrol asset check a.example b.example c.example
372
-
373
- # repeated --asset (handy when content has spaces or special chars)
374
- chainpatrol asset check --asset a.example --asset b.example
375
-
376
- # from a file of one-asset-per-line (use xargs to splat into one call)
377
- xargs -a domains.txt chainpatrol asset check
378
- \`\`\`
379
-
380
- JSON mode is the agent-friendly default \u2014 single-asset JSON keeps the
381
- flat \`{ content, status, source, reason?, sources[], watchStatus? }\`
382
- shape; multi-asset JSON returns \`{ results: [...], summary: { checked,
383
- blocked, allowed, unknown, errored } }\`:
384
-
385
- \`\`\`bash
386
- chainpatrol --json asset check https://phish.example
387
- chainpatrol --json asset check a.example b.example c.example
388
- \`\`\`
389
-
390
- Markdown / CSV are also available for sharing in docs / chat:
391
-
392
- \`\`\`bash
393
- chainpatrol asset check phish.example --output markdown
394
- chainpatrol asset check a.example b.example --output csv
395
- \`\`\`
396
-
397
- If any individual lookup fails, the CLI still prints results for the
398
- successful ones, then exits non-zero so failures aren't silently
399
- swallowed.
400
-
401
- ### \`asset types\` \u2014 List every supported asset type and what it means
402
-
403
- When a user asks "what is \`ARCHIVE_ORG\`?", "what does \`PAGE\` mean?",
404
- or "what asset types does ChainPatrol support?" \u2014 don't guess. Run:
405
-
406
- \`\`\`bash
407
- chainpatrol asset types
408
- chainpatrol --json asset types
409
- \`\`\`
410
-
411
- The mapping is bundled with the CLI (no API or auth required), so this
412
- is safe to run anywhere. Each row is \`{ type, label, description }\`:
413
- \`type\` is the canonical enum value used by every \`--asset-type\` flag
414
- (\`asset list\`, \`threats list\`, \`takedowns list\`, \`detections list\`,
415
- \`reports list\`, \`orgs assets list\`); \`label\` is the human-friendly
416
- display name (\`ARCHIVE_ORG\` \u2192 \`Archive.org\`, \`PAGE\` \u2192 \`Page\`,
417
- \`FIVE_HUNDRED_PX\` \u2192 \`500px\`); \`description\` is a one-line note for
418
- unobvious entries.
419
-
420
- Use this whenever you need to translate between enum and display name,
421
- validate that a type the user mentioned is real, or enumerate options
422
- before constructing a filter.
423
-
424
- ### \`configs list\` \u2014 List detection configurations
425
-
426
- Requires authentication and an organization slug.
427
-
428
- \`\`\`bash
429
- chainpatrol configs list --org <slug>
430
- \`\`\`
431
-
432
- JSON mode:
433
- \`\`\`bash
434
- chainpatrol --json configs list --org <slug>
435
- \`\`\`
436
-
437
- The \`--org\` flag is saved for future commands. Once set, you can omit it:
438
- \`\`\`bash
439
- chainpatrol configs list
440
- \`\`\`
441
-
442
- ### \`reports list\` \u2014 List recent reports for an organization
443
-
444
- Returns the most recent reports submitted for an organization. Each report
445
- includes a \`reportedByCustomer\` boolean indicating whether the report was
446
- submitted by a customer of ChainPatrol (e.g. via API key, Slack/Telegram bot,
447
- or another external integration) rather than by ChainPatrol's automated
448
- detections or staff reviewers.
449
-
450
- \`\`\`bash
451
- chainpatrol reports list --org <slug>
452
- \`\`\`
453
-
454
- Common flags:
455
- - \`--limit <n>\` page size (1-20)
456
- - \`--cursor <id>\` pagination cursor (use \`nextCursor\` from a previous response)
457
- - \`--status <s>\` filter by report status (e.g. \`TODO\`, \`IN_PROGRESS\`, \`DONE\`)
458
- - \`--search <q>\` search query across title/description/asset content
459
- - \`--reported-by-customer\` only show reports submitted by a customer
460
- - \`--no-reported-by-customer\` only show reports NOT submitted by a customer
461
- (i.e. found by ChainPatrol's automation or staff)
462
-
463
- JSON mode is recommended when you want to analyze the data programmatically:
464
- \`\`\`bash
465
- chainpatrol --json reports list --org <slug> --reported-by-customer
466
- \`\`\`
467
-
468
- #### Use case: finding gaps in ChainPatrol detection
469
-
470
- Customer-reported reports (\`reportedByCustomer=true\`) are a valuable signal
471
- for finding gaps in ChainPatrol's automated detections and staff triage: each
472
- one is a threat the customer found before ChainPatrol's own systems did. When
473
- the user asks something like:
474
-
475
- - "show me the threats our customers are reporting"
476
- - "what is X's customer reporting?"
477
- - "where are we missing detections for org Y?"
478
- - "summarize recent reports submitted by customers"
479
-
480
- \u2026use \`chainpatrol --json reports list --org <slug> --reported-by-customer\`
481
- to fetch them, then highlight patterns (asset types, domains, common
482
- keywords, recurring brands) so the user can:
483
- 1. Spot detection coverage gaps to fix in the org's detection configs.
484
- 2. Improve staff triage runbooks for recurring scams.
485
- 3. Work directly with the customer to close the loop and prevent future
486
- misses.
487
-
488
- You can also compare with the non-customer set using
489
- \`--no-reported-by-customer\` to gauge detection coverage on the same time
490
- window.
491
-
492
- ### \`detections healthcheck\` \u2014 Validate enabled detection configs produce recent results
493
-
494
- Server-side check. The CLI calls the ChainPatrol API; the server fetches each
495
- enabled detection config for the org, counts results produced in the lookback
496
- window, and FAILs configs that fall under \`--min-results\` (or whose run
497
- errored when \`--run\` is set).
498
-
499
- \`\`\`bash
500
- chainpatrol --json detections healthcheck --org <slug>
501
- \`\`\`
502
-
503
- Flags:
504
- - \`--source <key>\` only validate one source (e.g. \`twitter_search\`)
505
- - \`--min-results <n>\` minimum results required in the window to pass
506
- - \`--lookback-hours <n>\` size of the lookback window
507
- - \`--run\` ask the server to run each config first, then validate the fresh output
508
- - \`--include-disabled\` also validate disabled configs
509
-
510
- What this command covers:
511
- - Configs that have gone silent (recentResultCount below threshold)
512
- - Configs that error when run (runOk=false) when \`--run\` is set
513
-
514
- What it does NOT cover:
515
- - Reviewing backlog / SLA breaches \u2192 use \`queues snapshot\`
516
- - Takedown ToDo / In Progress / Cancelled volumes \u2192 use \`queues snapshot\`
517
- - Spikes or drops in detection volume over time \u2192 use \`metrics breakdown\`
518
- - Customer-reported gaps \u2192 use \`reports list --reported-by-customer\`
519
- - Google Safe Browsing submission errors (not yet exposed in CLI)
520
-
521
- Use it as the first signal in the Detection part of an org healthcheck, then
522
- fall back to the manual checks in the HealthCheck Guide below for everything
523
- else.
524
-
525
- > Prefer the newer \`healthchecks\` namespace below. \`detections healthcheck\`
526
- > is the original single-purpose command; the \`healthchecks\` namespace is
527
- > the canonical place to discover and run every check we expose.
528
-
529
- ### \`healthchecks list | run\` \u2014 Run uniform org healthchecks via the public API
530
-
531
- The \`healthchecks\` namespace is the canonical way to run the named checks
532
- from the Organization HealthCheck Guide below. Each implemented endpoint
533
- returns the same uniform shape \u2014 \`{ id, ok, severity, observed, threshold,
534
- findings, suggestedAction }\` \u2014 so the CLI / agent can render every check the
535
- same way regardless of category.
536
-
537
- \`\`\`bash
538
- # Discover every check the platform exposes today, including planned checks
539
- # that are not yet implemented on the backend.
540
- chainpatrol --json healthchecks list
541
-
542
- # Run a single named check.
543
- chainpatrol --json healthchecks run reviewing.backlog --org <slug>
544
-
545
- # Run every implemented check in parallel and aggregate the results.
546
- chainpatrol --json healthchecks run --all --org <slug>
547
- \`\`\`
548
-
549
- Each implemented check has a stable id of the form \`category.name\`. Implemented
550
- ids today: \`detections.silent-configs\`, \`reviewing.backlog\`,
551
- \`reviewing.old-proposals\`, \`reviewing.watchlist-backlog\`,
552
- \`reviewing.watchlist-old\`, \`takedowns.todo-volume\`,
553
- \`takedowns.in-progress-volume\`, \`takedowns.stale-in-progress\`,
554
- \`takedowns.cancelled-count\`, \`takedowns.automation-off\`,
555
- \`assets.dead-asset-spike\`.
556
-
557
- ### Pending proposals: "Needs Review" vs "Watchlisted"
558
-
559
- PENDING proposals split into two operationally distinct buckets, and we
560
- grade them with separate checks:
561
-
562
- - **Needs Review** \u2014 pending proposals the reviewing UI shows by default
563
- (\`excludeWatchlisted=true\`): assets that are NOT on a watchlist, OR
564
- reports submitted by a customer (those stay visible even when the asset
565
- is watchlisted). This is the actionable queue reviewers work from, so
566
- pile-ups and old items here are high-priority signals (\`fail\` severity
567
- is reachable).
568
- - **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
569
- customer-reported reports). The reviewing UI hides these by default
570
- because watchlisting is the act of intentionally deferring an asset.
571
- Pile-ups and aged items here are worth surfacing as cleanup work, but
572
- severity is **capped at warn** so they never block on the same SLA as
573
- Needs Review. When reporting findings, treat these as lower-priority.
574
-
575
- Each healthcheck result includes an \`appUrl\` field (string or null) that
576
- deep-links to the relevant filtered admin page in the web app \u2014 e.g. the
577
- takedowns page filtered to IN_PROGRESS for \`takedowns.stale-in-progress\`,
578
- or the review page filtered to oldest pending for \`reviewing.old-proposals\`.
579
- **When reporting a non-OK healthcheck to the user, always surface the
580
- \`appUrl\` so they can jump straight to the right view.** Some checks
581
- (\`detections.silent-configs\`, \`assets.dead-asset-spike\`) emit \`null\`
582
- because no filterable list page exists for that signal yet.
583
-
584
- Implemented checks today:
585
-
586
- - **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
587
- exposed under the uniform shape.
588
- - **reviewing.backlog** \u2014 counts Needs Review pending proposals and grades
589
- severity against per-org thresholds (default warn=50, fail=100).
590
- - **reviewing.old-proposals** \u2014 counts Needs Review proposals older than
591
- the warn / fail age thresholds (default 7 / 14 days) and lists the
592
- oldest offenders.
593
- - **reviewing.watchlist-backlog** \u2014 counts watchlisted pending proposals
594
- (default warn=200). Severity capped at warn.
595
- - **reviewing.watchlist-old** \u2014 counts watchlisted pending proposals older
596
- than the warn-age threshold (default 30 days) and lists the oldest.
597
- Severity capped at warn.
598
- - **takedowns.todo-volume** \u2014 counts takedowns in TODO (default warn=50,
599
- fail=100). Pile-ups here usually mean an automation gap on a new threat
600
- surface, or manual-filing capacity issues.
601
- - **takedowns.in-progress-volume** \u2014 counts takedowns currently IN_PROGRESS
602
- regardless of age (default warn=30, fail=75). Complements
603
- \`stale-in-progress\` \u2014 a high count signals vendor-side or
604
- submission-format problems even before items go stale.
605
- - **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
606
- past the staleness threshold (default 7 days) and lists the oldest.
607
- - **takedowns.cancelled-count** \u2014 counts CANCELLED transitions from the
608
- TakedownEvent log over a rolling window (default 7d, warn=3, fail=10).
609
- Cancellations should be rare; a spike usually means a proposal-funnel
610
- quality problem or misuse of the CANCELLED status.
611
- - **takedowns.automation-off** \u2014 flags orgs with takedown service enabled
612
- but \`isAutomatedTakedownsActive\` off for too long (default warn=30d,
613
- fail=60d). Skipped for orgs with takedown service entirely disabled.
614
- - **assets.dead-asset-spike** \u2014 compares DEAD-detection events in the
615
- current window against the prior baseline rate; warns on a multiplier
616
- exceeding the threshold (default 24h vs 7d, \xD72 warn / \xD74 fail) once the
617
- current count clears the \`minSpikeCount\` floor. Catches liveness-checker
618
- regressions after platform changes.
619
-
620
- The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
621
- but **not yet implemented on the backend** \u2014 when the agent surfaces them in
622
- a healthcheck report, mark them explicitly as "manual check, no API yet":
623
-
624
- - **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
625
- Still requires manual reasoning with \`configs list\` + \`reports list\`.
626
- - **detections.spike** / **detections.drop** \u2014 require server-side baseline
627
- modeling. Use \`metrics breakdown --by day\` as an interim signal.
628
- - **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
629
- human approvers in the review history. Use \`metrics breakdown\` as a proxy.
630
- - **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
631
- is not yet exposed in the public API.
632
- - **assets.dead-but-alive** / **assets.alive-but-marked-dead** \u2014 require live
633
- HTTP probes against asset URLs, which is not a synchronous-healthcheck
634
- shape. Until a dedicated probe command exists, sample a handful manually
635
- from \`assets list --status DEAD\` (or ALIVE) and verify in a browser. Notify
636
- ChainPatrol engineering if the liveness checker looks miscalibrated.
637
-
638
- When the user asks to "run a healthcheck on org X", the canonical command is:
639
-
640
- \`\`\`bash
641
- chainpatrol --json healthchecks run --all --org X
642
- \`\`\`
643
-
644
- This iterates the implemented entries in \`healthchecks list\`, runs them in
645
- parallel, and aggregates the uniform results. Combine with the manual checks
646
- in the HealthCheck Guide below for everything still marked
647
- \`implemented: false\`.
648
-
649
- ### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
650
-
651
- Server-side aggregation of the operations review queue (pending proposals,
652
- SLA breaches, age buckets) and the takedown queue (open, in-progress, stale).
653
-
654
- \`\`\`bash
655
- chainpatrol --json queues snapshot --org <slug>
656
- \`\`\`
657
-
658
- Useful for the **Reviewing** and **Takedowns** sections of the HealthCheck
659
- Guide. Key signals in the response:
660
- - \`reviewQueue.totalPendingProposals\` and \`reviewQueue.distinctReports\` \u2014
661
- backlog size
662
- - \`reviewQueue.slaBuckets.breached\` \u2014 SLA breaches (treat any breach as a
663
- finding)
664
- - \`reviewQueue.ageBuckets.gte168h\` \u2014 proposals older than 7 days (anything
665
- >14 days from the manual guide should always be in here)
666
- - \`takedownQueue.totalOpen\` and \`takedownQueue.staleInProgress\` \u2014 open
667
- and stuck takedowns
668
-
669
- Use \`--all\` to snapshot every org you have access to instead of a single slug.
670
-
671
- ### \`orgs list\` \u2014 List organizations with subscription status, service toggles, and admin-approval scope
672
-
673
- Returns every organization the caller can see, with each org's
674
- subscription status (\`PROSPECT\`, \`TRIAL\`, \`ACTIVE\`, \`INTEGRATION\`),
675
- which services are active, and the per-org "Obligatory Organization
676
- Admin Approval" toggle (with its asset-type scope). Use it to answer
677
- questions like "which customers have takedowns enabled but automation
678
- off?", "which prospects don't have detection turned on yet?", or "which
679
- orgs require admin approval before adding Twitter assets to the
680
- blocklist?" \u2014 filters compose with AND and are applied server-side, so
681
- one call returns the final list.
682
-
683
- \`\`\`bash
684
- chainpatrol --json orgs list \\
685
- --subscription-status ACTIVE \\
686
- --service-active takedowns \\
687
- --service-manual takedowns
688
- \`\`\`
689
-
690
- #### Per-service response shape \u2014 \`automated\` is takedowns-only
691
-
692
- Every service exposes an \`active\` boolean. **Only \`takedowns\`
693
- additionally exposes \`automated\`** \u2014 that is the one service where the
694
- manual-vs-automated distinction changes real platform behavior (whether
695
- ChainPatrol files the takedown on its own, or queues it for a human to
696
- file). \`reporting\`, \`reviewing\`, and \`protection\` have backing
697
- \`isAutomated*Active\` columns in the database, but those flags have no
698
- operational effect today, so the API deliberately omits them from both
699
- the response and the \`services\` filter \u2014 surfacing them would invite
700
- misleading filters like "reporting.automated=true" that don't mean
701
- anything. \`detection\` and \`darkWebMonitoring\` have always been
702
- single-flag services.
703
-
704
- #### Obligatory Organization Admin Approval
705
-
706
- Each org also has a separate "Obligatory Organization Admin Approval"
707
- toggle (\`Organization.requiresCustomerReview\` in the schema, surfaced
708
- on the Services settings page in the app) that gates customer-side
709
- review of proposals ChainPatrol staff have already approved before
710
- they're added to the blocklist. It is NOT one of the operational
711
- services above \u2014 it is an org-policy toggle with its own per-asset-type
712
- scope.
713
-
714
- Each org's response includes:
715
-
716
- \`\`\`json
717
- {
718
- "obligatoryAdminApproval": {
719
- "active": true,
720
- "assetTypes": ["TWITTER", "URL"]
721
- }
722
- }
723
- \`\`\`
724
-
725
- - \`active=true\` + empty \`assetTypes\` means approval applies to
726
- **all** asset types \u2014 the org has not narrowed the scope.
727
- - \`active=true\` + a non-empty \`assetTypes\` list means approval is
728
- required only for those asset types.
729
- - \`active=false\` means no approval is required; \`assetTypes\` is
730
- always empty in that case.
731
-
732
- So per-org JSON looks like:
733
-
734
- \`\`\`json
735
- {
736
- "services": {
737
- "reporting": { "active": true },
738
- "reviewing": { "active": true },
739
- "protection": { "active": false },
740
- "takedowns": { "active": true, "automated": false },
741
- "detection": { "active": true },
742
- "darkWebMonitoring": { "active": false }
743
- },
744
- "obligatoryAdminApproval": { "active": true, "assetTypes": ["TWITTER"] },
745
- "pendingServiceApprovals": [
746
- {
747
- "service": "protection",
748
- "automated": false,
749
- "serviceType": "isProtectionActive",
750
- "serviceName": "Wallet Blocking",
751
- "requestedAt": "2026-05-30T18:04:11.000Z"
752
- }
753
- ]
754
- }
755
- \`\`\`
756
-
757
- #### Pending service approvals (Wallet Blocking / Takedowns)
758
-
759
- Turning on **Wallet Blocking** (\`protection\`, the "is protection active"
760
- toggle) or **Takedowns** is approval-gated: when a lower-level org member
761
- tries to enable one, ChainPatrol records a pending request that a higher-up
762
- at the org \u2014 an org OWNER, or ChainPatrol staff acting as owner \u2014 must
763
- approve before the service actually turns on. \`pendingServiceApprovals\`
764
- lists those open, awaiting-sign-off requests per org.
765
-
766
- This is the answer to "which organizations have a pending approval for
767
- Wallet blocking or Takedown services?" \u2014 it is NOT the same as "which orgs
768
- have Wallet Blocking turned off". An org can have \`protection.active=false\`
769
- with **no** pending approval (nobody has asked) OR **with** a pending
770
- approval (someone asked and it's waiting on an owner). Only
771
- \`pendingServiceApprovals\` distinguishes the two.
772
-
773
- Each entry:
774
-
775
- - \`service\` \u2014 \`protection\` (Wallet Blocking) or \`takedowns\`, matching the
776
- \`services\` keys.
777
- - \`automated\` \u2014 \`true\` when the request is for the automated variant
778
- (e.g. "Automated Wallet Blocking").
779
- - \`serviceType\` \u2014 raw enum, e.g. \`isProtectionActive\`.
780
- - \`serviceName\` \u2014 human label, e.g. \`Wallet Blocking\`.
781
- - \`requestedAt\` \u2014 when the enable request was submitted (ISO 8601, UTC).
782
-
783
- An **empty array means nothing is awaiting approval** \u2014 read \`services\` for
784
- the current on/off state, never infer it from this field.
785
-
786
- When a user asks about "automated reporting / reviewing / protection",
787
- explain that the flag exists in the DB but has no operational effect and
788
- isn't exposed by the public API \u2014 only \`takedowns.automated\` carries a
789
- real meaning.
790
-
791
- Filter flags (all optional, all comma-separated lists where noted):
792
-
793
- - \`--query <text>\` partial name match (substring, case-insensitive)
794
- - \`--subscription-status <list>\` one or more of \`PROSPECT\`, \`TRIAL\`,
795
- \`ACTIVE\`, \`INTEGRATION\`. \`INACTIVE\` is intentionally not reachable
796
- through this filter \u2014 \`orgs list\` only ever returns live customers.
797
- - \`--service-active <list>\` services that must be active
798
- - \`--service-inactive <list>\` services that must be inactive
799
- - \`--service-automated <list>\` services whose automation must be ON.
800
- **Only \`takedowns\` is accepted** \u2014 the other services don't have a
801
- meaningful automation toggle. Passing any other service name errors out.
802
- - \`--service-manual <list>\` services whose automation must be OFF.
803
- Same restriction \u2014 only \`takedowns\`.
804
- - \`--obligatory-approval-active\` \u2014 only orgs with admin approval on
805
- - \`--obligatory-approval-inactive\` \u2014 only orgs with admin approval off
806
- - \`--obligatory-approval-asset-type <list>\` \u2014 only orgs whose admin
807
- approval scope covers EVERY listed asset type. The match treats an
808
- org's empty per-asset-type list as "applies to all asset types", so a
809
- fully-broad org matches every value passed here. Passing this flag
810
- implies the feature is on, so it can't be combined with
811
- \`--obligatory-approval-inactive\`. Use the canonical asset-type enum
812
- names (\`TWITTER\`, \`URL\`, \`PAGE\`, \u2026); run \`chainpatrol asset types\`
813
- to see them all.
814
- - \`--pending-approval-active\` \u2014 only orgs with at least one open
815
- service-enable approval awaiting a higher-up's sign-off.
816
- - \`--pending-approval-inactive\` \u2014 only orgs with no pending approvals.
817
- - \`--pending-approval-service <list>\` \u2014 only orgs with a pending approval
818
- for one of the listed services. Accepts \`protection\` (Wallet Blocking)
819
- and/or \`takedowns\` \u2014 those are the only approval-gated services.
820
- Matches both the manual and automated variant. Passing this flag implies
821
- a pending approval exists, so it can't be combined with
822
- \`--pending-approval-inactive\`.
823
-
824
- Service names: \`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\`,
825
- \`detection\`, \`darkWebMonitoring\`.
826
-
827
- Customers see only orgs they're a member of. Staff/superuser sessions see
828
- every matching org. The response is the same in both cases; visibility is
829
- enforced server-side.
830
-
831
- #### Use case: finding which orgs require admin approval for an asset type
832
-
833
- When a user asks "which orgs require admin approval before blocking
834
- Twitter assets?", reach straight for the filter \u2014 no client-side
835
- post-processing needed:
836
-
837
- \`\`\`bash
838
- chainpatrol --json orgs list --obligatory-approval-asset-type TWITTER
839
- \`\`\`
840
-
841
- To audit just the broad opt-ins ("which orgs require approval for
842
- everything?"), filter on the toggle and inspect \`assetTypes\` in the
843
- output \u2014 an empty array means "all":
844
-
845
- \`\`\`bash
846
- chainpatrol --json orgs list --obligatory-approval-active \\
847
- | jq '.organizations[] | select(.obligatoryAdminApproval.assetTypes | length == 0) | .slug'
848
- \`\`\`
849
-
850
- #### Use case: tracking orgs with a pending Wallet Blocking / Takedown approval
851
-
852
- When a user asks "which organizations have a pending approval for Wallet
853
- blocking or Takedown services?", do NOT reach for \`--service-inactive
854
- protection\` \u2014 that lists orgs with the service turned OFF, which is a
855
- different question. Use the pending-approval filter, which surfaces
856
- staff-initiated enable requests still waiting on a higher-up:
857
-
858
- \`\`\`bash
859
- # Every org with a pending Wallet Blocking OR Takedown approval:
860
- chainpatrol --json orgs list --pending-approval-service protection,takedowns
861
-
862
- # Just Wallet Blocking:
863
- chainpatrol --json orgs list --pending-approval-service protection
864
-
865
- # Any pending approval at all, then read each org's pendingServiceApprovals:
866
- chainpatrol --json orgs list --pending-approval-active \\
867
- | jq '.organizations[] | {slug, pending: [.pendingServiceApprovals[].serviceName]}'
868
- \`\`\`
869
-
870
- ### \`orgs get\` \u2014 Get a single organization by slug
871
-
872
- Look up one organization the caller has access to. Returns the same per-org
873
- shape as a single row from \`orgs list\` \u2014 \`active\` for every service,
874
- plus \`automated\` on \`takedowns\` only (see the note above on why other
875
- services don't expose an automation flag), plus \`obligatoryAdminApproval\`
876
- (\`active\` + \`assetTypes\`) and \`pendingServiceApprovals\` (open Wallet
877
- Blocking / Takedown enable requests awaiting a higher-up's sign-off).
878
- Anything you could read from \`orgs list\` you can also read here without
879
- paging or filtering. Use it when the user names a specific customer ("show
880
- me acme's setup", "is takedowns automation on for morpho?", "does this org
881
- require admin approval for Twitter?", "is morpho waiting on a Wallet
882
- Blocking approval?") and you don't need the rest of the catalogue.
883
-
884
- \`\`\`bash
885
- chainpatrol orgs get <slug>
886
- chainpatrol --json orgs get <slug>
887
- \`\`\`
888
-
889
- Permission rules match \`orgs list\`:
890
-
891
- - Org-scoped API keys: must match the slug \u2014 querying another org returns
892
- 403.
893
- - User sessions / user-scoped API keys: need an active OrganizationMembership
894
- on the slug. Staff/superusers can read any org.
895
- - Soft-deleted orgs are not returned (404). A 404 also fires for orgs the
896
- caller would not be authorized to see \u2014 the endpoint deliberately does
897
- not distinguish "doesn't exist" from "you can't see this" beyond the
898
- generic 403/404 envelope.
899
-
900
- Prefer \`orgs get\` over \`orgs list\` + client-side filter whenever the
901
- caller already knows the slug; it's one round-trip and side-steps the
902
- list filters entirely.
903
-
904
- #### Use case: finding service configuration gaps across the customer base
905
-
906
- When the user asks something like "which customers are paying us but don't
907
- have takedowns automated yet?" or "any orgs running detection without
908
- takedowns?", reach for \`orgs list\` \u2014 it's the only command that exposes
909
- service flags across multiple orgs in one call. Run it in \`--json\` mode
910
- and summarize patterns by service or by subscription tier.
911
-
912
- ### \`brands list\` \u2014 List the brands (sub-brands) belonging to an org
913
-
914
- Returns every brand for the org the caller is authenticated as, both
915
- parent / product brands (\`type: "ORGANIZATION"\`) and individual /
916
- employee brands (\`type: "INDIVIDUAL"\`). Soft-deleted brands are excluded.
917
- The endpoint is org-scoped and inferred from auth \u2014 there's no \`--org\`
918
- flag \u2014 so org-scoped API keys, user API keys, and user sessions all work
919
- without extra arguments.
920
-
921
- \`\`\`bash
922
- chainpatrol brands list # all brands
923
- chainpatrol brands list --type INDIVIDUAL # employee / person brands only
924
- chainpatrol brands list --type ORGANIZATION # product / parent brands only
925
- chainpatrol --json brands list # machine-readable
926
- \`\`\`
927
-
928
- Each entry has \`{ id, slug, name, type, description, brandGroupId, createdAt }\`.
929
- Use this when you need to:
930
-
931
- - Distinguish employee vs. product brands ahead of time \u2014 e.g. while
932
- running a trend search and you want to highlight which spiking
933
- sub-brands are employees so the user gets a direct heads-up to the
934
- person involved.
935
- - Resolve a brand name the user mentions to its \`slug\` for use with
936
- \`metrics organization --brand-slug <slug>\` or as a \`brandId\` for
937
- \`takedowns list --brand <id>\`.
938
- - Audit a customer's setup \u2014 "how many employee brands does this org
939
- protect?" or "are there brands the org set up but never wired into a
940
- detection config?"
941
-
942
- ### \`metrics summary | found | breakdown | organization\` \u2014 Org metrics for spike/drop analysis
943
-
944
- #### Decision rule \u2014 read this before picking a metrics subcommand
945
-
946
- When the user asks for a number that **spans more than one customer/org/brand**
947
- \u2014 phrases like "across all clients", "across all customers", "across all orgs",
948
- "across all brands", "company-wide", "total takedowns", "total threats",
949
- "average takedowns per day across customers", "rollup across customers",
950
- "how many takedowns in the last 7 days?" (no org named) \u2014 the answer is
951
- **\`chainpatrol metrics organization --all-my-orgs\`** (or \`--slugs\`
952
- if you want a specific subset). \`summary\`, \`found\`, and \`breakdown\`
953
- are single-org commands and have **no** \`--slugs\`/\`--all-my-orgs\`
954
- form; only \`organization\` does. The multi-org form rolls totals +
955
- per-day / per-org-per-day averages + a per-org breakdown server-side
956
- in **one** HTTP call.
957
-
958
- **Anti-patterns to avoid:**
959
-
960
- - \u274C "There's no cross-org aggregation in the CLI." There is \u2014
961
- \`metrics organization --all-my-orgs\` (or \`--slugs\`). Don't bail
962
- out citing the docs without trying these flags.
963
- - \u274C Looping \`metrics summary --org X\` once per customer to sum
964
- client-side. The multi-org form is one round-trip; the loop is what
965
- made the endpoint 503-prone in the first place.
966
- - \u274C Calling \`orgs list\` to build a slug list when you just want
967
- "everything." Use \`--all-my-orgs\` and skip the enumeration entirely \u2014
968
- the server resolves the same set via shared logic. Save \`orgs list\`
969
- + \`--slugs\` for cases where the user asked for a specific subset.
970
- - \u274C Routing the user to Metabase / the data warehouse for a question
971
- the CLI can answer. Reach for Metabase only when the metric isn't
972
- in the \`--include\` list (\`reports\`, \`newThreats\`,
973
- \`threatsWatchlisted\`, \`takedownsFiled\`, \`takedownsCompleted\`,
974
- \`domainThreats\`, \`twitterThreats\`, \`telegramThreats\`,
975
- \`otherThreats\`, \`blockedByType\`, \`blockedByDay\`).
976
- - \u274C "Iterating 100+ orgs one-by-one isn't practical here." Right \u2014
977
- that's why you don't iterate. \`--all-my-orgs\` covers up to 500
978
- orgs in a single call; for larger fleets, narrow with
979
- \`--subscription-status\` / \`--service-active\`, or split an
980
- explicit \`--slugs\` list into batches and sum.
981
-
982
- #### Common cross-org recipe (copy and run)
983
-
984
- The shortest answer to "how many takedowns across all customers in the
985
- last 7 days?" is a single call \u2014 no \`orgs list\` step needed:
986
-
987
- \`\`\`bash
988
- chainpatrol --json metrics organization \\
989
- --all-my-orgs \\
990
- --subscription-status ACTIVE \\
991
- --service-active takedowns \\
992
- --include takedownsCompleted \\
993
- --from <YYYY-MM-DD 7 days ago> --to <YYYY-MM-DD today>
994
-
995
- # Read these fields from the JSON:
996
- # .scope.orgs \u2190 which orgs the server resolved
997
- # .metrics.takedownsCompleted \u2190 grand total across all orgs
998
- # .averages.perDay.takedownsCompleted \u2190 total / windowDays
999
- # .averages.perOrgPerDay.takedownsCompleted \u2190 total / numOrgs / windowDays
1000
- # .perOrg[slug].metrics.takedownsCompleted \u2190 per-customer breakdown
1001
- \`\`\`
1002
-
1003
- Replace \`takedownsCompleted\` with whatever metric the user asked about.
1004
- Replace the date range with whatever window they asked about (default
1005
- 3 months if they didn't say).
1006
-
1007
- If the user *asked* for a specific subset of customers ("how many for
1008
- acme, beta, gamma combined?"), use \`--slugs acme,beta,gamma\` instead
1009
- of \`--all-my-orgs\`. \`--slugs\` accepts up to 500 entries.
1010
-
1011
- **Auth note for \`--all-my-orgs\`:** requires a user session or a
1012
- user-scoped API key (it inherits your memberships). Org-scoped API
1013
- keys are rejected \u2014 they're pinned to a single org by design. If
1014
- \`whoami\` shows an org-scoped key, use a Bearer session instead or
1015
- ask the user to provision a user API key.
1016
-
1017
- #### Single-org examples
1018
-
1019
- \`\`\`bash
1020
- chainpatrol --json metrics summary --org <slug> # defaults to last 3 months
1021
- chainpatrol --json metrics summary --org <slug> --this-week
1022
- chainpatrol --json metrics breakdown --org <slug> --by day --this-week
1023
- chainpatrol --json metrics found --org <slug> --from <YYYY-MM-DD> --to <YYYY-MM-DD>
1024
- chainpatrol --json metrics organization --org <slug> # defaults to last 3 months
1025
- \`\`\`
1026
-
1027
- **Always operate on a bounded window.** When no \`--from\`/\`--to\`/\`--this-week\`
1028
- is passed, every metrics subcommand defaults to the trailing **last 3 months**
1029
- ending now. The default exists because unbounded org-wide aggregates over
1030
- multi-year history routinely time out at the platform layer (Vercel's
1031
- 30s function cap), which surfaces to clients as a 503. Stick to the default,
1032
- or pass an explicit narrower window \u2014 don't try to bypass the default by
1033
- guessing wide \`--from\` values.
1034
-
1035
- The resolved range is echoed back in every output format so the user can
1036
- see exactly what they got:
1037
-
1038
- - JSON: \`{ "range": { "startDate": "...", "endDate": "...", "label": "last 3 months" } }\`
1039
- - Markdown / human: the header line includes the label, e.g.
1040
- \`Organization metrics for acme \u2014 last 3 months\`, followed by
1041
- \`Range: <startDate> \u2192 <endDate>\`.
1042
-
1043
- When reporting numbers to the user (especially averages and rates),
1044
- **state the window explicitly** \u2014 say "averaged over the last 3 months"
1045
- rather than just quoting a number, since the same prompt phrased
1046
- differently can pick a different window.
1047
-
1048
- \`breakdown\` is the one you usually want for healthchecks: it returns a
1049
- time series (by day or week) of reports, new threats, watchlisted threats,
1050
- and takedowns filed/completed. Compare the latest period against a prior
1051
- window to spot the **spike** or **drop** signals described in the manual
1052
- HealthCheck Guide. \`summary\` returns a single window total; \`found\` is
1053
- oriented around when threats were first discovered; \`organization\` is
1054
- the full customer-facing dashboard slice (reports, new threats,
1055
- watchlisted, takedowns filed/completed, plus per-type and per-day
1056
- breakdowns).
1057
-
1058
- #### \`--include\` \u2014 only compute the metrics you actually need
1059
-
1060
- \`metrics organization\` runs one Prisma aggregate per requested field.
1061
- By default all 11 are computed in parallel. Pass \`--include\` (or
1062
- \`include: [\u2026]\` in the JSON body) with the comma-separated subset you
1063
- care about to skip the rest \u2014 the unrequested fields come back as
1064
- \`null\` instead of a number, and the server never runs those queries:
1065
-
1066
- \`\`\`bash
1067
- # Only takedowns \u2014 one of the cheap shapes
1068
- chainpatrol --json metrics organization --org <slug> \\
1069
- --include takedownsFiled,takedownsCompleted
1070
-
1071
- # Time series only, no scalar counts
1072
- chainpatrol --json metrics organization --org <slug> --include blockedByDay
1073
- \`\`\`
1074
-
1075
- Allowed values: \`reports\`, \`newThreats\`, \`threatsWatchlisted\`,
1076
- \`takedownsFiled\`, \`takedownsCompleted\`, \`domainThreats\`,
1077
- \`twitterThreats\`, \`telegramThreats\`, \`otherThreats\`,
1078
- \`blockedByType\`, \`blockedByDay\`.
1079
-
1080
- Use this whenever the user's question is specific ("how many takedowns
1081
- did we file last month?"). It is the lowest-effort way to make a
1082
- metrics call cheap enough to run repeatedly. Pair it with an explicit
1083
- date window for the best behavior.
1084
-
1085
- #### \`--slugs\` \u2014 multi-org rollups in one call (use this for "across all customers/clients")
1086
-
1087
- When the user asks for a total or an average **across more than one
1088
- org** \u2014 e.g. "how many takedowns did we complete across all clients
1089
- in the last 7 days?", "average reports per org per day this month",
1090
- "top 5 customers by new threats" \u2014 reach for \`--slugs\` instead of
1091
- looping. The server fans out the per-org aggregates internally,
1092
- sums into totals, computes per-day and per-org-per-day averages,
1093
- and returns a per-org breakdown in a **single HTTP round trip**:
1094
-
1095
- \`\`\`bash
1096
- # Total takedowns completed across three customers, last 7 days
1097
- chainpatrol --json metrics organization \\
1098
- --slugs acme,beta,gamma \\
1099
- --include takedownsCompleted \\
1100
- --from 2026-05-12 --to 2026-05-19
1101
- \`\`\`
1102
-
1103
- The response shape switches to multi-org:
1104
-
1105
- \`\`\`json
1106
- {
1107
- "scope": { "mode": "multi", "orgs": ["acme", "beta", "gamma"] },
1108
- "metrics": { "takedownsCompleted": 117, ... }, // sum across orgs
1109
- "averages": {
1110
- "perDay": { "takedownsCompleted": 16.71, ... }, // total / windowDays
1111
- "perOrgPerDay": { "takedownsCompleted": 5.57, ... }, // total / numOrgs / windowDays
1112
- "windowDays": 7
1113
- },
1114
- "perOrg": { "acme": { "metrics": { ... } }, "beta": { ... }, "gamma": { ... } }
1115
- }
1116
- \`\`\`
1117
-
1118
- To answer **"across all of our customers"**, prefer \`--all-my-orgs\` \u2014
1119
- it lets the server resolve the org set in one round-trip, no \`orgs
1120
- list\` step needed:
1121
-
1122
- \`\`\`bash
1123
- chainpatrol --json metrics organization \\
1124
- --all-my-orgs \\
1125
- --subscription-status ACTIVE \\
1126
- --service-active takedowns \\
1127
- --include takedownsCompleted \\
1128
- --from 2026-05-12 --to 2026-05-19
1129
- \`\`\`
1130
-
1131
- Use \`--slugs <comma-list>\` only when the user asked for a *specific*
1132
- subset of customers ("how many for acme, beta, gamma combined?"); the
1133
- two flags are mutually exclusive. Both forms cap at 500 orgs per call;
1134
- if a real fleet exceeds that, narrow with
1135
- \`--subscription-status\`/\`--service-active\` filters or split an
1136
- explicit \`--slugs\` list into batches and sum. **Do not loop
1137
- \`metrics organization\` once per org** \u2014 N HTTP round-trips is what
1138
- made the old code 503.
1139
-
1140
- Always pair the multi-org form with \`--include\` to narrow the metric
1141
- set to what the user actually asked about. "How many takedowns did
1142
- we complete?" \u2192 \`--include takedownsCompleted\`; "average reports
1143
- per day per customer?" \u2192 \`--include reports\`. Skipping \`--include\`
1144
- runs ~11 aggregates per org which is rarely necessary.
1145
-
1146
- #### Auth: which credential can use \`--slugs\` / \`--all-my-orgs\`
1147
-
1148
- - **Org-scoped API key** (the most common production key): can only
1149
- query its own org. If \`--slugs\` contains foreign orgs the server
1150
- returns 403; \`--all-my-orgs\` is also rejected. Use the \`--org\`
1151
- form (or omit both) instead.
1152
- - **User API key**: inherits the user's org memberships, so it can
1153
- query any org the user is a member of (or any org for staff
1154
- users) via \`--slugs\` or \`--all-my-orgs\`. This is the credential
1155
- you want for cross-org rollups from automated agents.
1156
- - **Session auth** (Bearer token, e.g. \`chainpatrol login\`): same
1157
- membership rules as the user. Staff users can query any org.
1158
-
1159
- #### Reporting numbers to the user
1160
-
1161
- Whatever window and scope you choose, **make both explicit in your
1162
- reply** \u2014 say "averaged across 12 customers over the last 3 months"
1163
- or "summed across all paying orgs for the last 7 days." The same
1164
- prompt phrased slightly differently can pick a different window or
1165
- org set, and the response already echoes
1166
- \`scope.mode\` / \`scope.orgs\` / \`averages.windowDays\` /
1167
- \`range.label\` so you don't have to guess.
1168
-
1169
- ### \`presets list | run\` \u2014 Packaged workflows for common jobs
1170
-
1171
- \`\`\`bash
1172
- chainpatrol presets list
1173
- chainpatrol presets run cs-weekly-health --org <slug>
1174
- \`\`\`
1175
-
1176
- Use \`presets list\` to discover packaged multi-step workflows. The bundled
1177
- \`cs-weekly-health\` preset runs the standard customer-success weekly health
1178
- sweep; prefer it over hand-rolling the same sequence of commands.
1179
-
1180
- ## Headless / agent-mode tips
1181
-
1182
- When running these commands from an agent or CI:
1183
-
1184
- - Prefer \`--json\` (or \`--output json\`) so output is structured and the
1185
- update-check stderr nudge is suppressed automatically.
1186
- - Pass \`--no-input\` to forbid any interactive prompt (the CLI will error
1187
- out instead of waiting on a TTY).
1188
- - Pass \`--no-color\` or set \`NO_COLOR=1\` if your sink can't render ANSI.
1189
- - Set \`CHAINPATROL_NO_UPDATE_CHECK=1\` to silence the skill/npm freshness
1190
- nudge entirely.
1191
- - For \`login\` specifically, see the headless runbook in the login section
1192
- above \u2014 login is the one command that needs background + tail handling.
1193
-
1194
- ## Checking Auth Status
1195
-
1196
- To check if the user is logged in, read the credentials file:
1197
-
1198
- \`\`\`bash
1199
- cat ~/.chainpatrol/credentials.json 2>/dev/null && echo "Logged in" || echo "Not logged in"
1200
- \`\`\`
1201
-
1202
- Or start the login flow which will detect existing sessions:
1203
- \`\`\`bash
1204
- chainpatrol --json login
1205
- \`\`\`
1206
-
1207
- ## Configuration
1208
-
1209
- Config is stored at \`~/.chainpatrol/config.json\`:
1210
- - \`apiUrl\` \u2014 API base URL (default: \`https://app.chainpatrol.io\`)
1211
- - \`defaultOrg\` \u2014 Saved organization slug
1212
-
1213
- Override config dir with \`CHAINPATROL_CONFIG_DIR\` env var.
1214
-
1215
- ## Version Checks
1216
-
1217
- The CLI runs two lightweight version checks alongside each command and prints
1218
- a nudge to stderr if anything is out of date:
1219
-
1220
- - **Skill freshness**: compares the installed skill (\`~/.claude/skills/chainpatrol/SKILL.md\`)
1221
- to the version bundled with the CLI. If it's missing or older, the user is
1222
- asked to run \`chainpatrol setup\`.
1223
- - **NPM freshness**: compares the running CLI version to the latest published
1224
- on the npm registry. The check is throttled to once per 24 hours and capped
1225
- at a 1.5s timeout, with the result cached at \`<configDir>/version-check.json\`.
1226
-
1227
- Set \`CHAINPATROL_NO_UPDATE_CHECK=1\` to silence both checks. JSON mode
1228
- (\`--json\`) and quiet mode (\`-q\` / \`--quiet\`) also suppress the nudges so
1229
- machine-readable output is never polluted.
1230
-
1231
- ## Global Flags
1232
-
1233
- | Flag | Description |
1234
- |------------------|--------------------------------------------------------|
1235
- | \`--json\` | Machine-readable JSON output (shortcut for \`--output json\`) |
1236
- | \`--output <fmt>\` | Output format: \`human\` (default), \`json\`, \`markdown\`, \`csv\` |
1237
- | \`--quiet\`, \`-q\` | Suppress non-essential output and the update-check nudge |
1238
- | \`--no-color\` | Disable ANSI colors (also respects the \`NO_COLOR\` env var) |
1239
- | \`--no-input\` | Disable interactive prompts (use in scripts and agents) |
1240
- | \`--org <slug>\` | Organization slug (saved as the default for later commands) |
1241
- | \`--help\`, \`-h\` | Show help |
1242
- | \`--version\` | Show version |
1243
-
1244
- ## Workflow
1245
-
1246
- When the user asks to use the CLI, follow this order:
1247
-
1248
- 1. **Check login status** \u2014 Read \`~/.chainpatrol/credentials.json\` to see if they're logged in
1249
- 2. **Login if needed** \u2014 Run the login command and guide them through the device code flow
1250
- 3. **Set org if needed** \u2014 Ensure \`--org\` is provided or already saved in config
1251
- 4. **Run the requested command** \u2014 Execute the CLI command and show results
1252
-
1253
- ## Detection Config Output
1254
-
1255
- The \`configs list\` command shows detection sources grouped by:
1256
- - **Configured** \u2014 Sources with org-level configs (enabled/disabled with details)
1257
- - **Global** \u2014 Sources that run globally for all orgs (CERTSTREAM, ASSET_CHECK, BLOCKLIST, etc.)
1258
- - **Not Configured** \u2014 Sources available but not yet set up for the org
1259
-
1260
- Each config entry includes: title, status, cron schedule, and configuration parameters.
1261
-
1262
- ## Organization HealthCheck Guide
1263
-
1264
- This guide explains how to look for things that may be wrong for a given org
1265
- across the full pipeline: **detection \u2192 reviewing \u2192 blocklisting \u2192 takedowns**.
1266
- When the user asks for a "health check", "audit", "what's wrong with org X",
1267
- "review org X's setup", or similar, walk through each section below and surface
1268
- findings.
1269
-
1270
- In each section there are things you can look for that may be wrong. Some
1271
- checks have a dedicated CLI command that does most of the work server-side;
1272
- others are soft / qualitative signals that still need you to fetch data with
1273
- \`configs list\` or \`reports list\` and reason about it manually.
1274
-
1275
- ### Quick Path: CLI commands that automate parts of this guide
1276
-
1277
- The canonical first step is now the \`healthchecks\` namespace, which runs
1278
- every implemented check via the public API and returns a uniform shape per
1279
- check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
1280
- \`suggestedAction\`):
1281
-
1282
- \`\`\`bash
1283
- # Discover every check the platform exposes, implemented or planned.
1284
- chainpatrol --json healthchecks list
1285
-
1286
- # Run every implemented healthcheck in parallel and aggregate the results.
1287
- chainpatrol --json healthchecks run --all --org <slug>
1288
-
1289
- # Run a single named check.
1290
- chainpatrol --json healthchecks run reviewing.backlog --org <slug>
1291
- \`\`\`
1292
-
1293
- After \`healthchecks run --all\`, use these complementary commands to cover
1294
- the signals that are not yet exposed as a uniform healthcheck endpoint:
1295
-
1296
- \`\`\`bash
1297
- # Spikes / drops in detection volume over time (compare windows)
1298
- chainpatrol --json metrics breakdown --org <slug> --by day --this-week
1299
-
1300
- # Customer-reported threats \u2014 gaps in our own detection
1301
- chainpatrol --json reports list --org <slug> --reported-by-customer
1302
-
1303
- # What's enabled vs disabled vs not configured for the org
1304
- chainpatrol --json configs list --org <slug>
1305
-
1306
- # Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
1307
- chainpatrol --json queues snapshot --org <slug>
1308
-
1309
- # Packaged weekly customer-success sweep (preferred when it covers the ask)
1310
- chainpatrol presets run cs-weekly-health --org <slug>
1311
- \`\`\`
1312
-
1313
- Treat each command's output as one input to the healthcheck. The manual
1314
- checks below still apply \u2014 especially for signals the CLI cannot infer on
1315
- its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
1316
- is disabled", or "this drop is fine because the config isn't relevant
1317
- to this org"). Each subsection of the guide notes whether a healthcheck
1318
- endpoint exists today and what to fall back on when it doesn't.
1319
-
1320
- ### Reporting progress while running a healthcheck
1321
-
1322
- When the user asks for a healthcheck, narrate each step in real time so they
1323
- can watch the run unfold. Do NOT batch results and dump everything at the
1324
- end. For every check you run (Quick Path CLI command, or a manual signal
1325
- where you're fetching data with \`configs list\` / \`reports list\` to
1326
- reason about):
1327
-
1328
- 1. **Before** you call the command, emit ONE short sentence stating what
1329
- you're about to check, including the org slug. Examples:
1330
- - "Running detection config healthcheck for morpho\u2026"
1331
- - "Snapshotting review and takedown queues for morpho\u2026"
1332
- - "Fetching customer-reported reports for morpho to look for detection gaps\u2026"
1333
-
1334
- 2. **As soon as** the command returns, emit ONE short result line that
1335
- starts with a status word and ends with a key number or finding:
1336
- - \`DONE\` \u2014 ran cleanly, nothing to flag
1337
- - \`WARN\` \u2014 soft signal worth surfacing (e.g. a borderline backlog,
1338
- mild spike or drop, a config you'd want a human to confirm)
1339
- - \`FAIL\` \u2014 concrete failure that the user should act on
1340
- - Examples:
1341
- - "DONE \u2014 14/14 detection configs passing."
1342
- - "WARN \u2014 23 proposals in the review queue; 4 are older than 7 days."
1343
- - "FAIL \u2014 twitter_post_search returned 0 results in the last 24h with --run."
1344
-
1345
- 3. Run **independent** checks in **parallel** (single message, multiple
1346
- Bash tool calls) so progress lines arrive quickly. \`detections
1347
- healthcheck\`, \`queues snapshot\`, \`metrics breakdown\`,
1348
- \`reports list --reported-by-customer\`, and \`configs list\` are all
1349
- independent of each other and safe to fire concurrently. Dependent
1350
- follow-ups (e.g. paginating \`reports list\` with a returned cursor,
1351
- or fetching extra detail on a single failing config) run after the
1352
- first round.
1353
-
1354
- 4. After every check is reported, emit a short final **Summary** section:
1355
- - A one-line status per check (\u2713 / \u26A0 / \u2717 + name + key number).
1356
- - A short "Top issues" list of the highest-priority FAIL / WARN
1357
- findings, in priority order, with the concrete next action for each.
1358
-
1359
- Keep each progress line to one sentence. The goal is for the user to see
1360
- the healthcheck happening, not to read a wall of text mid-run \u2014 full
1361
- detail belongs in the final Summary or in a follow-up when the user asks
1362
- about a specific finding.
1363
-
1364
- ### Detection
1365
-
1366
- #### Enable Config That May Be Turned Off for Current Threats
1367
-
1368
- Blocked threats exist in an asset type but the matching detection source is
1369
- turned off. For example: lots of Twitter assets are on the blocklist, but
1370
- detection sources like "Twitter / X User Search" or "Twitter Post Search" are
1371
- disabled. Those should be turned on.
1372
-
1373
- **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
1374
- \`healthchecks list\` as \`detections.coverage-gaps\` with
1375
- \`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
1376
- "manual check, no API yet". Until the endpoint lands, do the correlation
1377
- manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
1378
- disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
1379
- recent blocked-item asset types. Flag any asset type where blocked items
1380
- exist but the matching detection source has \`status: "disabled"\` (or
1381
- appears in the "Not configured" group).
1382
-
1383
- #### Spike in Detections
1384
-
1385
- A spike in recent detections is worth investigating. It could be a bad config
1386
- change, or it could be a legitimate new attack push in this area \u2014 useful
1387
- intel to surface to the security team as a targeted spike.
1388
-
1389
- **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
1390
- in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
1391
- Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
1392
- (and a comparison window via \`--from\`/\`--to\`) to see daily detection
1393
- volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
1394
- against recent config changes for that source.
1395
-
1396
- #### Drop in Detections
1397
-
1398
- A drop is also worth looking into. It may be a bad config change, or it may
1399
- mean the config is not really relevant and can be safely turned off (not all
1400
- default-on configs are relevant to every org).
1401
-
1402
- **Run via CLI:** the extreme case ("this source went silent") is covered
1403
- today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
1404
- which is the canonical replacement for the older \`detections healthcheck\`.
1405
- It fails any config whose \`recentResultCount\` is below \`--min-results\`
1406
- in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
1407
- \`detections healthcheck --run\`) to also catch configs that error when
1408
- executed. For soft drops (still producing results but below baseline),
1409
- \`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
1410
- \u2014 use \`metrics breakdown --by day\` and compare windows manually.
1411
-
1412
- ### Reviewing
1413
-
1414
- PENDING proposals split into two operationally distinct buckets:
1415
-
1416
- - **Needs Review** \u2014 assets not on a watchlist, or reports submitted by a
1417
- customer. This is the reviewing UI's default view (\`excludeWatchlisted=true\`)
1418
- and the actionable queue reviewers work from. Pile-ups and aged items
1419
- here are high priority \u2014 \`fail\` severity is reachable.
1420
- - **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
1421
- customer-reported reports). The UI hides these by default because
1422
- watchlisting is the act of intentionally deferring the asset. Pile-ups
1423
- and aged items here are worth surfacing as cleanup work but **severity
1424
- is capped at warn** \u2014 they should never block on the same SLA as Needs
1425
- Review.
1426
-
1427
- Each bucket has its own pile-up and age check, so you can grade them
1428
- independently and tune thresholds without one drowning the other.
1429
-
1430
- #### Pile Up / Backlog of Needs-Review Proposals
1431
-
1432
- Too many proposals waiting in review. For most organizations this is over 100
1433
- reports, but really the threshold is relative to the average number of
1434
- confirmed threats per week. Example: if an org only adds 5 blocked threats per
1435
- week, then a 7-day backlog of even 10 proposals is a really big deal.
1436
-
1437
- **Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
1438
- The endpoint counts the **Needs Review** subset only (assets not
1439
- watchlisted, or reports marked \`reportedByCustomer\`) and grades severity
1440
- against per-org thresholds (default warn=50, fail=100; override with
1441
- \`--warn-threshold\` / \`--fail-threshold\` via the run payload). This is
1442
- the number the reviewing page in the app shows by default, so the
1443
- healthcheck output matches what reviewers see.
1444
-
1445
- For raw counts plus SLA / age breakdowns,
1446
- \`chainpatrol --json queues snapshot --org <slug>\` remains useful and
1447
- exposes \`reviewQueue.totalPendingProposals\` and
1448
- \`reviewQueue.distinctReports\` (note: \`queues snapshot\` does NOT apply
1449
- the watchlist filter, so its number is the sum of Needs Review +
1450
- Watchlisted). Compare against the org's typical weekly throughput
1451
- (use \`metrics summary --this-week\` for that baseline) \u2014 a backlog that
1452
- exceeds a week of typical confirmed-threat volume is a finding regardless
1453
- of the absolute number.
1454
-
1455
- #### Really Old Needs-Review Proposals
1456
-
1457
- Any Needs-Review proposal waiting longer than 14 days is a sign something
1458
- has gone wrong. Even complex investigations rarely take longer than this.
1459
- Except for rare cases, these should be rejected or approved to prevent a
1460
- backlog from building.
1461
-
1462
- **Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
1463
- The endpoint counts Needs-Review proposals older than the warn / fail age
1464
- thresholds (default 7 / 14 days) and lists the oldest offenders in
1465
- \`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
1466
- works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
1467
- strictest SLA breaches separately \u2014 any non-zero value is worth raising.
1468
-
1469
- #### Watchlist Pile-Up / Old Watchlisted Proposals
1470
-
1471
- Watchlisted-pending proposals are deferred on purpose, but they shouldn't
1472
- grow unbounded \u2014 a huge pile or very-old items signal that the watchlist
1473
- needs a cleanup pass. These don't block on the same SLA as Needs Review.
1474
-
1475
- **Run via CLI:** **Implemented as \`healthchecks run reviewing.watchlist-backlog\`**
1476
- (pile-up count, default warn=200) and
1477
- **\`healthchecks run reviewing.watchlist-old\`** (default warn-age 30
1478
- days). Both cap severity at warn. When reporting findings during an org
1479
- healthcheck, group them under "watchlist cleanup" rather than mixing with
1480
- Needs-Review findings \u2014 they're operationally different concerns.
1481
-
1482
- #### Spike in Auto Approved Reports
1483
-
1484
- If suddenly a lot of proposals in an org are being approved by automation,
1485
- that can be a sign of a bad rule approving too much, or a break in auto
1486
- confidences. Sometimes detection is spamming things that uniquely combine
1487
- with a weakness of a rule \u2014 which is effectively a bad rule. In all these
1488
- cases, notify an engineer at ChainPatrol and check any detection configs you
1489
- adjusted recently, since those may be the cause of spam combined with a weak
1490
- rule.
1491
-
1492
- **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
1493
- in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
1494
- \`implemented: false\`. As a proxy until the endpoint lands, use
1495
- \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
1496
- and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
1497
- isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
1498
- points at automation doing the approving.
1499
-
1500
- ### Blocklisting
1501
-
1502
- #### Google Safe Browsing (Coming Soon)
1503
-
1504
- (Needs new public API added before this works.)
1505
-
1506
- High error rate in Google Safe Browsing submission tracker. Each submission
1507
- has a status. If too many are in \`CANCELLED\`, that means Google's engine
1508
- denied our submission. Contact ChainPatrol's eng team to investigate why, and
1509
- also take a look at the org's custom detection sources \u2014 it's possible there
1510
- are too many false positives landing on the blocklist, indicating issues with
1511
- detection and reviewing rules.
1512
-
1513
- **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
1514
- in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
1515
- \`implemented: false\`. Until Google Safe Browsing submission state is
1516
- exposed in the public API, this remains a manual / engineering-team check.
1517
-
1518
- ### Takedowns
1519
-
1520
- The takedown pipeline has three stages \u2014 TODO (queued, not yet filed),
1521
- IN_PROGRESS (filed, waiting on vendor / customer / refile), and a terminal
1522
- state (COMPLETED or CANCELLED). Healthchecks cover pile-ups at each stage,
1523
- plus quality/configuration issues.
1524
-
1525
- #### Too Many Takedowns in ToDo
1526
-
1527
- Can mean a gap in automated takedowns not being implemented for some new area
1528
- of threats. It can also mean the areas that require manual takedowns are
1529
- being missed by the takedown team.
1530
-
1531
- **Run via CLI:** **Implemented as \`healthchecks run takedowns.todo-volume\`.**
1532
- Counts takedowns sitting in TODO (default warn=50, fail=100). For raw
1533
- breakdowns by type, cross-reference with
1534
- \`chainpatrol --json metrics breakdown --org <slug> --by assetType\` \u2014
1535
- items piled up on a specific platform usually point at an automation
1536
- gap there.
1537
-
1538
- #### Too Many Takedowns In Progress
1539
-
1540
- Typically means something is wrong with the submission itself. The takedown
1541
- may need to be resubmitted, the vendor asked for more evidence, or we may
1542
- have submitted it in the wrong place.
1543
-
1544
- **Run via CLI:** Two checks, complementary:
1545
-
1546
- - **\`healthchecks run takedowns.in-progress-volume\`** \u2014 counts all
1547
- IN_PROGRESS takedowns regardless of age (default warn=30, fail=75).
1548
- Catches a vendor-side or submission-format problem before items go
1549
- stale.
1550
- - **\`healthchecks run takedowns.stale-in-progress\`** \u2014 counts IN_PROGRESS
1551
- takedowns past a staleness threshold (default 7 days), lists the oldest
1552
- offenders. Any non-zero value is worth investigating; a growing count
1553
- across snapshots strongly suggests vendor-side or format issues.
1554
-
1555
- #### Too Many Cancelled Takedowns
1556
-
1557
- Takedowns should rarely be cancelled. A cancelled takedown means "we will not
1558
- do this takedown" for some reason. Cases like adding an item to the blocklist
1559
- when it's already taken down are treated as completed, not cancelled. So even
1560
- 3 cancelled takedowns in a 7-day period is too many.
1561
-
1562
- **Run via CLI:** **Implemented as \`healthchecks run takedowns.cancelled-count\`.**
1563
- Counts transitions into the CANCELLED status from the TakedownEvent log
1564
- within the lookback window (default 7 days, warn=3, fail=10). The check
1565
- uses the event log rather than \`Takedown.updatedAt\` so it correctly
1566
- attributes the cancellation date even if the takedown has since been
1567
- edited. A spike usually means a quality problem in the proposal funnel
1568
- or the CANCELLED status being used as a catch-all.
1569
-
1570
- #### Automated Takedowns Turned Off for Over 30 Days
1571
-
1572
- Automated takedowns should be on by default for nearly every organization.
1573
- Any issue that would make you want to turn off automated takedowns should be
1574
- resolved within 30 days.
1575
-
1576
- **Run via CLI:** **Implemented as \`healthchecks run takedowns.automation-off\`.**
1577
- Checks \`Organization.isAutomatedTakedownsActive\` and derives the
1578
- off-duration from the most recent \`SERVICES_AUTOMATED_TAKEDOWNS_UPDATED\`
1579
- entry in \`OrganizationEvent\` (default warn 30 days, fail 60 days). Orgs
1580
- with takedown service entirely disabled (\`isTakedownsActive=0\`) are
1581
- skipped \u2014 automation being off is implied in that case.
1582
-
1583
- ### Assets
1584
-
1585
- Healthchecks on the asset model \u2014 specifically asset liveness state, which
1586
- the takedown team depends on for follow-up.
1587
-
1588
- #### Spike in Recently Dead Assets
1589
-
1590
- A sudden spike in DEAD detections can be a good signal (takedowns or platform
1591
- moderation working) but it can also mean the liveness checker is
1592
- misclassifying assets after a platform change, captcha rollout, or anti-bot
1593
- update. If many assets become dead at once, sample a few manually.
1594
-
1595
- **Run via CLI:** **Implemented as \`healthchecks run assets.dead-asset-spike\`.**
1596
- Compares \`DETECTED_AS_DEAD\` events in the current window (default 24h)
1597
- against the baseline rate from the prior \`baselineDays\` (default 7d).
1598
- Severity fires only when the current count clears \`minSpikeCount\` (default
1599
- 10) AND exceeds the multiplier (default warn \xD72, fail \xD74). The
1600
- \`minSpikeCount\` floor suppresses noise on orgs with near-zero baseline
1601
- activity. When this fires, pull a sample of recent DEAD assets, verify a
1602
- few in a browser, and notify ChainPatrol engineering if the sample is
1603
- clearly still live.
1604
-
1605
- #### Assets Marked Dead but Still Online / Assets Not Marked Dead Even Though They Are Down
1606
-
1607
- These two opposite failure modes are **not implemented as healthchecks**
1608
- (listed as \`assets.dead-but-alive\` and \`assets.alive-but-marked-dead\` with
1609
- \`implemented: false\`). Both require live HTTP probes against asset URLs,
1610
- which is not a synchronous-healthcheck shape.
1611
-
1612
- Until a dedicated probe command exists:
1613
-
1614
- - For "marked dead but still alive": sample a handful of recently-DEAD
1615
- assets, open them in a browser, and watch for any that load. Common
1616
- causes: bot protection, geo-blocking, rate limits, or liveness logic
1617
- that does not handle the asset type correctly.
1618
- - For "alive but marked dead": after a known takedown event, sample
1619
- assets that *should* be dead but are still marked alive. Common causes:
1620
- cached responses, soft-404 pages, parked-domain redirects, platform
1621
- suspension pages still returning 200.
1622
-
1623
- In both cases, if liveness looks miscalibrated for a class of assets,
1624
- notify ChainPatrol engineering \u2014 the checker likely needs a tuning pass for
1625
- that platform.
1626
-
1627
- ## Organization Trend Search Guide
1628
-
1629
- Trend search asks "what's changed recently for this org?" \u2014 the answer can
1630
- surface coordinated attacks, new attack channels, or campaigns targeting
1631
- specific people, **before** they show up as a healthcheck failure. Unlike
1632
- the HealthCheck Guide above (which grades single signals against
1633
- thresholds), trend search compares a recent window to a baseline window
1634
- and flags ratios, not absolute counts. A trend is interesting even when
1635
- the pipeline is keeping up with it \u2014 the user usually wants to know.
1636
-
1637
- When the user asks something like:
1638
-
1639
- - "are there any trends in org X?"
1640
- - "search for trends in <org>"
1641
- - "anything unusual happening with <org> lately?"
1642
- - "any spikes for <org>?"
1643
- - "what's new for <org> this week?"
1644
- - "is anyone targeting <org>'s employees more than usual?"
1645
- - "are we seeing more YouTube/Telegram/etc. threats for <org>?"
1646
-
1647
- \u2026run the three trend checks below in parallel (single message, multiple
1648
- Bash calls) and surface anything where the current rate is \u2265 2\xD7 the
1649
- baseline AND clears a small absolute floor.
1650
-
1651
- ### How to compute "current vs. baseline" rates
1652
-
1653
- Pick two non-overlapping windows: a **current** window the user cares about
1654
- (default last 7 days) and a **baseline** window ending where the current
1655
- one starts (default the prior ~90 days, i.e. \`current_start - 90d\` \u2192
1656
- \`current_start - 1d\`). For any breakdown bucket, compute:
1657
-
1658
- \`\`\`
1659
- current_per_day = current_count / current_window_days
1660
- baseline_per_day = baseline_count / baseline_window_days
1661
- ratio = current_per_day / max(baseline_per_day, epsilon)
1662
- \`\`\`
1663
-
1664
- Flag a bucket if \`ratio >= 2\` AND \`current_count >= 5\`. The floor
1665
- suppresses noise on orgs with near-zero baseline activity (a single new
1666
- YouTube report jumping the rate from 0 to 1/day shouldn't trigger). If
1667
- the user gives a different window ("last 3 days", "this month"), adjust
1668
- both windows proportionally and keep the same ratio threshold.
1669
-
1670
- ### Trend 1 \u2014 Spike in a specific asset type
1671
-
1672
- A sudden jump in threats on a platform the org doesn't usually see
1673
- traffic on ("normally you don't get targeted on YouTube much, but now
1674
- there's a spike there") is worth flagging \u2014 it usually means an attacker
1675
- has discovered a new channel that works for them. Even when the absolute
1676
- number is small, the *ratio* against the org's normal mix is what
1677
- matters.
1678
-
1679
- Use \`metrics breakdown --by type\` over both windows:
1680
-
1681
- \`\`\`bash
1682
- # Current window \u2014 last 7 days, broken out by asset type
1683
- chainpatrol --json metrics breakdown --org <slug> --by type \\
1684
- --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1685
-
1686
- # Baseline window \u2014 prior ~90 days ending where the current window starts
1687
- chainpatrol --json metrics breakdown --org <slug> --by type \\
1688
- --from <YYYY-MM-DD 97d ago> --to <YYYY-MM-DD 8d ago>
1689
- \`\`\`
1690
-
1691
- Each entry in \`.points\` has \`{ type, count }\`. Join the two windows on
1692
- \`type\`, apply the ratio rule above, and report each flagged type.
1693
-
1694
- Cross-reference any flagged type with \`configs list --org <slug>\` \u2014 if
1695
- the spike is on Twitter / X but \`twitter_post_search\` is disabled, the
1696
- detection source for that channel isn't even running for this org and
1697
- the spike is being caught by something else (likely customer reports);
1698
- suggest turning it on.
1699
-
1700
- ### Trend 2 \u2014 Spike in overall threat volume
1701
-
1702
- A sharp rise in total threats across the org \u2014 regardless of type or
1703
- brand \u2014 usually means a coordinated attack or campaign is underway. This
1704
- isn't a healthcheck (reviewing and takedowns may be keeping up just
1705
- fine), but the security team still wants to know so they can warn
1706
- customers / employees / partners.
1707
-
1708
- \`\`\`bash
1709
- # Daily volume for the last ~6 weeks \u2014 enough to eyeball a baseline AND
1710
- # see the spike on the right edge of the series.
1711
- chainpatrol --json metrics breakdown --org <slug> --by day \\
1712
- --from <YYYY-MM-DD 42d ago> --to <YYYY-MM-DD today>
1713
- \`\`\`
1714
-
1715
- Read \`.points\` (one entry per day). Take the trailing 7-day average and
1716
- compare against the prior ~5 weeks (the same 2\xD7 / floor rule). For round
1717
- totals to quote to the user, also pull:
1718
-
1719
- \`\`\`bash
1720
- # Two calls \u2014 current and baseline windows \u2014 using --include to skip
1721
- # the per-type / per-day series and keep the call cheap.
1722
- chainpatrol --json metrics organization --org <slug> \\
1723
- --include reports,newThreats,threatsWatchlisted \\
1724
- --from <current_from> --to <current_to>
1725
-
1726
- chainpatrol --json metrics organization --org <slug> \\
1727
- --include reports,newThreats,threatsWatchlisted \\
1728
- --from <baseline_from> --to <baseline_to>
1729
- \`\`\`
1730
-
1731
- When volume spikes broadly, also skim \`reports list --reported-by-customer\`
1732
- for the same window \u2014 a wave of customer reports often arrives a few hours
1733
- ahead of automated detection on a real coordinated push.
1734
-
1735
- ### Trend 3 \u2014 Spike on a specific sub-brand (especially employee brands)
1736
-
1737
- Sub-brands the org has set up for individual people \u2014 employees,
1738
- executives, public figures \u2014 are high-signal targets. A sudden spike on
1739
- one usually means an attacker is impersonating that person specifically,
1740
- which is materially different from a generic phishing wave and usually
1741
- warrants a direct heads-up to the person involved. In the database these
1742
- are \`Brand\` rows with \`type: INDIVIDUAL\` (vs. \`ORGANIZATION\` for
1743
- product / parent brands).
1744
-
1745
- **Step 1: Pull the brand roster** so you can label each spike as
1746
- "employee" or "product" without asking the user.
1747
-
1748
- \`\`\`bash
1749
- chainpatrol --json brands list
1750
- \`\`\`
1751
-
1752
- Build a map of \`slug \u2192 type\` from the response so the next step's
1753
- findings can be tagged \`(employee)\` or \`(product)\` automatically.
1754
-
1755
- **Step 2: Run the breakdown over both windows.**
1756
-
1757
- \`\`\`bash
1758
- # Current window \u2014 last 7 days, broken out by sub-brand
1759
- chainpatrol --json metrics breakdown --org <slug> --by brand \\
1760
- --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1761
-
1762
- # Baseline window \u2014 prior ~90 days
1763
- chainpatrol --json metrics breakdown --org <slug> --by brand \\
1764
- --from <YYYY-MM-DD 97d ago> --to <YYYY-MM-DD 8d ago>
1765
- \`\`\`
1766
-
1767
- Each entry in \`.points\` has \`{ brandId, brandSlug, brandName, count }\`.
1768
- Apply the ratio rule, join against the \`slug \u2192 type\` map from Step 1,
1769
- and report each spiking sub-brand by name plus its type.
1770
-
1771
- **Step 3: Drill into the flagged brand** with \`metrics organization
1772
- --brand-slug <slug>\` for a per-day / per-type breakdown scoped to that
1773
- brand only. The server applies the brand filter to the scalar metrics
1774
- (\`newThreats\`, \`takedownsFiled\`, etc.) AND to \`blockedByType\` /
1775
- \`blockedByDay\`, so the time series and per-asset-type counts are real
1776
- brand-only data:
1777
-
1778
- \`\`\`bash
1779
- chainpatrol --json metrics organization --org <slug> \\
1780
- --brand-slug <brand-slug> \\
1781
- --include newThreats,blockedByType,blockedByDay \\
1782
- --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1783
- \`\`\`
1784
-
1785
- If multiple \`INDIVIDUAL\` brands are spiking at the same time, treat that
1786
- as a stronger signal than a single one \u2014 likely a campaign targeting the
1787
- company's people rather than a single impersonation. Prioritize those
1788
- findings over single-brand spikes when summarizing.
1789
-
1790
- ### Reporting trend findings
1791
-
1792
- Report each spiking bucket as its own finding with: the trend it falls
1793
- under (asset type / overall volume / sub-brand), the current vs. baseline
1794
- rate (e.g. "youtube: 12/day last 7d vs. 1.2/day prior 90d, \xD710
1795
- baseline"), and a one-line follow-up:
1796
-
1797
- - Asset-type spike \u2192 check \`configs list\` for the matching detection
1798
- source and turn it on if it's off.
1799
- - Overall-volume spike \u2192 flag a possible coordinated campaign; suggest
1800
- the security team look at the recent \`reports list\` for shared
1801
- infrastructure (sender, registrar, hosting).
1802
- - Sub-brand spike \u2192 look up \`type\` via \`brands list\`; if
1803
- \`INDIVIDUAL\`, suggest a direct heads-up to that person; if
1804
- \`ORGANIZATION\`, suggest a product-team alert.
1805
-
1806
- If none of the three trends fire above the 2\xD7 / floor thresholds, say so
1807
- explicitly ("no significant trends in the last 7 days vs. the prior 90")
1808
- rather than dumping every breakdown number \u2014 the absence is the
1809
- finding.
1810
- `;
1811
- }
1812
- function getBundledSkillVersion() {
1813
- return getCliVersion();
1814
- }
1815
- function getBundledSkillContent() {
1816
- return buildSkillContent(getCliVersion());
1817
- }
1818
- function readInstalledSkillVersion() {
1819
- if (!existsSync2(SKILL_FILE)) return void 0;
1820
- try {
1821
- const raw = readFileSync3(SKILL_FILE, "utf-8");
1822
- return parseSkillVersion(raw);
1823
- } catch {
1824
- return void 0;
1825
- }
1826
- }
1827
- function isSkillInstalled() {
1828
- return existsSync2(SKILL_FILE);
1829
- }
1830
- function parseSkillVersion(content) {
1831
- const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1832
- if (!fmMatch) return void 0;
1833
- const versionMatch = fmMatch[1].match(/^version:\s*(.+?)\s*$/m);
1834
- return versionMatch ? versionMatch[1].trim() : void 0;
1835
- }
1836
- var LOGO = `
1837
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1838
- \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
1839
- \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
1840
- \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255D
1841
- \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551
1842
- \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D
1843
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1844
- C H A I N P A T R O L
1845
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1846
- `;
1847
- function setupSkill(options) {
1848
- const skillContent = buildSkillContent(getCliVersion());
1849
- const skillAlreadyExists = existsSync2(SKILL_FILE);
1850
- const skillUpToDate = skillAlreadyExists && readFileSync3(SKILL_FILE, "utf-8") === skillContent;
1851
- let skillStatus;
1852
- if (!skillAlreadyExists) {
1853
- mkdirSync2(SKILL_DIR, { recursive: true });
1854
- writeFileSync2(SKILL_FILE, skillContent, { mode: 420 });
1855
- skillStatus = "installed";
1856
- } else if (!skillUpToDate) {
1857
- writeFileSync2(SKILL_FILE, skillContent, { mode: 420 });
1858
- skillStatus = "updated";
1859
- } else {
1860
- skillStatus = "up-to-date";
1861
- }
1862
- const completionResult = installCompletions();
1863
- const loginHookResult = options.cloud ? installLoginHook() : null;
1864
- const completionsChanged = completionResult.installed || completionResult.configuredShellRc;
1865
- const loginHookChanged = loginHookResult != null && (loginHookResult.hookWritten || loginHookResult.settingsUpdated);
1866
- const overallStatus = skillStatus !== "up-to-date" ? skillStatus : completionsChanged || loginHookChanged ? "updated" : "up-to-date";
1867
- if (options.json) {
1868
- console.log(
1869
- JSON.stringify({
1870
- status: overallStatus,
1871
- path: SKILL_FILE,
1872
- skill: { status: skillStatus, path: SKILL_FILE },
1873
- completions: completionResult,
1874
- loginHook: loginHookResult
1875
- })
1876
- );
1877
- return;
1878
- }
1879
- if (overallStatus === "up-to-date") {
1880
- console.log("Claude Code skill, completions, and login hook are already up to date.");
1881
- return;
1882
- }
1883
- console.log(LOGO);
1884
- if (skillStatus === "installed") {
1885
- console.log(`Installed Claude Code skill at ${SKILL_FILE}`);
1886
- } else if (skillStatus === "updated") {
1887
- console.log(`Updated Claude Code skill at ${SKILL_FILE}`);
1888
- }
1889
- console.log("You can now use /chainpatrol in Claude Code from any project.");
1890
- if (loginHookChanged && loginHookResult) {
1891
- console.log(`Installed auto-login hook at ${loginHookResult.hookPath}`);
1892
- if (loginHookResult.settingsUpdated) {
1893
- console.log(`Registered SessionStart hook in ${loginHookResult.settingsPath}`);
1894
- }
1895
- }
1896
- if (completionResult.installed) {
1897
- console.log(
1898
- `Installed ${completionResult.shell} completions at ${completionResult.path}`
1899
- );
1900
- if (completionResult.configuredShellRc) {
1901
- console.log("Added completion config to ~/.zshrc");
1902
- }
1903
- console.log('Run "exec zsh" or open a new terminal to enable tab completion.');
1904
- }
1905
- }
1906
- function uninstallSkill(options) {
1907
- const skillInstalled = existsSync2(SKILL_FILE);
1908
- if (skillInstalled) {
1909
- rmSync(SKILL_DIR, { recursive: true });
1910
- }
1911
- const removedCompletions = uninstallCompletions();
1912
- const loginHookResult = uninstallLoginHook();
1913
- const removedAnything = skillInstalled || removedCompletions || loginHookResult.hookRemoved || loginHookResult.settingsUpdated;
1914
- if (options.json) {
1915
- console.log(
1916
- JSON.stringify({
1917
- status: removedAnything ? "uninstalled" : "not-installed",
1918
- path: SKILL_FILE,
1919
- removedCompletions,
1920
- loginHook: loginHookResult
1921
- })
1922
- );
1923
- return;
1924
- }
1925
- if (!removedAnything) {
1926
- console.log("Claude Code skill is not installed.");
1927
- return;
1928
- }
1929
- if (skillInstalled) {
1930
- console.log("Removed Claude Code skill from " + SKILL_DIR);
1931
- }
1932
- if (loginHookResult.hookRemoved || loginHookResult.settingsUpdated) {
1933
- console.log("Removed chainpatrol auto-login hook.");
1934
- }
1935
- if (removedCompletions) {
1936
- console.log("Removed shell completions.");
1937
- }
1938
- }
1939
-
1940
- export {
1941
- getCliVersion,
1942
- compareVersions,
1943
- getBundledSkillVersion,
1944
- getBundledSkillContent,
1945
- readInstalledSkillVersion,
1946
- isSkillInstalled,
1947
- parseSkillVersion,
1948
- setupSkill,
1949
- uninstallSkill
1950
- };