@chainpatrol/cli 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 034096d: `chainpatrol asset check` now accepts multiple assets in a single
8
+ invocation — either as positional arguments
9
+ (`chainpatrol asset check a.example b.example c.example`) or via
10
+ repeated `--asset` flags. Lookups run in parallel (concurrency 10) and
11
+ the JSON output for bulk calls is
12
+ `{ results: [...], summary: { checked, blocked, allowed, unknown, errored } }`.
13
+ Single-asset JSON keeps the existing flat shape for back-compat. The
14
+ bundled `/chainpatrol` Claude Code skill is updated to call out the bulk
15
+ form so agents stop falling back to per-asset shell loops (or refusing
16
+ the request) when asked to check many domains at once.
17
+
3
18
  ## 0.9.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -45,11 +45,23 @@ chainpatrol queues snapshot --all --output csv
45
45
  # look up a single asset against the ChainPatrol blocklist and external feeds
46
46
  chainpatrol asset check https://phish.example
47
47
 
48
- # JSON output for automation (returns status, per-source breakdown, watchStatus)
48
+ # bulk: check many assets in one call (parallel, concurrency=10)
49
+ chainpatrol asset check a.example b.example c.example
50
+
51
+ # repeated --asset is equivalent to positional args
52
+ chainpatrol asset check --asset a.example --asset b.example
53
+
54
+ # pipe a file of one-domain-per-line via xargs into a single CLI call
55
+ xargs -a domains.txt chainpatrol --json asset check
56
+
57
+ # JSON output for automation (single-asset returns flat shape; bulk returns
58
+ # { results: [...], summary: { checked, blocked, allowed, unknown, errored } })
49
59
  chainpatrol --json asset check 0xabc123...
60
+ chainpatrol --json asset check a.example b.example c.example
50
61
 
51
- # markdown summary
62
+ # markdown / csv summary tables
52
63
  chainpatrol asset check phish.example --output markdown
64
+ chainpatrol asset check a.example b.example --output csv
53
65
  ```
54
66
 
55
67
  ### Detection commands
@@ -0,0 +1,203 @@
1
+ import {
2
+ CliExitError,
3
+ ExitCode
4
+ } from "./chunk-E2LAMILJ.js";
5
+ import {
6
+ printOutput,
7
+ toCsvRows
8
+ } from "./chunk-VFT3TD3E.js";
9
+ import {
10
+ createApiClient
11
+ } from "./chunk-RIKR2WFT.js";
12
+ import "./chunk-EGWK6SRQ.js";
13
+ import "./chunk-TFCNKBRC.js";
14
+ import "./chunk-U73SABXK.js";
15
+
16
+ // src/commands/asset/check.ts
17
+ var DEFAULT_CONCURRENCY = 10;
18
+ function statusLine(result) {
19
+ const watchTag = result.watchStatus ? ` watch=${result.watchStatus}` : "";
20
+ const reasonTag = result.reason ? ` reason=${result.reason}` : "";
21
+ return `${result.status} (source=${result.source}${reasonTag}${watchTag})`;
22
+ }
23
+ async function runWithConcurrency(items, worker, concurrency) {
24
+ const results = new Array(items.length);
25
+ let cursor = 0;
26
+ const workers = new Array(Math.min(concurrency, items.length)).fill(null).map(async () => {
27
+ while (true) {
28
+ const index = cursor;
29
+ cursor += 1;
30
+ if (index >= items.length) return;
31
+ results[index] = await worker(items[index]);
32
+ }
33
+ });
34
+ await Promise.all(workers);
35
+ return results;
36
+ }
37
+ async function runAssetCheck(options) {
38
+ const contents = (options.contents ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
39
+ if (contents.length === 0) {
40
+ throw new CliExitError(
41
+ "asset check requires at least one asset. Example: chainpatrol asset check https://example.com",
42
+ ExitCode.USAGE
43
+ );
44
+ }
45
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
46
+ const client = options.apiClient ?? createApiClient();
47
+ const perAssetResults = await runWithConcurrency(
48
+ contents,
49
+ async (content) => {
50
+ try {
51
+ const result = await client.assetCheck({ content });
52
+ return { content, ok: true, result };
53
+ } catch (err) {
54
+ return {
55
+ content,
56
+ ok: false,
57
+ error: err instanceof Error ? err.message : String(err)
58
+ };
59
+ }
60
+ },
61
+ DEFAULT_CONCURRENCY
62
+ );
63
+ const summary = {
64
+ checked: perAssetResults.length,
65
+ blocked: perAssetResults.filter((r) => r.ok && r.result.status === "BLOCKED").length,
66
+ allowed: perAssetResults.filter((r) => r.ok && r.result.status === "ALLOWED").length,
67
+ unknown: perAssetResults.filter((r) => r.ok && r.result.status === "UNKNOWN").length,
68
+ errored: perAssetResults.filter((r) => !r.ok).length
69
+ };
70
+ const isSingle = contents.length === 1;
71
+ const jsonPayload = isSingle ? buildSingleJsonPayload(perAssetResults[0], options.explain) : {
72
+ results: perAssetResults.map(toJsonRow),
73
+ summary,
74
+ explanation: options.explain ? "Bulk asset check ran the public asset/check endpoint for each input in parallel (concurrency=10). Each row reports the aggregated ChainPatrol status plus the per-source breakdown." : void 0
75
+ };
76
+ const markdown = isSingle ? buildSingleMarkdown(perAssetResults[0]) : [
77
+ `# Asset Check (${summary.checked})`,
78
+ "",
79
+ `- Blocked: ${summary.blocked}`,
80
+ `- Allowed: ${summary.allowed}`,
81
+ `- Unknown: ${summary.unknown}`,
82
+ ...summary.errored > 0 ? [`- Errored: ${summary.errored}`] : [],
83
+ "",
84
+ "| Content | Status | Source | Reason | Watch |",
85
+ "| --- | --- | --- | --- | --- |",
86
+ ...perAssetResults.map(toMarkdownRow)
87
+ ].join("\n");
88
+ const csv = toCsvRows(perAssetResults.map(toCsvRow));
89
+ printOutput({
90
+ outputFormat,
91
+ json: jsonPayload,
92
+ markdown,
93
+ csv,
94
+ human: () => {
95
+ for (const entry of perAssetResults) {
96
+ if (entry.ok) {
97
+ console.log(`${entry.content} -> ${statusLine(entry.result)}`);
98
+ } else {
99
+ console.log(`${entry.content} -> ERROR: ${entry.error}`);
100
+ }
101
+ }
102
+ if (isSingle && perAssetResults[0].ok) {
103
+ const sources = perAssetResults[0].result.sources;
104
+ if (sources.length > 0) {
105
+ console.log("Sources:");
106
+ for (const entry of sources) {
107
+ console.log(` - ${entry.source}: ${entry.status}`);
108
+ }
109
+ }
110
+ if (perAssetResults[0].result.message) {
111
+ console.log(`Message: ${perAssetResults[0].result.message}`);
112
+ }
113
+ if (perAssetResults[0].result.code) {
114
+ console.log(`Code: ${perAssetResults[0].result.code}`);
115
+ }
116
+ } else {
117
+ console.log(
118
+ `Summary: ${summary.checked} checked \u2014 blocked=${summary.blocked} allowed=${summary.allowed} unknown=${summary.unknown}${summary.errored > 0 ? ` errored=${summary.errored}` : ""}`
119
+ );
120
+ }
121
+ if (options.explain) {
122
+ console.log(
123
+ "Status is the aggregated verdict across ChainPatrol and external feeds; see the per-source rows for the underlying signals."
124
+ );
125
+ }
126
+ }
127
+ });
128
+ if (summary.errored > 0) {
129
+ throw new CliExitError(
130
+ `asset check: ${summary.errored} of ${summary.checked} request(s) failed.`,
131
+ ExitCode.UNKNOWN
132
+ );
133
+ }
134
+ }
135
+ function buildSingleJsonPayload(entry, explain) {
136
+ if (!entry.ok) {
137
+ return {
138
+ content: entry.content,
139
+ error: entry.error,
140
+ explanation: explain ? "asset check failed for this asset. Inspect the error message and retry." : void 0
141
+ };
142
+ }
143
+ return {
144
+ content: entry.content,
145
+ ...entry.result,
146
+ explanation: explain ? "Asset check aggregates ChainPatrol records and external sources to classify a domain, URL, or crypto address as BLOCKED, ALLOWED, or UNKNOWN." : void 0
147
+ };
148
+ }
149
+ function buildSingleMarkdown(entry) {
150
+ if (!entry.ok) {
151
+ return [`# Asset Check: ${entry.content}`, "", `- Error: ${entry.error}`].join("\n");
152
+ }
153
+ const result = entry.result;
154
+ return [
155
+ `# Asset Check: ${entry.content}`,
156
+ "",
157
+ `- Status: **${result.status}**`,
158
+ `- Source: ${result.source}`,
159
+ ...result.reason ? [`- Reason: ${result.reason}`] : [],
160
+ ...result.watchStatus ? [`- Watch status: ${result.watchStatus}`] : [],
161
+ ...result.message ? [`- Message: ${result.message}`] : [],
162
+ ...result.code ? [`- Code: ${result.code}`] : [],
163
+ "",
164
+ "## Per-source results",
165
+ "",
166
+ ...result.sources.map((entry2) => `- ${entry2.source}: ${entry2.status}`)
167
+ ].join("\n");
168
+ }
169
+ function toJsonRow(entry) {
170
+ if (!entry.ok) {
171
+ return { content: entry.content, error: entry.error };
172
+ }
173
+ return { content: entry.content, ...entry.result };
174
+ }
175
+ function toMarkdownRow(entry) {
176
+ if (!entry.ok) {
177
+ return `| ${entry.content} | ERROR | \u2014 | ${entry.error.replace(/\|/g, "\\|")} | \u2014 |`;
178
+ }
179
+ const r = entry.result;
180
+ return `| ${entry.content} | ${r.status} | ${r.source} | ${r.reason ?? ""} | ${r.watchStatus ?? ""} |`;
181
+ }
182
+ function toCsvRow(entry) {
183
+ if (!entry.ok) {
184
+ return {
185
+ content: entry.content,
186
+ status: "ERROR",
187
+ source: "",
188
+ reason: entry.error,
189
+ watchStatus: ""
190
+ };
191
+ }
192
+ const r = entry.result;
193
+ return {
194
+ content: entry.content,
195
+ status: r.status,
196
+ source: r.source,
197
+ reason: r.reason ?? "",
198
+ watchStatus: r.watchStatus ?? ""
199
+ };
200
+ }
201
+ export {
202
+ runAssetCheck
203
+ };
@@ -312,7 +312,7 @@ so the same background+tail pattern works without \`--json\`. Prefer
312
312
  chainpatrol logout
313
313
  \`\`\`
314
314
 
315
- ### \`asset check\` \u2014 Check a single asset against the blocklist
315
+ ### \`asset check\` \u2014 Check one or many assets against the blocklist
316
316
 
317
317
  Look up a URL, domain, or crypto address and return its aggregated status
318
318
  (\`BLOCKED\`, \`ALLOWED\`, or \`UNKNOWN\`) plus a per-source breakdown
@@ -320,24 +320,53 @@ Look up a URL, domain, or crypto address and return its aggregated status
320
320
  polkadot-phishing). Works whether you're authenticated via device-code
321
321
  login or via a \`CHAINPATROL_API_KEY\` env var.
322
322
 
323
+ Single asset:
324
+
323
325
  \`\`\`bash
324
326
  chainpatrol asset check https://phish.example
325
327
  chainpatrol asset check 0xabc123...
326
328
  \`\`\`
327
329
 
328
- JSON mode (recommended for automations and agents \u2014 returns full
329
- \`{ status, source, reason?, sources[], watchStatus? }\`):
330
+ #### Bulk checks (preferred for >1 asset)
331
+
332
+ Pass multiple assets in a single invocation \u2014 the CLI runs them in
333
+ parallel (concurrency 10) and returns one row per asset. **Do this
334
+ instead of looping the CLI in a shell \`for\` loop**: one process, one
335
+ auth handshake, parallel HTTP. Use either positional args or repeated
336
+ \`--asset\`:
337
+
338
+ \`\`\`bash
339
+ # positional form
340
+ chainpatrol asset check a.example b.example c.example
341
+
342
+ # repeated --asset (handy when content has spaces or special chars)
343
+ chainpatrol asset check --asset a.example --asset b.example
344
+
345
+ # from a file of one-asset-per-line (use xargs to splat into one call)
346
+ xargs -a domains.txt chainpatrol asset check
347
+ \`\`\`
348
+
349
+ JSON mode is the agent-friendly default \u2014 single-asset JSON keeps the
350
+ flat \`{ content, status, source, reason?, sources[], watchStatus? }\`
351
+ shape; multi-asset JSON returns \`{ results: [...], summary: { checked,
352
+ blocked, allowed, unknown, errored } }\`:
330
353
 
331
354
  \`\`\`bash
332
355
  chainpatrol --json asset check https://phish.example
356
+ chainpatrol --json asset check a.example b.example c.example
333
357
  \`\`\`
334
358
 
335
- Markdown is also supported for sharing in docs / chat:
359
+ Markdown / CSV are also available for sharing in docs / chat:
336
360
 
337
361
  \`\`\`bash
338
362
  chainpatrol asset check phish.example --output markdown
363
+ chainpatrol asset check a.example b.example --output csv
339
364
  \`\`\`
340
365
 
366
+ If any individual lookup fails, the CLI still prints results for the
367
+ successful ones, then exits non-zero so failures aren't silently
368
+ swallowed.
369
+
341
370
  ### \`configs list\` \u2014 List detection configurations
342
371
 
343
372
  Requires authentication and an organization slug.
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-P4L4N5LM.js";
16
+ } from "./chunk-R2DZGMGT.js";
17
17
  import "./chunk-Z76CUWSS.js";
18
18
  import {
19
19
  DateTime
@@ -92,18 +92,24 @@ var HELP = {
92
92
  usage: "chainpatrol logout"
93
93
  },
94
94
  asset: {
95
- description: "Check whether an asset is BLOCKED, ALLOWED, or UNKNOWN.",
96
- usage: "chainpatrol asset check <content>",
95
+ description: "Check whether one or more assets are BLOCKED, ALLOWED, or UNKNOWN.",
96
+ usage: "chainpatrol asset check <content> [<content> ...]",
97
97
  examples: [
98
98
  "chainpatrol asset check https://phish.example",
99
+ "chainpatrol asset check a.example b.example c.example",
99
100
  "chainpatrol --json asset check 0xabc123\u2026"
100
101
  ]
101
102
  },
102
103
  "asset check": {
103
- description: "Look up an asset (URL, domain, or crypto address) against the ChainPatrol blocklist and external feeds. Returns the aggregated status plus a per-source breakdown.",
104
- usage: "chainpatrol asset check <content>",
104
+ description: "Look up one or more assets (URL, domain, or crypto address) against the ChainPatrol blocklist and external feeds. Returns the aggregated status plus a per-source breakdown. Multiple assets can be passed as positional arguments or via repeated --asset; they are checked in parallel (concurrency 10).",
105
+ usage: "chainpatrol asset check <content> [<content> ...]",
106
+ options: [
107
+ "--asset <content> Asset content (repeatable; alternative to positional args)"
108
+ ],
105
109
  examples: [
106
110
  "chainpatrol asset check https://phish.example",
111
+ "chainpatrol asset check a.example b.example c.example --json",
112
+ "chainpatrol asset check --asset a.example --asset b.example --output csv",
107
113
  "chainpatrol asset check phish.example --output markdown",
108
114
  "chainpatrol --json asset check 0xabc123\u2026"
109
115
  ]
@@ -875,11 +881,12 @@ async function main() {
875
881
  }
876
882
  case "asset": {
877
883
  if (subcommand === "check") {
878
- const positional = cli.input[2];
879
- const content = positional ?? cli.flags.asset;
880
- const { runAssetCheck } = await import("./check-YZRIAUOK.js");
884
+ const positionals = cli.input.slice(2);
885
+ const flagAssets = parseAssetInputs();
886
+ const contents = [...positionals, ...flagAssets];
887
+ const { runAssetCheck } = await import("./check-7QDINQPT.js");
881
888
  await runAssetCheck({
882
- content,
889
+ contents,
883
890
  json: jsonMode,
884
891
  outputFormat: cliContext.outputFormat,
885
892
  explain: cliContext.explain
@@ -888,7 +895,7 @@ async function main() {
888
895
  }
889
896
  const hint = subcommand ? suggest(subcommand, ["check"]) : null;
890
897
  throw new Error(
891
- subcommand ? `Unknown subcommand: asset ${subcommand}${hint ? `. Did you mean "asset ${hint}"?` : ""}` : "Usage: chainpatrol asset check <content>"
898
+ subcommand ? `Unknown subcommand: asset ${subcommand}${hint ? `. Did you mean "asset ${hint}"?` : ""}` : "Usage: chainpatrol asset check <content> [<content> ...]"
892
899
  );
893
900
  }
894
901
  case "detections": {
@@ -1189,12 +1196,12 @@ async function main() {
1189
1196
  case "setup":
1190
1197
  case "install":
1191
1198
  case "i": {
1192
- const { setupSkill } = await import("./setup-skill-DR4KGQMG.js");
1199
+ const { setupSkill } = await import("./setup-skill-BLJLRCXL.js");
1193
1200
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1194
1201
  break;
1195
1202
  }
1196
1203
  case "uninstall": {
1197
- const { uninstallSkill } = await import("./setup-skill-DR4KGQMG.js");
1204
+ const { uninstallSkill } = await import("./setup-skill-BLJLRCXL.js");
1198
1205
  uninstallSkill({ json: jsonMode });
1199
1206
  break;
1200
1207
  }
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-P4L4N5LM.js";
9
+ } from "./chunk-R2DZGMGT.js";
10
10
  import "./chunk-Z76CUWSS.js";
11
11
  export {
12
12
  getBundledSkillContent,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chainpatrol/cli",
3
3
  "description": "The official ChainPatrol CLI — terminal interface for threat detection",
4
4
  "author": "Umar Ahmed <umar@chainpatrol.io>",
5
- "version": "0.9.0",
5
+ "version": "0.10.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -1,85 +0,0 @@
1
- import {
2
- CliExitError,
3
- ExitCode
4
- } from "./chunk-E2LAMILJ.js";
5
- import {
6
- printOutput,
7
- toCsvRows
8
- } from "./chunk-VFT3TD3E.js";
9
- import {
10
- createApiClient
11
- } from "./chunk-RIKR2WFT.js";
12
- import "./chunk-EGWK6SRQ.js";
13
- import "./chunk-TFCNKBRC.js";
14
- import "./chunk-U73SABXK.js";
15
-
16
- // src/commands/asset/check.ts
17
- function statusLine(result) {
18
- const watchTag = result.watchStatus ? ` watch=${result.watchStatus}` : "";
19
- const reasonTag = result.reason ? ` reason=${result.reason}` : "";
20
- return `${result.status} (source=${result.source}${reasonTag}${watchTag})`;
21
- }
22
- async function runAssetCheck(options) {
23
- const content = options.content?.trim();
24
- if (!content) {
25
- throw new CliExitError(
26
- "asset check requires an asset. Example: chainpatrol asset check https://example.com",
27
- ExitCode.USAGE
28
- );
29
- }
30
- const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
31
- const client = options.apiClient ?? createApiClient();
32
- const result = await client.assetCheck({ content });
33
- printOutput({
34
- outputFormat,
35
- json: {
36
- content,
37
- ...result,
38
- explanation: options.explain ? "Asset check aggregates ChainPatrol records and external sources to classify a domain, URL, or crypto address as BLOCKED, ALLOWED, or UNKNOWN." : void 0
39
- },
40
- markdown: [
41
- `# Asset Check: ${content}`,
42
- "",
43
- `- Status: **${result.status}**`,
44
- `- Source: ${result.source}`,
45
- ...result.reason ? [`- Reason: ${result.reason}`] : [],
46
- ...result.watchStatus ? [`- Watch status: ${result.watchStatus}`] : [],
47
- ...result.message ? [`- Message: ${result.message}`] : [],
48
- ...result.code ? [`- Code: ${result.code}`] : [],
49
- "",
50
- "## Per-source results",
51
- "",
52
- ...result.sources.map((entry) => `- ${entry.source}: ${entry.status}`)
53
- ].join("\n"),
54
- csv: toCsvRows(
55
- result.sources.map((entry) => ({
56
- content,
57
- source: entry.source,
58
- status: entry.status
59
- }))
60
- ),
61
- human: () => {
62
- console.log(`${content} -> ${statusLine(result)}`);
63
- if (result.message) {
64
- console.log(`Message: ${result.message}`);
65
- }
66
- if (result.code) {
67
- console.log(`Code: ${result.code}`);
68
- }
69
- if (result.sources.length > 0) {
70
- console.log("Sources:");
71
- for (const entry of result.sources) {
72
- console.log(` - ${entry.source}: ${entry.status}`);
73
- }
74
- }
75
- if (options.explain) {
76
- console.log(
77
- "Status is the aggregated verdict across ChainPatrol and external feeds; see the per-source rows for the underlying signals."
78
- );
79
- }
80
- }
81
- });
82
- }
83
- export {
84
- runAssetCheck
85
- };