solguard-cli 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,69 +1,75 @@
1
- # solguard-cli
2
-
3
- Run SolGuard security agents from your terminal.
4
-
5
- ## Install / run
6
-
7
- ```bash
8
- npx solguard-cli
9
- ```
10
-
11
- No global install required. Works with `npm install -g solguard-cli` as well.
12
-
13
- Requires **Node.js 18+**.
14
-
15
- ## Modes
16
-
17
- | Mode | Description |
18
- |------|-------------|
19
- | **Free** | Uses SolGuard API + your 2 signup credits (same as website). Identity: anonymous `cliInstallId` in `~/.solguard/config.json`. |
20
- | **Premium (Beta)** | Bring your own OpenAI, Anthropic, or Gemini key runs **locally** on your machine. Key never sent to SolGuard. Six services supported. |
21
-
22
- ## Config
23
-
24
- `~/.solguard/config.json` (mode `600` on Unix):
25
-
26
- ```json
27
- {
28
- "cliInstallId": "<uuid>",
29
- "token": "<jwt>",
30
- "baseUrl": "https://www.solguard.space/api",
31
- "savedKeys": {}
32
- }
33
- ```
34
-
35
- ## Premium local (Beta)
36
-
37
- - **BYOK LLM required:** Cyber Security Consultant, Solana Token Verification
38
- - **Local only (no API key):** Wallet Verification, dApp Frontend Scan, OpenClaw Audit, Smart Contract Audit
39
- - **On-chain agents:** set `HELIUS_API_KEY` in your environment
40
-
41
- ## Local development
42
-
43
- 1. Start the API: `npm run dev` (from repo root)
44
- 2. Run the CLI — it auto-detects `http://localhost:3000/api` when the dev server is up
45
-
46
- Or set explicitly:
47
-
48
- ```powershell
49
- $env:SOLGUARD_API = "http://localhost:3000/api"
50
- node bin/solguard-cli.js
51
- ```
52
-
53
- ## Publish (maintainers)
54
-
55
- ```bash
56
- npm login
57
- cd packages/solguard-cli
58
- npm publish --access public
59
- ```
60
-
61
- **Do not publish without explicit approval.**
62
-
63
- ## QA
64
-
65
- ```bash
66
- node scripts/qa-cli-auth.mjs http://localhost:3000/api
67
- node scripts/qa-cli-local-premium.mjs
68
- ```
69
-
1
+ # solguard-cli
2
+
3
+ Run SolGuard security agents from your terminal.
4
+
5
+ ## Install / run
6
+
7
+ ```bash
8
+ npx solguard-cli
9
+ ```
10
+
11
+ No global install required. Works with `npm install -g solguard-cli` as well.
12
+
13
+ Requires **Node.js 18+**.
14
+
15
+ ## Modes
16
+
17
+ | Mode | Description |
18
+ |------|-------------|
19
+ | **Local (recommended)** | Runs on your machine. No SolGuard free credits. Bring your own OpenAI / Anthropic / Gemini key for AI agents. |
20
+ | **Free** | Optionaluses SolGuard API + signup credits (same as website). |
21
+
22
+ Local mode does **not** call the SolGuard backend for execution. Keys stay on your machine (`~/.solguard/config.json`).
23
+
24
+ ## Services (local)
25
+
26
+ - Cyber Security Consultant (BYOK LLM)
27
+ - Solana Token Verification (BYOK LLM + Helius for on-chain)
28
+ - Smart Contract Security Audit (Helius)
29
+ - Wallet Verification (Helius)
30
+ - Web3 dApp Frontend Verification
31
+ - OpenClaw AI Agent Verification
32
+
33
+ ## Config
34
+
35
+ `~/.solguard/config.json` (mode `600` on Unix):
36
+
37
+ ```json
38
+ {
39
+ "cliInstallId": "<uuid>",
40
+ "token": "<jwt>",
41
+ "baseUrl": "https://www.solguard.space/api",
42
+ "savedKeys": {}
43
+ }
44
+ ```
45
+
46
+ ## Local tips
47
+
48
+ - **BYOK LLM:** Cyber Security Consultant, Solana Token Verification
49
+ - **On-chain agents:** set `HELIUS_API_KEY` in your environment
50
+ - Free mode only: needs network access to `https://www.solguard.space/api`
51
+
52
+ ## Local development
53
+
54
+ ```powershell
55
+ $env:SOLGUARD_API = "http://localhost:3000/api"
56
+ node bin/solguard-cli.js
57
+ ```
58
+
59
+ Local **premium** mode works without the API. Free mode needs `npm run dev`.
60
+
61
+ ## Publish (maintainers)
62
+
63
+ ```bash
64
+ npm login
65
+ cd packages/solguard-cli
66
+ npm publish --access public
67
+ ```
68
+
69
+ Bump `version` in `package.json` before republishing.
70
+
71
+ ## QA
72
+
73
+ ```bash
74
+ node scripts/qa-cli-local-premium.mjs
75
+ ```
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { runWizard } from "../src/wizard.js";
3
3
 
4
4
  /** Strip common API key patterns from error output. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "solguard-cli",
3
- "version": "0.1.0",
4
- "description": "Run SolGuard security agents from your terminal — free credits via SolGuard API or premium local BYOK",
3
+ "version": "0.1.1",
4
+ "description": "Run SolGuard security agents locally from your terminal (BYOK) optional free SolGuard credits",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "solguard-cli": "./bin/solguard-cli.js"
package/src/config.js CHANGED
@@ -6,6 +6,10 @@ import crypto from "crypto";
6
6
  export const DEFAULT_BASE_URL = "https://www.solguard.space/api";
7
7
  export const LOCAL_DEV_URL = "http://localhost:3000/api";
8
8
 
9
+ function isLocalUrl(url) {
10
+ return /localhost|127\.0\.0\.1/i.test(url || "");
11
+ }
12
+
9
13
  /** Resolve API base: --api flag > SOLGUARD_API env > config > local dev probe > production. */
10
14
  export async function resolveBaseUrl(config, cliArgs = process.argv.slice(2)) {
11
15
  const flagIdx = cliArgs.findIndex((a) => a === "--api" || a === "-a");
@@ -15,13 +19,16 @@ export async function resolveBaseUrl(config, cliArgs = process.argv.slice(2)) {
15
19
  if (process.env.SOLGUARD_API) {
16
20
  return process.env.SOLGUARD_API.replace(/\/$/, "");
17
21
  }
22
+ // Prefer a saved non-default URL, but never stick on dead localhost.
18
23
  if (config?.baseUrl && config.baseUrl !== DEFAULT_BASE_URL) {
19
- return config.baseUrl.replace(/\/$/, "");
24
+ const saved = config.baseUrl.replace(/\/$/, "");
25
+ if (!isLocalUrl(saved)) return saved;
26
+ if (await probeApi(saved)) return saved;
20
27
  }
21
28
  if (await probeApi(LOCAL_DEV_URL)) {
22
29
  return LOCAL_DEV_URL;
23
30
  }
24
- return (config?.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
31
+ return DEFAULT_BASE_URL;
25
32
  }
26
33
 
27
34
  async function probeApi(baseUrl) {
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Built-in catalog for premium local runs — no SolGuard API required.
3
+ * Kept in sync with lib/solguard/services.js + formSchemas.js for the 6 local agents.
4
+ */
5
+
6
+ export const LOCAL_SERVICES = [
7
+ {
8
+ id: "cyber-consultant",
9
+ name: "Cyber Security Consultant",
10
+ category: "Advisory",
11
+ primaryAgentId: "ai-consultant",
12
+ inputs: [
13
+ {
14
+ key: "query",
15
+ type: "text",
16
+ label: "Your Question",
17
+ placeholder: "e.g. How do honeypot tokens lock liquidity?",
18
+ example: "What is the most common Solana token rug-pull pattern in 2025?",
19
+ multiline: true,
20
+ },
21
+ ],
22
+ },
23
+ {
24
+ id: "solana-token-verification",
25
+ name: "Solana Token Verification",
26
+ category: "Token",
27
+ primaryAgentId: "solana-token-verification",
28
+ inputs: [
29
+ {
30
+ key: "tokenAddress",
31
+ label: "Token Mint Address",
32
+ placeholder: "e.g. DezXAZ8z7Pnr...",
33
+ example: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
34
+ },
35
+ ],
36
+ },
37
+ {
38
+ id: "smart-contract-audit",
39
+ name: "Smart Contract Security Audit",
40
+ category: "Contract",
41
+ primaryAgentId: "contract-security",
42
+ inputs: [
43
+ {
44
+ key: "tokenAddress",
45
+ label: "Token Mint Address",
46
+ placeholder: "e.g. DezXAZ8z7Pnr...",
47
+ example: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ id: "wallet-verification",
53
+ name: "Wallet Verification",
54
+ category: "Wallet",
55
+ primaryAgentId: "wallet-verification",
56
+ inputs: [
57
+ {
58
+ key: "walletAddress",
59
+ label: "Wallet Address",
60
+ placeholder: "Solana wallet pubkey",
61
+ example: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ id: "dapp-frontend-verification",
67
+ name: "Web3 dApp Frontend Verification",
68
+ category: "Web",
69
+ primaryAgentId: "website-security",
70
+ inputs: [
71
+ {
72
+ key: "url",
73
+ type: "url",
74
+ label: "Website URL",
75
+ placeholder: "https://example.com",
76
+ example: "https://solana.com",
77
+ },
78
+ ],
79
+ },
80
+ {
81
+ id: "openclaw-ai-agent-verification",
82
+ name: "OpenClaw AI Agent Verification",
83
+ category: "AI Agent",
84
+ primaryAgentId: "openclaw-ai-agent-verification",
85
+ inputs: [
86
+ {
87
+ key: "config",
88
+ type: "json",
89
+ label: "Agent Config (JSON)",
90
+ placeholder: '{"name":"support-agent","gateway":{"auth":"none"}}',
91
+ example: '{"name":"support-agent","gateway":{"auth":"none"}}',
92
+ },
93
+ ],
94
+ },
95
+ ];
96
+
97
+ export function listLocalServices() {
98
+ return LOCAL_SERVICES;
99
+ }
100
+
101
+ export function getLocalService(serviceId) {
102
+ return LOCAL_SERVICES.find((s) => s.id === serviceId) || null;
103
+ }
package/src/wizard.js CHANGED
@@ -1,483 +1,225 @@
1
1
  import * as p from "@clack/prompts";
2
-
3
2
  import fs from "fs";
4
3
 
5
4
  import { ensureConfig, updateConfig, saveConfig, resolveBaseUrl } from "./config.js";
6
-
7
5
  import {
8
-
9
6
  ensureAuth,
10
-
11
7
  fetchConfig,
12
-
13
- fetchServices,
14
-
15
- fetchServiceDetail,
16
-
17
8
  fetchMe,
18
-
19
9
  runAgentFree,
20
-
21
10
  } from "./api.js";
22
-
23
- import { formatReport, printReport, reportFilename } from "./output.js";
24
-
11
+ import { printReport, reportFilename } from "./output.js";
25
12
  import {
26
-
27
13
  listByokProviders,
28
-
29
14
  runAgentLocalPremium,
30
-
31
- canRunLocalPremium,
32
-
33
15
  agentUsesByokLlm,
34
-
35
16
  localPremiumRequiresHelius,
36
-
37
17
  } from "./premium.js";
38
-
39
-
18
+ import { listLocalServices } from "./localCatalog.js";
40
19
 
41
20
  function isCancel(v) {
42
-
43
21
  if (p.isCancel(v)) {
44
-
45
22
  p.cancel("Cancelled.");
46
-
47
23
  process.exit(0);
48
-
49
24
  }
50
-
51
25
  return v;
52
-
53
26
  }
54
27
 
55
-
56
-
57
28
  async function promptInputs(fields) {
58
-
59
29
  const inputs = {};
60
-
61
30
  for (const field of fields) {
62
-
63
31
  const opts = {
64
-
65
32
  message: field.label || field.key,
66
-
67
33
  placeholder: field.placeholder || field.example || "",
68
-
69
34
  };
70
-
71
35
  if (field.type === "json") {
72
-
73
36
  opts.message += " (paste JSON)";
74
-
75
37
  }
76
-
77
- if (field.multiline || field.type === "text" || field.type === "json") {
78
-
79
- let val = isCancel(await p.text({ ...opts, defaultValue: "" }));
80
-
81
- while (!val?.trim()) {
82
-
83
- p.log.warn("Please enter a value.");
84
-
85
- val = isCancel(await p.text({ ...opts, defaultValue: "" }));
86
-
87
- }
88
-
89
- inputs[field.key] = val.trim();
90
-
91
- } else {
92
-
93
- let val = isCancel(await p.text({ ...opts, defaultValue: "" }));
94
-
95
- while (!val?.trim()) {
96
-
97
- p.log.warn("Please enter a value.");
98
-
99
- val = isCancel(await p.text({ ...opts, defaultValue: "" }));
100
-
101
- }
102
-
103
- inputs[field.key] = val.trim();
104
-
38
+ let val = isCancel(await p.text({ ...opts, defaultValue: "" }));
39
+ while (!val?.trim()) {
40
+ p.log.warn("Please enter a value.");
41
+ val = isCancel(await p.text({ ...opts, defaultValue: "" }));
105
42
  }
106
-
43
+ inputs[field.key] = val.trim();
107
44
  }
108
-
109
45
  return inputs;
110
-
111
46
  }
112
47
 
113
-
114
-
115
48
  function showReport(report, opts) {
116
-
117
49
  p.log.step("Report");
118
-
119
50
  printReport(report, opts);
120
-
121
51
  }
122
52
 
123
-
124
-
125
53
  async function promptSaveReport(report) {
126
-
127
54
  const save = isCancel(
128
-
129
55
  await p.confirm({ message: "Save result to file?", initialValue: false })
130
-
131
56
  );
132
-
133
57
  if (!save) return;
134
-
135
58
  const name = reportFilename();
136
-
137
59
  fs.writeFileSync(name, JSON.stringify(report, null, 2), "utf8");
138
-
139
60
  p.log.success(`Saved to ${name}`);
140
-
141
61
  }
142
62
 
143
-
144
-
145
63
  async function promptByokCredentials(config) {
146
-
147
64
  const providers = listByokProviders();
148
-
149
65
  const providerId = isCancel(
150
-
151
66
  await p.select({
152
-
153
67
  message: "LLM provider",
154
-
155
68
  options: providers.map((pr) => ({ value: pr.id, label: pr.label })),
156
-
157
69
  })
158
-
159
70
  );
160
71
 
161
-
162
-
163
72
  let apiKey = config.savedKeys?.[providerId];
164
-
165
73
  if (apiKey) {
166
-
167
74
  const reuse = isCancel(
168
-
169
75
  await p.confirm({
170
-
171
76
  message: `Use saved ${providerId} key from ~/.solguard/config.json?`,
172
-
173
77
  initialValue: true,
174
-
175
78
  })
176
-
177
79
  );
178
-
179
80
  if (!reuse) apiKey = null;
180
-
181
81
  }
182
82
 
183
-
184
-
185
83
  if (!apiKey) {
186
-
187
84
  apiKey = isCancel(await p.password({ message: "API key (input hidden)" }));
188
-
189
85
  const saveKey = isCancel(
190
-
191
86
  await p.confirm({ message: "Save this key for future runs?", initialValue: false })
192
-
193
87
  );
194
-
195
88
  if (saveKey) {
196
-
197
89
  config.savedKeys = { ...config.savedKeys, [providerId]: apiKey };
198
-
199
90
  saveConfig(config);
200
-
201
91
  p.log.info("Key saved to ~/.solguard/config.json (mode 600)");
202
-
203
92
  }
204
-
205
93
  }
206
94
 
207
-
208
-
209
95
  return { providerId, apiKey };
210
-
211
96
  }
212
97
 
213
-
214
-
215
98
  function premiumModeLabel(agentId) {
216
-
217
99
  if (agentUsesByokLlm(agentId)) {
218
-
219
- return "Premium (Beta) — your own API key (runs locally, key never sent to SolGuard)";
220
-
100
+ return "Local (recommended) — your own API key, no SolGuard credits";
221
101
  }
222
-
223
- return "Premium (Beta) — run locally (no SolGuard backend)";
224
-
102
+ return "Local (recommended) — runs on your machine, no SolGuard credits";
225
103
  }
226
104
 
227
-
228
-
229
105
  export async function runWizard() {
230
-
231
106
  p.intro("SolGuard CLI · Beta");
232
-
233
-
107
+ p.log.info("Default: run locally on your machine. SolGuard free credits are optional.");
234
108
 
235
109
  let config = ensureConfig();
236
-
237
- config.baseUrl = await resolveBaseUrl(config);
238
-
239
- updateConfig({ baseUrl: config.baseUrl });
240
-
241
-
242
-
243
- config = { ...config, ...(await ensureAuth(config)) };
244
-
245
- updateConfig({ token: config.token });
246
-
247
-
248
-
249
- if (config.baseUrl.includes("localhost")) {
250
-
251
- p.log.info(`Using local API: ${config.baseUrl}`);
252
-
253
- }
254
-
255
-
256
-
257
- const remoteConfig = await fetchConfig(config.baseUrl);
258
-
259
- const services = await fetchServices(config.baseUrl);
260
-
261
-
262
-
263
- if (!services.length) {
264
-
265
- p.log.error("No services returned from API.");
266
-
267
- process.exit(1);
268
-
269
- }
270
-
271
-
110
+ const services = listLocalServices();
272
111
 
273
112
  const serviceId = isCancel(
274
-
275
113
  await p.select({
276
-
277
114
  message: "Select a service",
278
-
279
115
  options: services.map((s) => ({
280
-
281
116
  value: s.id,
282
-
283
117
  label: s.name,
284
-
285
118
  hint: s.category,
286
-
287
119
  })),
288
-
289
120
  })
290
-
291
121
  );
292
122
 
293
-
294
-
295
123
  const service = services.find((s) => s.id === serviceId);
296
-
297
- const detail = await fetchServiceDetail(config.baseUrl, serviceId);
298
-
299
124
  const agentId = service.primaryAgentId;
125
+ const inputFields = service.inputs || [];
300
126
 
301
- const inputFields = detail?.agent?.inputs || [];
302
-
303
- const supportsLocalPremium = canRunLocalPremium(agentId);
304
-
305
-
306
-
307
- let mode = "free";
308
-
309
- if (supportsLocalPremium) {
310
-
311
- mode = isCancel(
312
-
313
- await p.select({
314
-
315
- message: "Run mode",
316
-
317
- options: [
318
-
319
- {
320
-
321
- value: "free",
322
-
323
- label: "Free — SolGuard credits (server-side execution)",
324
-
325
- hint: `${config.credits ?? "?"} credits remaining`,
326
-
327
- },
328
-
329
- {
330
-
331
- value: "premium",
332
-
333
- label: premiumModeLabel(agentId),
334
-
335
- },
336
-
337
- ],
338
-
339
- })
340
-
341
- );
342
-
343
- } else {
344
-
345
- p.log.info("Premium local mode is not available for this service (requires SolGuard backend). Using free credits.");
346
-
347
- }
348
-
349
-
127
+ const mode = isCancel(
128
+ await p.select({
129
+ message: "Run mode",
130
+ initialValue: "premium",
131
+ options: [
132
+ {
133
+ value: "premium",
134
+ label: premiumModeLabel(agentId),
135
+ hint: agentUsesByokLlm(agentId) ? "BYOK" : "offline rules / RPC",
136
+ },
137
+ {
138
+ value: "free",
139
+ label: "Free — SolGuard credits (server-side)",
140
+ hint: "uses your signup credits",
141
+ },
142
+ ],
143
+ })
144
+ );
350
145
 
351
146
  const inputs = await promptInputs(inputFields);
352
-
353
147
  const s = p.spinner();
354
-
355
-
356
-
357
148
  let report;
358
149
 
359
-
360
-
361
- if (mode === "premium" && supportsLocalPremium) {
362
-
150
+ if (mode === "premium") {
363
151
  if (localPremiumRequiresHelius(agentId) && !process.env.HELIUS_API_KEY) {
364
-
365
152
  p.log.warn("HELIUS_API_KEY is not set — on-chain scans will fail. Export it before running.");
366
-
367
153
  }
368
154
 
369
-
370
-
371
155
  let providerId = null;
372
-
373
156
  let apiKey = null;
374
-
375
157
  if (agentUsesByokLlm(agentId)) {
376
-
377
158
  ({ providerId, apiKey } = await promptByokCredentials(config));
378
-
379
159
  }
380
160
 
381
-
382
-
383
- s.start(agentUsesByokLlm(agentId)
384
-
385
- ? "Running locally via your provider (no SolGuard backend call)…"
386
-
387
- : "Running locally (no SolGuard backend call)…");
388
-
161
+ s.start(
162
+ agentUsesByokLlm(agentId)
163
+ ? "Running locally via your provider (no SolGuard credits)…"
164
+ : "Running locally (no SolGuard credits)…"
165
+ );
389
166
  try {
390
-
391
167
  const result = await runAgentLocalPremium(agentId, inputs, { provider: providerId, apiKey });
392
-
393
168
  if (result?.error) {
394
-
395
169
  s.stop("Failed");
396
-
397
170
  p.log.error(result.error);
398
-
399
171
  process.exit(1);
400
-
401
172
  }
402
-
403
173
  report = { agentId, agentName: service.name, result };
404
-
405
174
  s.stop("Done");
406
-
407
- const modeLabel = providerId ? `Premium (${providerId}, local)` : "Premium (local)";
408
-
175
+ const modeLabel = providerId ? `Local (${providerId})` : "Local";
409
176
  showReport(report, { mode: modeLabel });
410
-
411
177
  } catch (e) {
412
-
413
178
  s.stop("Failed");
414
-
415
179
  throw e;
416
-
417
180
  }
418
-
419
181
  } else {
182
+ config.baseUrl = await resolveBaseUrl(config);
183
+ updateConfig({ baseUrl: config.baseUrl });
184
+ if (config.baseUrl.includes("localhost")) {
185
+ p.log.info(`Using local API: ${config.baseUrl}`);
186
+ }
420
187
 
188
+ config = { ...config, ...(await ensureAuth(config)) };
189
+ updateConfig({ token: config.token });
190
+
191
+ const remoteConfig = await fetchConfig(config.baseUrl);
421
192
  const paymentMethod = remoteConfig.testingModeFreeRuns ? "testing" : "credit";
422
193
 
423
194
  if (paymentMethod === "credit" && (config.credits ?? 0) <= 0) {
424
-
425
- p.log.error("No free credits left. Try premium local mode or connect a wallet on solguard.space.");
426
-
195
+ p.log.error(
196
+ "No free credits left. Choose Local mode (your own API key) or top up on solguard.space."
197
+ );
427
198
  process.exit(1);
428
-
429
199
  }
430
200
 
431
-
432
-
433
201
  s.start("Running via SolGuard API…");
434
-
435
202
  try {
436
-
437
203
  report = await runAgentFree(config.baseUrl, config.token, agentId, inputs, paymentMethod);
438
-
439
204
  s.stop("Done");
440
-
441
205
  const me = await fetchMe(config.baseUrl, config.token);
442
-
443
206
  const creditsRemaining = me.credits;
444
-
445
207
  showReport(report, {
446
-
447
208
  mode: paymentMethod === "testing" ? "Free (testing mode)" : "Free (SolGuard credits)",
448
-
449
209
  creditsRemaining,
450
-
451
210
  });
452
-
453
211
  config.credits = creditsRemaining;
454
-
455
212
  updateConfig({ token: config.token, credits: creditsRemaining });
456
-
457
213
  } catch (e) {
458
-
459
214
  s.stop("Failed");
460
-
461
215
  if (e.status === 402) {
462
-
463
- p.log.error("Payment required — no credits left.");
464
-
216
+ p.log.error("Payment required — no credits left. Use Local mode instead.");
465
217
  process.exit(1);
466
-
467
218
  }
468
-
469
219
  throw e;
470
-
471
220
  }
472
-
473
221
  }
474
222
 
475
-
476
-
477
223
  await promptSaveReport(report);
478
-
479
224
  p.outro("Done");
480
-
481
225
  }
482
-
483
-