@vigilbase/cli 0.2.2 → 0.2.3

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/CLOUDFLARE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `vigilbase cf` and `vigilbase cloudflare` use the authenticated Vigilbase One API. Run `vigilbase login` first (signing in isn't supported on Windows yet). Your organization, permissions, customer links, and allowed zones determine access. The CLI never requires a Cloudflare API key.
4
4
 
5
- `cf checkup` runs on One's servers. `cloudflare-checkup` (also `local cloudflare-checkup`) runs on your machine with Cloudflare's own `cf` CLI and needs no Vigilbase login: one command, `npx @vigilbase/cli cloudflare-checkup`, verifies your work email with a code, installs cf if needed, signs in read-only, collects and uploads after you confirm. It works while vigilbase.com has local checkups switched on; otherwise it says so before asking for your email and points you to the checkup in your browser at https://vigilbase.com/tools/cloudflare-checkup (see [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/LOCAL.md)).
5
+ `cf checkup` runs on One's servers. `cloudflare-checkup` (also `local cloudflare-checkup`) runs on your machine with Cloudflare's own `cf` CLI and needs no Vigilbase login: one command, `npx @vigilbase/cli cloudflare-checkup`, verifies your work email with a code, installs cf if needed, signs in read-only, collects and uploads after you confirm. It works while vigilbase.com has local checkups switched on; otherwise it says so before asking for your email and points you to the checkup in your browser at https://vigilbase.com/tools/cloudflare-checkup (see [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md)).
6
6
 
7
7
  Customer provider access requires an `active` or `trialing` Cloudflare service
8
8
  entitlement for the selected account. Historical/inactive rows and legacy account
package/COMMANDS.md CHANGED
@@ -10,11 +10,11 @@ below omit `--json`, which every command supports.
10
10
 
11
11
  | Area | Complete command and API reference |
12
12
  | --- | --- |
13
- | Cloudflare | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/CLOUDFLARE.md) |
14
- | Security | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/SECURITY.md) |
15
- | Customers, organizations, monitoring, support | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/ONE.md) |
16
- | Local Cloudflare Checkup (cf on your machine) | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/LOCAL.md) |
17
- | Sign-in, credentials, configuration, troubleshooting | [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/README.md) |
13
+ | Cloudflare | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) |
14
+ | Security | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) |
15
+ | Customers, organizations, monitoring, support | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/ONE.md) |
16
+ | Local Cloudflare Checkup (cf on your machine) | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) |
17
+ | Sign-in, credentials, configuration, troubleshooting | [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md) |
18
18
 
19
19
  The references list the supported operations, filters, response and pagination
20
20
  behaviour, API endpoints and access requirements of the CLI.
@@ -141,7 +141,19 @@ credentials file is there.
141
141
  When a public scan fails and vigilbase.com explains why with a known error
142
142
  code, the CLI prints `Error: <message> (<code>)` (JSON: `error`, `code`,
143
143
  `status`), with the message cleaned to one line of at most 300 characters.
144
- Otherwise it prints `Request failed (HTTP <status>).`
144
+ Otherwise it prints a message of its own, never the server's text.
145
+
146
+ When a scan run through Vigilbase One fails (One answers 422), the CLI prints
147
+ `Error: The scan couldn't be completed. Try again later.` and exits 1. JSON
148
+ has `success: false`, that `error`, `status: 422` and the `toolRunId` One
149
+ recorded, for `vigilbase runs get <toolRunId>`.
150
+
151
+ When the scanner ran but couldn't assess the target, for example because the
152
+ site answered with a bot verification challenge or an error page, the CLI
153
+ prints `✗ <tool> · <target> · n/a` and the scanner's reason on the next line,
154
+ cleaned to one line of at most 300 characters, and exits 1. JSON has
155
+ `success: false`, that reason as `error`, `grade: null`, the scanner's
156
+ `result`, `status: 422`, `code: "scan_unassessed"` and the `toolRunId`.
145
157
 
146
158
  ## Account and billing
147
159
 
@@ -158,7 +170,7 @@ Otherwise it prints `Request failed (HTTP <status>).`
158
170
  | `billing` | `summary`, `show`, `status` | GET `/api/account/billing` |
159
171
  | `billing portal` | `open`; `--no-open` prints the portal URL without opening a browser | POST `/api/billing/portal` |
160
172
 
161
- Invitations need a One browser session: Identity's management API doesn't accept CLI sign-ins, so `account invites` and `account invites create` answer 403 (`browser_session_required`) and the CLI points you to https://one.vigilbase.com.
173
+ Invitations need a One browser session: Identity's management API doesn't accept CLI sign-ins, so `account invites` and `account invites create` answer 403 (`browser_session_required`): the CLI prints `You don't have access to this.` and points you to https://one.vigilbase.com.
162
174
 
163
175
  Creating an invitation sends it; `billing portal` starts a Stripe portal
164
176
  session. Both are explicit actions. Billing data comes from One's Stripe
@@ -226,14 +238,21 @@ the uploaded data and the error codes.
226
238
 
227
239
  Exit codes: **0** success, **1** validation, API, network or sign-in failure,
228
240
  **2** HTTP 429 rate limiting. A completed scan whose result is unsuccessful
229
- also exits 1. A signed-out `whoami` is not a failure, and a healthy `doctor`
241
+ also exits 1. So does a scan the scanner could not grade (result `status`
242
+ `"error"`, no grade): it prints `✗ <tool> · <target> · n/a` and the scanner's
243
+ message, cleaned to one line of at most 300 characters, and JSON has
244
+ `success: false` with that message as `error`. A signed-out `whoami` is not a
245
+ failure, and a healthy `doctor`
230
246
  does not need a sign-in. API error JSON keeps the status, code and retry delay
231
247
  when available.
232
248
 
233
249
  Vigilbase One's error messages are never shown, because they can describe
234
- One's configuration: the CLI prints `Request failed (HTTP <status>).` When it
235
- recognises the failure from the route, the status and One's error code, it adds
236
- a hint of its own (`Hint: …` in text, `hint` in JSON):
250
+ One's configuration. The CLI prints its own message for the status instead:
251
+ ``Authentication required. Run `vigilbase login`.`` (401),
252
+ `You don't have access to this.` (403), `Rate limit exceeded.` (429) and
253
+ `Request failed (HTTP <status>).` otherwise. When it recognises the failure
254
+ from the route, the status and One's error code, it adds a hint of its own
255
+ (`Hint: …` in text, `hint` in JSON):
237
256
 
238
257
  | One answers | Hint |
239
258
  | --- | --- |
package/LOCAL.md CHANGED
@@ -15,7 +15,7 @@ before it asks for your email; when local mode is off it stops with
15
15
  your browser at <https://vigilbase.com/tools/cloudflare-checkup>.
16
16
 
17
17
  `vigilbase cf checkup` runs on Vigilbase One's servers with stored credentials
18
- (see [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/CLOUDFLARE.md));
18
+ (see [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md));
19
19
  `vigilbase cloudflare-checkup` runs on your machine and needs no Vigilbase
20
20
  login.
21
21
 
@@ -34,7 +34,7 @@ of the CLI (0.1.x) is running instead: `npx` prefers an installed global
34
34
  update the global install with `npm install -g @vigilbase/cli@latest`. With
35
35
  Yarn 4.15+ or pnpm 11+, `yarn dlx` and `pnpm dlx` run an older release for a
36
36
  day after each release; use `npx` (see
37
- [Troubleshooting](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/README.md#troubleshooting)).
37
+ [Troubleshooting](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md#troubleshooting)).
38
38
 
39
39
  The command does everything, in this order, and stops at the first problem
40
40
  before any slower step:
package/ONE.md CHANGED
@@ -95,4 +95,4 @@ vigilbase support tickets [--all] [--limit 1..100] [--customer-id <uuid>] [--jso
95
95
 
96
96
  ## Related commands and validation
97
97
 
98
- See [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/README.md) for sign-in and configuration, [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/COMMANDS.md) for account, billing, public scanners, run history and Watchdog, [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/CLOUDFLARE.md) for `cf`/`cloudflare`, and [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/SECURITY.md) for security commands. The read operations above do not replace the existing explicitly confirmed write commands.
98
+ See [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md) for sign-in and configuration, [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/COMMANDS.md) for account, billing, public scanners, run history and Watchdog, [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) for `cf`/`cloudflare`, and [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) for security commands. The read operations above do not replace the existing explicitly confirmed write commands.
package/README.md CHANGED
@@ -28,11 +28,11 @@ supported on Windows yet; public tools and `cloudflare-checkup` still work there
28
28
 
29
29
  | Reference | Covers |
30
30
  | --- | --- |
31
- | [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/COMMANDS.md) | Core commands, aliases, global options, API mapping, sign-in, output |
32
- | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/CLOUDFLARE.md) | Accounts, zones, DNS, WAF, analytics, Zero Trust, Workers/Pages, audit, checkups |
33
- | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/SECURITY.md) | Incidents, logs, intelligence, analytics, assets, connections, policies, notifications, reports |
34
- | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/ONE.md) | Customer records, billing, reports, investigations, organizations, monitoring, support |
35
- | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/LOCAL.md) | `cloudflare-checkup`: verifies your work email, collects your Cloudflare configuration on your machine with Cloudflare's `cf` and uploads it after you confirm |
31
+ | [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/COMMANDS.md) | Core commands, aliases, global options, API mapping, sign-in, output |
32
+ | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) | Accounts, zones, DNS, WAF, analytics, Zero Trust, Workers/Pages, audit, checkups |
33
+ | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) | Incidents, logs, intelligence, analytics, assets, connections, policies, notifications, reports |
34
+ | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/ONE.md) | Customer records, billing, reports, investigations, organizations, monitoring, support |
35
+ | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) | `cloudflare-checkup`: verifies your work email, collects your Cloudflare configuration on your machine with Cloudflare's `cf` and uploads it after you confirm |
36
36
 
37
37
  These files ship with the package. Built-in help: `vigilbase <command> --help`
38
38
  or `vigilbase help <command>`; `vigilbase help <tool>` shows how to run a tool.
@@ -143,7 +143,7 @@ read-only Cloudflare API token instead. When you are done:
143
143
  npx @vigilbase/cli cloudflare-checkup logout # removes the read-only cf login and the cf it installed
144
144
  ```
145
145
 
146
- See [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.2/files/LOCAL.md) for
146
+ See [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) for
147
147
  every step, what is uploaded and the error codes.
148
148
 
149
149
  ## Public tools and sign-in
@@ -191,9 +191,12 @@ fails and vigilbase.com explains why, the CLI shows that reason:
191
191
  `Error: <message> (<code>)` in text, and `error` plus `code` in JSON. The
192
192
  reason is shown only when vigilbase.com sends it as a known error code, and it
193
193
  is cleaned to one line of at most 300 characters. Anything else, and every
194
- Vigilbase One error, prints only `Request failed (HTTP <status>).` without the
195
- server's text. Hints (`Hint: …`, `hint` in JSON) are always written by the CLI;
196
- COMMANDS.md lists the One errors that get one.
194
+ Vigilbase One error, prints a message the CLI wrote (for example
195
+ `You don't have access to this.` or `Request failed (HTTP <status>).`), never
196
+ the server's text. The exception is a One scan the scanner couldn't assess
197
+ (for example a bot verification challenge): the CLI prints the scanner's
198
+ reason, cleaned the same way. Hints (`Hint: …`, `hint` in JSON) are always written by the
199
+ CLI; COMMANDS.md lists the One errors that get one.
197
200
 
198
201
  Commands use named One APIs and One's own authorization. They never take
199
202
  Cloudflare or other provider secrets, delegated identity claims or an arbitrary
package/SECURITY.md CHANGED
@@ -99,7 +99,7 @@ Provider events map `--provider` to `provider_id`, `--dataset` to `dataset_id`,
99
99
  | `settings broadcast-emails` | None | `/settings/broadcast-emails` |
100
100
  | `settings applications` | None | `/integrations/grants` |
101
101
 
102
- Intel lookup input is trimmed and limited to 512 characters; the server validates the indicator and applies lookup quota. Personal settings show existing preferences and authorized application grants. They do not change preferences or revoke grants. Personal settings and `notifications preferences` belong to each user: with `--customer-id` (View-as), a refusal prints `Request failed (HTTP 403).` with the hint `Personal settings belong to each user, so View-as (--customer-id) cannot read them.` Reports are fetched on demand; this CLI does not persist report or billing records.
102
+ Intel lookup input is trimmed and limited to 512 characters; the server validates the indicator and applies lookup quota. Personal settings show existing preferences and authorized application grants. They do not change preferences or revoke grants. Personal settings and `notifications preferences` belong to each user: with `--customer-id` (View-as), a refusal prints `You don't have access to this.` with the hint `Personal settings belong to each user, so View-as (--customer-id) cannot read them.` Reports are fetched on demand; this CLI does not persist report or billing records.
103
103
 
104
104
  ## Limits and pagination
105
105
 
@@ -14,10 +14,13 @@ export declare class DeviceAuthError extends Error {
14
14
  readonly code?: string;
15
15
  readonly status: number;
16
16
  readonly body: unknown;
17
+ /** The message is the CLI's own text, never the server's (see VigilbaseApiError). */
18
+ readonly isCliAuthored: boolean;
17
19
  constructor(message: string, options: {
18
20
  status: number;
19
21
  code?: string;
20
22
  body?: unknown;
23
+ isCliAuthored?: boolean;
21
24
  });
22
25
  }
23
26
  export declare function requestDeviceCode(config: CliConfig, options?: {
@@ -8,12 +8,15 @@ export class DeviceAuthError extends Error {
8
8
  code;
9
9
  status;
10
10
  body;
11
+ /** The message is the CLI's own text, never the server's (see VigilbaseApiError). */
12
+ isCliAuthored;
11
13
  constructor(message, options) {
12
14
  super(message);
13
15
  this.name = 'DeviceAuthError';
14
16
  this.status = options.status;
15
17
  this.code = options.code;
16
18
  this.body = options.body;
19
+ this.isCliAuthored = options.isCliAuthored ?? false;
17
20
  }
18
21
  }
19
22
  function authBase(config) {
@@ -139,7 +142,7 @@ export async function pollDeviceToken(config, deviceCode) {
139
142
  },
140
143
  };
141
144
  }
142
- throw new DeviceAuthError(`Device token request failed (HTTP ${status})`, { status, code, body: errorBody });
145
+ throw new DeviceAuthError(`Device token request failed (HTTP ${status})`, { status, code, body: errorBody, isCliAuthored: true });
143
146
  }
144
147
  export async function sleep(ms, signal) {
145
148
  await new Promise((resolve, reject) => {
@@ -198,14 +201,19 @@ export async function waitForDeviceAuthorization(config, device, options) {
198
201
  consecutiveTransient += 1;
199
202
  lastTransientStatus = result.httpStatus;
200
203
  if (consecutiveTransient >= maxTransient) {
204
+ // After an HTTP error the description is the server's, so only a network failure's
205
+ // own (CLI-authored) description joins the message.
206
+ const detail = (!lastTransientStatus && result.error.error_description) ||
207
+ 'Retry `vigilbase login` once One is healthy.';
201
208
  throw new DeviceAuthError(`Lost contact with device authorization on ${config.oneBaseUrl} while waiting for approval` +
202
209
  (lastTransientStatus
203
210
  ? ` (HTTP ${lastTransientStatus})`
204
211
  : '') +
205
- `. ${result.error.error_description ?? 'Retry `vigilbase login` once One is healthy.'}`, {
212
+ `. ${detail}`, {
206
213
  status: lastTransientStatus,
207
214
  code: 'unreachable',
208
215
  body: result.error,
216
+ isCliAuthored: true,
209
217
  });
210
218
  }
211
219
  break;
@@ -214,9 +222,10 @@ export async function waitForDeviceAuthorization(config, device, options) {
214
222
  status: 403,
215
223
  code: 'access_denied',
216
224
  body: result.error,
225
+ isCliAuthored: true,
217
226
  });
218
227
  case 'expired':
219
- throw new DeviceAuthError('The device code expired. Run `vigilbase login` again.', { status: 400, code: 'expired_token', body: result.error });
228
+ throw new DeviceAuthError('The device code expired. Run `vigilbase login` again.', { status: 400, code: 'expired_token', body: result.error, isCliAuthored: true });
220
229
  default: {
221
230
  const _exhaustive = result;
222
231
  throw new DeviceAuthError(`Unexpected poll result: ${JSON.stringify(_exhaustive)}`, {
@@ -4,7 +4,7 @@ import { NetworkError } from '../lib/network.js';
4
4
  import { PUBLIC_TOOLS_PATH, VigilbaseApiError } from '../lib/client.js';
5
5
  import { getOneAccessToken, getOptionalOneAccessToken, hasUsableSignIn, readCredentials } from '../auth/index.js';
6
6
  import { SignInUnsupportedError } from '../auth/store.js';
7
- import { formatExecuteResult, formatToolDescription, formatToolsList, } from '../lib/format.js';
7
+ import { formatExecuteResult, formatToolDescription, formatToolsList, isSuccessfulScan, } from '../lib/format.js';
8
8
  import { isOneAuthRequiredTool, isOneExecutableTool, ONE_AUTH_REQUIRED_TOOLS, resolveToolDefinition, VigilbaseOneClient, } from '../lib/one-client.js';
9
9
  import { EXIT, ok, outputJson, withSpinner, } from './helpers.js';
10
10
  const ONE_SIGN_IN_REQUIRED = 'This tool needs a Vigilbase One sign-in: run `vigilbase login`.';
@@ -99,21 +99,27 @@ export const handleRun = async (ctx) => {
99
99
  }
100
100
  const oneClient = new VigilbaseOneClient(ctx.config, accessToken, true);
101
101
  const oneResult = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => oneClient.executeTool(toolName, target));
102
- formatExecuteResult({
103
- success: Boolean(oneResult.success),
102
+ const unassessed = oneResult.code === 'scan_unassessed';
103
+ const toolRunId = oneResult.data?.toolRunId;
104
+ const result = {
105
+ success: oneResult.success === true,
104
106
  toolName,
105
107
  target,
106
108
  grade: oneResult.data?.grade ?? null,
107
109
  gradeScore: oneResult.data?.gradeScore ?? null,
108
- result: oneResult.data?.result ?? null,
109
- error: oneResult.error ?? null,
110
+ // Only a completed scan, or one the scanner could not assess, carries the scanner's result.
111
+ result: oneResult.success === true || unassessed ? oneResult.data?.result ?? null : null,
112
+ // One's error text is server text; a failed scan prints the CLI's own message.
113
+ error: null,
110
114
  uiUrl: `${ctx.config.oneBaseUrl}/dashboard/tools/${toolName}`,
111
- }, ctx.mode, ctx.writer);
112
- return oneResult.success ? ok() : EXIT.ERROR;
115
+ ...(unassessed ? { status: 422, code: 'scan_unassessed', ...(toolRunId ? { toolRunId } : {}) } : {}),
116
+ };
117
+ formatExecuteResult(result, ctx.mode, ctx.writer);
118
+ return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
113
119
  }
114
120
  const result = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => ctx.publicClient.executeTool(toolName, target));
115
121
  formatExecuteResult(result, ctx.mode, ctx.writer);
116
- return result.success ? ok() : EXIT.ERROR;
122
+ return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
117
123
  };
118
124
  export const handleDescribe = async (ctx) => {
119
125
  validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
@@ -9,6 +9,13 @@ export declare class VigilbaseApiError extends Error {
9
9
  readonly reason?: string;
10
10
  /** CLI-authored remediation, never server text (see one-error-hints.ts). */
11
11
  readonly hint?: string;
12
+ /**
13
+ * The message is the CLI's own text, never the server's, so formatError prints it although the
14
+ * error carries a body. Unmarked errors with a body print only a generic `Request failed`.
15
+ */
16
+ readonly isCliAuthored: boolean;
17
+ /** The One tool run a failed scan recorded. */
18
+ readonly toolRunId?: string;
12
19
  constructor(message: string, options: {
13
20
  status: number;
14
21
  body?: unknown;
@@ -16,6 +23,8 @@ export declare class VigilbaseApiError extends Error {
16
23
  code?: string;
17
24
  reason?: string;
18
25
  hint?: string;
26
+ isCliAuthored?: boolean;
27
+ toolRunId?: string;
19
28
  });
20
29
  }
21
30
  export declare function retryAfterSeconds(response: Response, body: unknown): number | undefined;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { singleLine } from './format.js';
2
+ import { SCAN_FAILED_MESSAGE, singleLine } from './format.js';
3
3
  import { parseJsonResponse, requestText } from './network.js';
4
4
  import { fetchSkillText } from './skill-fetch.js';
5
5
  import { AGENT_API_ERROR_CODES, } from '../types.js';
@@ -12,6 +12,13 @@ export class VigilbaseApiError extends Error {
12
12
  reason;
13
13
  /** CLI-authored remediation, never server text (see one-error-hints.ts). */
14
14
  hint;
15
+ /**
16
+ * The message is the CLI's own text, never the server's, so formatError prints it although the
17
+ * error carries a body. Unmarked errors with a body print only a generic `Request failed`.
18
+ */
19
+ isCliAuthored;
20
+ /** The One tool run a failed scan recorded. */
21
+ toolRunId;
15
22
  constructor(message, options) {
16
23
  super(message);
17
24
  this.name = 'VigilbaseApiError';
@@ -21,6 +28,8 @@ export class VigilbaseApiError extends Error {
21
28
  this.code = options.code;
22
29
  this.reason = options.reason;
23
30
  this.hint = options.hint;
31
+ this.isCliAuthored = options.isCliAuthored ?? false;
32
+ this.toolRunId = options.toolRunId;
24
33
  }
25
34
  }
26
35
  export function retryAfterSeconds(response, body) {
@@ -61,6 +70,7 @@ function problemReason(response, body) {
61
70
  const message = singleLine(problem.data.error.message);
62
71
  return message ? { code: code.data, message } : undefined;
63
72
  }
73
+ /** `fallback` is the CLI's own message, used when com gave no reason it accepts. */
64
74
  function publicApiError(fallback, response, body) {
65
75
  const reason = problemReason(response, body);
66
76
  return new VigilbaseApiError(reason?.message ?? fallback, {
@@ -69,6 +79,7 @@ function publicApiError(fallback, response, body) {
69
79
  retryAfterSec: retryAfterSeconds(response, body),
70
80
  code: reason?.code,
71
81
  reason: reason?.message,
82
+ isCliAuthored: !reason,
72
83
  });
73
84
  }
74
85
  /** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
@@ -131,7 +142,7 @@ export class VigilbaseClient {
131
142
  throw new VigilbaseApiError('Invalid tool execution response. Check the API URL or retry later.', { status: response.status });
132
143
  }
133
144
  if (!parsed.success) {
134
- throw publicApiError('Tool execution was unsuccessful.', response, body);
145
+ throw publicApiError(SCAN_FAILED_MESSAGE, response, body);
135
146
  }
136
147
  return parsed;
137
148
  }
@@ -3,6 +3,8 @@ export type Writer = {
3
3
  stdout: (line?: string) => void;
4
4
  stderr: (line?: string) => void;
5
5
  };
6
+ /** A scan that ended without a result: com's copy (apps/com/lib/tools/execute-internal-scan.ts). */
7
+ export declare const SCAN_FAILED_MESSAGE = "The scan couldn't be completed. Try again later.";
6
8
  /** Untrusted API data must never become terminal control sequences or bidi overrides. */
7
9
  export declare function sanitizeTerminal(value: string): string;
8
10
  /** One sanitised line, cut to `maxLength` characters with an ellipsis. */
@@ -16,5 +18,7 @@ export declare function formatToolsList(catalog: AgentToolsCatalogResponse, mode
16
18
  isAuthenticated?: boolean;
17
19
  }): void;
18
20
  export declare function formatToolDescription(tool: AgentToolDefinition, mode: CliOutputMode, writer: Writer, skillMarkdown?: string): void;
21
+ /** Whether a scan produced a result; the exit code follows it. */
22
+ export declare function isSuccessfulScan(result: AgentToolExecuteResponse): boolean;
19
23
  export declare function formatExecuteResult(result: AgentToolExecuteResponse, mode: CliOutputMode, writer: Writer): void;
20
24
  export declare function formatError(error: unknown, mode: CliOutputMode, writer: Writer): void;
@@ -1,4 +1,7 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
+ /** A scan that ended without a result: com's copy (apps/com/lib/tools/execute-internal-scan.ts). */
3
+ export const SCAN_FAILED_MESSAGE = "The scan couldn't be completed. Try again later.";
4
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
5
  /** Untrusted API data must never become terminal control sequences or bidi overrides. */
3
6
  export function sanitizeTerminal(value) {
4
7
  return stripVTControlCharacters(value).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, '');
@@ -245,27 +248,42 @@ export function formatToolDescription(tool, mode, writer, skillMarkdown) {
245
248
  writer.stdout(skillMarkdown.trimEnd());
246
249
  }
247
250
  }
251
+ /**
252
+ * Why a scan failed, or null when it has a result. A scan com could not grade comes back
253
+ * inside a successful response as `status: "error"` with a message and no grade.
254
+ */
255
+ function scanFailure(result) {
256
+ if (result.success && result.result?.status !== 'error')
257
+ return null;
258
+ const message = result.result?.status === 'error'
259
+ ? result.result.message
260
+ : result.success ? undefined : typeof result.error === 'string' ? result.error : result.error?.message;
261
+ // Vigilbase's scanner wrote it, but it is bounded like any server reason.
262
+ return (typeof message === 'string' && singleLine(message)) || SCAN_FAILED_MESSAGE;
263
+ }
264
+ /** Whether a scan produced a result; the exit code follows it. */
265
+ export function isSuccessfulScan(result) {
266
+ return scanFailure(result) === null;
267
+ }
248
268
  export function formatExecuteResult(result, mode, writer) {
269
+ const failure = scanFailure(result);
249
270
  if (mode === 'json') {
250
- printJson(writer, result);
271
+ // A scan without a result fails in JSON as in the exit code.
272
+ // A failed scan has no grade, whatever placeholder the scanner used.
273
+ printJson(writer, failure === null ? result : { ...result, success: false, error: failure, grade: null, gradeScore: null });
251
274
  return;
252
275
  }
253
276
  const tool = result.toolName ?? 'tool';
254
277
  const target = result.target ?? '';
255
- const grade = result.grade != null
278
+ const grade = failure === null && result.grade != null
256
279
  ? `${result.grade}${result.gradeScore != null ? ` ${result.gradeScore}` : ''}`
257
280
  : 'n/a';
258
- const head = result.success
259
- ? `✓ ${tool} · ${target} · ${grade}`
260
- : `✗ ${tool} · ${target} · ${grade}`;
261
- if (result.success)
262
- writer.stdout(head);
263
- else
264
- writer.stderr(head);
265
- const error = typeof result.error === 'string' ? result.error : result.error?.message;
266
- if (error) {
267
- writer.stderr(` ${error}`);
281
+ if (failure !== null) {
282
+ writer.stderr(`✗ ${tool} · ${target} · ${grade}`);
283
+ writer.stderr(` ${failure}`);
284
+ return;
268
285
  }
286
+ writer.stdout(`✓ ${tool} · ${target} · ${grade}`);
269
287
  for (const line of summarizeResultCompact(result.result)) {
270
288
  writer.stdout(` ${line}`);
271
289
  }
@@ -273,7 +291,8 @@ export function formatExecuteResult(result, mode, writer) {
273
291
  export function formatError(error, mode, writer) {
274
292
  let message = error instanceof Error ? error.message : String(error);
275
293
  let reason = '';
276
- // Server bodies can contain credentials, customer data, or arbitrary error text.
294
+ // Server bodies can contain credentials, customer data, or arbitrary error text, so an error that
295
+ // carries one shows only text the CLI wrote, or the reason the public client accepted.
277
296
  if (error && typeof error === 'object' && 'body' in error && error.body != null && 'status' in error && typeof error.status === 'number') {
278
297
  if ('code' in error && error.code === 'FLAREHAWK_VIEW_AS_REQUIRED') {
279
298
  message = 'Select a customer with --customer-id <customer UUID> for security commands.';
@@ -281,7 +300,8 @@ export function formatError(error, mode, writer) {
281
300
  else {
282
301
  // Only the reason the public client accepted from a known problem+json error is shown.
283
302
  reason = 'reason' in error && typeof error.reason === 'string' ? singleLine(error.reason) : '';
284
- message = reason || `Request failed (HTTP ${error.status}).`;
303
+ const isCliAuthored = 'isCliAuthored' in error && error.isCliAuthored === true;
304
+ message = reason || (isCliAuthored ? message : `Request failed (HTTP ${error.status}).`);
285
305
  }
286
306
  }
287
307
  const payload = {
@@ -310,6 +330,10 @@ export function formatError(error, mode, writer) {
310
330
  if (hint)
311
331
  payload.hint = hint;
312
332
  }
333
+ // The One tool run a failed scan recorded, for `vigilbase runs get`.
334
+ if ('toolRunId' in error && typeof error.toolRunId === 'string' && UUID.test(error.toolRunId)) {
335
+ payload.toolRunId = error.toolRunId;
336
+ }
313
337
  }
314
338
  if (mode === 'json') {
315
339
  printJson(writer, payload);
@@ -7,6 +7,8 @@ import type { CliConfig } from './config.js';
7
7
  export type OneToolExecuteResponse = {
8
8
  success: boolean;
9
9
  error?: string;
10
+ /** `scan_unassessed`: the scanner ran but could not assess the target. */
11
+ code?: string;
10
12
  data?: {
11
13
  toolRunId?: string;
12
14
  status?: string;
@@ -7,6 +7,7 @@ import { validateOneOrigin } from './config.js';
7
7
  import { validateStripePortalUrl } from './open-url.js';
8
8
  import { parseJsonResponse, requestText } from './network.js';
9
9
  import { oneErrorHint } from './one-error-hints.js';
10
+ import { SCAN_FAILED_MESSAGE } from './format.js';
10
11
  import { retryAfterSeconds, VigilbaseApiError } from './client.js';
11
12
  /**
12
13
  * Tools that require Vigilbase One authentication (auth-required scanners).
@@ -133,9 +134,12 @@ export class VigilbaseOneClient {
133
134
  const hint = response.ok ? undefined : oneErrorHint({
134
135
  method: init?.method ?? 'GET', path, status: response.status, code: errorDetails.code, hasCustomerScope: options?.hasCustomerScope,
135
136
  });
137
+ // Every message below is the CLI's own; One's error text stays in the body, which is never printed.
136
138
  if (response.status === 401 || response.status === 403) {
137
- throw new VigilbaseApiError(message ||
138
- 'Authentication required. Run `vigilbase login`.', { status: response.status, body: parsed, code: errorDetails.code, hint });
139
+ throw new VigilbaseApiError(message || (response.status === 401
140
+ ? 'Authentication required. Run `vigilbase login`.'
141
+ // Signed in, but One refuses this user or this request: logging in again won't help.
142
+ : "You don't have access to this."), { status: response.status, body: parsed, code: errorDetails.code, hint, isCliAuthored: true });
139
143
  }
140
144
  if (response.status === 429) {
141
145
  throw new VigilbaseApiError(message || 'Rate limit exceeded.', {
@@ -143,18 +147,21 @@ export class VigilbaseOneClient {
143
147
  body: parsed,
144
148
  retryAfterSec: retryAfterSeconds(response, body),
145
149
  code: errorDetails.code,
150
+ isCliAuthored: true,
146
151
  });
147
152
  }
148
153
  const allowed = options?.allowStatuses ?? [];
149
154
  if (!response.ok && !allowed.includes(response.status)) {
150
155
  throw new VigilbaseApiError(message ||
151
- `Request failed (HTTP ${response.status})`, { status: response.status, body: parsed, code: errorDetails.code, hint });
156
+ `Request failed (HTTP ${response.status}).`, { status: response.status, body: parsed, code: errorDetails.code, hint, isCliAuthored: true });
152
157
  }
153
158
  if (!z.record(z.string(), z.unknown()).safeParse(body).success) {
154
159
  throw new VigilbaseApiError('Invalid One API response. Expected a JSON object.', { status: response.status });
155
160
  }
156
161
  if (parsed.success === false || ('ok' in parsed && parsed.ok === false)) {
157
- throw new VigilbaseApiError('One API reported an unsuccessful operation.', { status: response.status, body: parsed, code: errorDetails.code });
162
+ throw new VigilbaseApiError('One API reported an unsuccessful operation.', {
163
+ status: response.status, body: parsed, code: errorDetails.code, isCliAuthored: true,
164
+ });
158
165
  }
159
166
  return parsed;
160
167
  }
@@ -188,7 +195,7 @@ export class VigilbaseOneClient {
188
195
  }
189
196
  const { response, body } = await this.request('/api/auth/get-session');
190
197
  if (!response.ok) {
191
- throw new VigilbaseApiError(`Failed to load One session (HTTP ${response.status})`, { status: response.status, body });
198
+ throw new VigilbaseApiError(`Failed to load One session (HTTP ${response.status})`, { status: response.status, body, isCliAuthored: true });
192
199
  }
193
200
  const schema = z.object({
194
201
  session: z.record(z.string(), z.unknown()).nullable().optional(),
@@ -283,10 +290,70 @@ export class VigilbaseOneClient {
283
290
  return this.requestJson(`/api/tool-runs/${encodeURIComponent(id)}`);
284
291
  }
285
292
  async executeTool(toolName, target) {
286
- return this.requestJson(`/api/tools/${encodeURIComponent(toolName)}/execute`, {
287
- method: 'POST',
288
- headers: { 'Content-Type': 'application/json' },
289
- body: JSON.stringify({ target }),
290
- }, { allowStatuses: [422] });
293
+ try {
294
+ return await this.requestJson(`/api/tools/${encodeURIComponent(toolName)}/execute`, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ target }),
298
+ }, { allowStatuses: [422] });
299
+ }
300
+ catch (error) {
301
+ const unassessed = unassessedScan(error);
302
+ if (unassessed)
303
+ return unassessed;
304
+ throw failedScanError(error) ?? error;
305
+ }
291
306
  }
292
307
  }
308
+ /** One's answer when the scan itself failed, for example when com ran out of memory. */
309
+ const failedScanSchema = z.object({
310
+ success: z.literal(false),
311
+ data: z.object({ status: z.literal('failed'), toolRunId: z.unknown().optional() }).passthrough(),
312
+ }).passthrough();
313
+ const unassessedScanSchema = z.object({
314
+ code: z.literal('scan_unassessed'),
315
+ data: z.object({
316
+ toolRunId: z.unknown().optional(),
317
+ result: z.object({ status: z.literal('error'), message: z.string() }).passthrough(),
318
+ }),
319
+ });
320
+ /**
321
+ * A scan the scanner ran but could not assess (a bot verification challenge, an
322
+ * error page). The result carries the scanner's message, which the formatter
323
+ * prints bounded like any scanner result; One's error text stays unused.
324
+ */
325
+ function unassessedScan(error) {
326
+ if (!(error instanceof VigilbaseApiError) || error.status !== 422)
327
+ return undefined;
328
+ const unassessed = unassessedScanSchema.safeParse(error.body);
329
+ if (!unassessed.success)
330
+ return undefined;
331
+ const toolRunId = z.string().uuid().safeParse(unassessed.data.data.toolRunId);
332
+ return {
333
+ success: false,
334
+ code: 'scan_unassessed',
335
+ data: {
336
+ toolRunId: toolRunId.success ? toolRunId.data : undefined,
337
+ status: 'failed',
338
+ grade: null,
339
+ gradeScore: null,
340
+ result: unassessed.data.data.result,
341
+ },
342
+ };
343
+ }
344
+ /** com's copy for a 422 failed scan, with the run One recorded; One's error text stays hidden. */
345
+ function failedScanError(error) {
346
+ if (!(error instanceof VigilbaseApiError) || error.status !== 422)
347
+ return undefined;
348
+ const failed = failedScanSchema.safeParse(error.body);
349
+ if (!failed.success)
350
+ return undefined;
351
+ const toolRunId = z.string().uuid().safeParse(failed.data.data.toolRunId);
352
+ return new VigilbaseApiError(SCAN_FAILED_MESSAGE, {
353
+ status: 422,
354
+ body: error.body,
355
+ code: error.code,
356
+ isCliAuthored: true,
357
+ toolRunId: toolRunId.success ? toolRunId.data : undefined,
358
+ });
359
+ }
package/dist/types.d.ts CHANGED
@@ -53,6 +53,10 @@ export type AgentToolExecuteResponse = {
53
53
  error?: AgentApiError | string | null;
54
54
  uiUrl?: string;
55
55
  skillUrl?: string;
56
+ /** A One scan the scanner could not assess: One's status and code, and the run it recorded. */
57
+ status?: number;
58
+ code?: string;
59
+ toolRunId?: string;
56
60
  retryAfterSec?: number;
57
61
  toolsUrl?: string;
58
62
  skillsIndex?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vigilbase/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.",
5
5
  "keywords": [
6
6
  "vigilbase",