@nuxtseo/cli 0.5.10 → 0.5.11

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
@@ -195,6 +195,8 @@ nuxtseo annotations create
195
195
  nuxtseo annotations update <annotation-id>
196
196
  nuxtseo annotations delete <annotation-id>
197
197
  nuxtseo analytics <view>
198
+ nuxtseo bot-traffic --crawler googlebot
199
+ nuxtseo bot-traffic --crawler bingbot
198
200
  nuxtseo search status
199
201
  nuxtseo search analytics <view>
200
202
  nuxtseo search indexing <summary|urls>
@@ -343,9 +345,9 @@ The CLI never falls back to MCP, a private route, or cached feature data.
343
345
 
344
346
  ### Claim response
345
347
 
346
- `actions resolve` saves your Claim. It returns before a requested scan finishes starting.
348
+ `actions resolve` saves your Claim. It returns before verification completes.
347
349
  `status: "open"` stays unchanged until the engine verifies the Action.
348
- Human output shows `Claimed, verifying` and the suggested check-back time.
350
+ Human output shows `Claimed, verifying`.
349
351
  Claimed Actions leave `actions list`. Use `actions show <action-id>` to check them.
350
352
 
351
353
  Both `resolve` and `show` return these additive fields:
@@ -353,8 +355,12 @@ Both `resolve` and `show` return these additive fields:
353
355
  | Field | Meaning |
354
356
  | --- | --- |
355
357
  | `markedAt` | ISO timestamp when your Claim was saved, or `null` before a Claim. |
356
- | `checkBackAt` | Suggested ISO timestamp to check again, or `null` when no automated check applies. |
357
-
358
- A check-back time does not promise a completed check.
359
- `scan-queued` can mean a re-assessment using scheduled crawl results, without a new scan.
358
+ | `checkBackAt` | Supported ISO timestamp to check again, or `null` when the schedule is unknown. |
359
+
360
+ `recheck-queued` requests a fresh check of the cited URLs.
361
+ `scan-queued` means a source refresh is queued or already running.
362
+ `next-assessment` waits for a site check, which can reuse stored results.
363
+ An assessment can already be queued without requesting fresh Crawl collection.
364
+ Unknown check times stay null, including retries and later reads.
365
+ A supported check-back time does not promise completed verification.
360
366
  A self-resolved Claim returns `status: "verified"` and `verification: "user-resolved"`.
package/dist/commands.js CHANGED
@@ -9,7 +9,7 @@ import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
9
9
  import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
10
10
  import { parseAbsolutePageUrl, parseChoice, parseInteger, parsePageUrlOrPath, parseRatio } from './parse.js';
11
11
  import { PULL_PERIODS, runPull } from './pull.js';
12
- import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderIndexingWatches, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemapEvidence, renderSitemaps, renderSitemapSubmission, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUrlInspectionRefresh, renderUsage, } from './render.js';
12
+ import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderBotTraffic, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderIndexingWatches, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemapEvidence, renderSitemaps, renderSitemapSubmission, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUrlInspectionRefresh, renderUsage, } from './render.js';
13
13
  import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
14
14
  import { resolveSite } from './site.js';
15
15
  import { installSkill, SKILL_AGENTS } from './skill.js';
@@ -1360,6 +1360,22 @@ async function backlinksHistory(runtime, globals, args) {
1360
1360
  }, { signal: runtime.requestSignal }));
1361
1361
  return present(runtime, globals, response, value => renderBacklinksHistory(value.data));
1362
1362
  }
1363
+ async function googlebotActivity(runtime, globals, args) {
1364
+ const days = parseInteger(args.days, { name: '--days', minimum: 1, maximum: 90, defaultValue: 28 });
1365
+ if (days._tag === 'Err')
1366
+ return days;
1367
+ const crawler = parseChoice(args.crawler, { name: '--crawler', choices: ['googlebot', 'bingbot'], defaultValue: 'googlebot' });
1368
+ if (crawler._tag === 'Err')
1369
+ return crawler;
1370
+ const resolved = await apiAndSite(runtime, globals);
1371
+ if (resolved._tag === 'Err')
1372
+ return resolved;
1373
+ const response = await withSpinner(runtime, 'Loading Bot Traffic', () => resolved.value.api.client.analytics.botActivity({
1374
+ params: { siteId: resolved.value.siteId },
1375
+ query: { days: days.value, crawler: crawler.value },
1376
+ }, { signal: runtime.requestSignal }));
1377
+ return present(runtime, globals, response, value => renderBotTraffic(value.data));
1378
+ }
1363
1379
  async function siteStatus(runtime, globals) {
1364
1380
  const resolved = await apiAndSite(runtime, globals);
1365
1381
  if (resolved._tag === 'Err')
@@ -2319,7 +2335,7 @@ export function createRootCommand(runtime, globals, execution) {
2319
2335
  'yes': { type: 'boolean', alias: 'y', description: 'Confirm a mutation without prompting' },
2320
2336
  },
2321
2337
  subCommands: {
2322
- login: defineCommand({
2338
+ 'login': defineCommand({
2323
2339
  meta: { name: 'login', description: 'Approve this machine in a browser and store the Team API token it mints' },
2324
2340
  args: {
2325
2341
  'with-token': { type: 'boolean', description: 'Paste an existing Team API token instead of pairing' },
@@ -2332,20 +2348,20 @@ export function createRootCommand(runtime, globals, execution) {
2332
2348
  noBrowser: args.browser === false,
2333
2349
  }), args._, 0)(),
2334
2350
  }),
2335
- logout: defineCommand({
2351
+ 'logout': defineCommand({
2336
2352
  meta: { name: 'logout', description: 'Remove the local credential' },
2337
2353
  run: ({ args }) => capture(execution, () => logout(runtime, globals), args._, 0)(),
2338
2354
  }),
2339
- whoami: defineCommand({
2355
+ 'whoami': defineCommand({
2340
2356
  meta: { name: 'whoami', description: 'Validate and describe the active credential' },
2341
2357
  run: ({ args }) => capture(execution, () => whoami(runtime, globals), args._, 0)(),
2342
2358
  }),
2343
- config: defineCommand({
2359
+ 'config': defineCommand({
2344
2360
  meta: { name: 'config', description: 'Show or edit local configuration' },
2345
2361
  run: ({ args }) => capture(execution, () => config(runtime, globals), args._, 0)(),
2346
2362
  }),
2347
2363
  sites,
2348
- feedback: defineCommand({
2364
+ 'feedback': defineCommand({
2349
2365
  meta: { name: 'feedback', description: 'Report CLI problems to Nuxt SEO' },
2350
2366
  subCommands: {
2351
2367
  submit: defineCommand({
@@ -2367,7 +2383,7 @@ export function createRootCommand(runtime, globals, execution) {
2367
2383
  }),
2368
2384
  },
2369
2385
  }),
2370
- usage: defineCommand({
2386
+ 'usage': defineCommand({
2371
2387
  meta: { name: 'usage', description: 'Read Team usage' },
2372
2388
  args: {
2373
2389
  group: {
@@ -2383,22 +2399,27 @@ export function createRootCommand(runtime, globals, execution) {
2383
2399
  backlinks,
2384
2400
  mentions,
2385
2401
  page,
2386
- performance: defineCommand({
2402
+ 'performance': defineCommand({
2387
2403
  meta: { name: 'performance', description: 'Read the stored Site performance overview' },
2388
2404
  run: ({ args }) => capture(execution, () => performance(runtime, globals), args._, 0)(),
2389
2405
  }),
2390
2406
  annotations,
2407
+ 'bot-traffic': defineCommand({
2408
+ meta: { name: 'bot-traffic', description: 'Read verified search engine requests from the Site\'s Cloudflare zone' },
2409
+ args: { days: { type: 'string', description: 'UTC days to read, 1 to 90, today included' }, crawler: { type: 'string', description: 'googlebot or bingbot', default: 'googlebot' } },
2410
+ run: ({ args }) => capture(execution, () => googlebotActivity(runtime, globals, { days: args.days, crawler: args.crawler }), args._, 0)(),
2411
+ }),
2391
2412
  pull,
2392
2413
  research,
2393
2414
  scans,
2394
2415
  search,
2395
2416
  sitemaps,
2396
- status: defineCommand({
2417
+ 'status': defineCommand({
2397
2418
  meta: { name: 'status', description: 'Read the Site assessment and the one thing to do next' },
2398
2419
  run: ({ args }) => capture(execution, () => siteStatus(runtime, globals), args._, 0)(),
2399
2420
  }),
2400
2421
  vitals,
2401
- analytics: defineCommand({
2422
+ 'analytics': defineCommand({
2402
2423
  meta: { name: 'analytics', description: 'Read Web Analytics for the selected Site' },
2403
2424
  args: {
2404
2425
  'view': positional('view', ANALYTICS_VIEWS.join(', ')),
package/dist/render.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { AccountUsageResponse, SitesListResponse, SiteStatus } from '@nuxtseo/protocol/v1';
2
2
  import type { AccountTokenData } from '@nuxtseo/protocol/v1/account';
3
3
  import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxtseo/protocol/v1/actions';
4
- import type { AnalyticsQueryData, AnalyticsView } from '@nuxtseo/protocol/v1/analytics';
4
+ import type { AnalyticsQueryData, AnalyticsView, BotActivity } from '@nuxtseo/protocol/v1/analytics';
5
5
  import type { AuditChanges, AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities, AuditLinkStructure } from '@nuxtseo/protocol/v1/audit';
6
6
  import type { BacklinkAnchorsData, BacklinksHistory, BacklinksSummaryData, MentionsData, RecoverableBacklinksData, ReferringDomainsData } from '@nuxtseo/protocol/v1/backlinks';
7
7
  import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, IndexingWatchesData, SearchAnalyticsData, SearchStatusData, UrlInspectionData, UrlInspectionRefreshData } from '@nuxtseo/protocol/v1/gsc';
@@ -31,10 +31,7 @@ export declare function renderIndexingWatches(data: IndexingWatchesData): string
31
31
  export declare function renderSitemaps(data: SiteSitemapsResponse['data']): string;
32
32
  export declare function renderSitemapUrls(data: SiteSitemapUrlsResponse['data']): string;
33
33
  export declare function renderSitemapAction(data: SiteSitemapActionResponse['data']): string;
34
- /**
35
- * Web Analytics rows differ per view and share no discriminant field, so the
36
- * requested view selects the projection instead of the payload.
37
- */
34
+ export declare function renderBotTraffic(data: BotActivity): string;
38
35
  export declare function renderAnalytics(view: AnalyticsView, data: AnalyticsQueryData): string;
39
36
  export declare function renderAccountToken(data: AccountTokenData): string;
40
37
  export declare function renderIndexingDiagnostics(data: IndexingDiagnosticsData): string;
package/dist/render.js CHANGED
@@ -433,6 +433,48 @@ export function renderSitemapAction(data) {
433
433
  * Web Analytics rows differ per view and share no discriminant field, so the
434
434
  * requested view selects the projection instead of the payload.
435
435
  */
436
+ function percent(share) {
437
+ return share === null ? 'none' : `${(share * 100).toFixed(1)}%`;
438
+ }
439
+ function googlebotAccessLines(access) {
440
+ if (access._tag === 'zone_access_missing') {
441
+ return [
442
+ `Collection stopped: ${access.message}`,
443
+ `Grant: ${access.permissions.join(', ')} (OAuth: ${access.oauthScopes.join(', ')})`,
444
+ ];
445
+ }
446
+ if (access._tag === 'collection_failed')
447
+ return [`Collection stopped (${access.reason}): ${access.message}`];
448
+ return [];
449
+ }
450
+ export function renderBotTraffic(data) {
451
+ const access = googlebotAccessLines(data.access);
452
+ const crawler = data.crawler === 'googlebot' ? 'Googlebot' : 'Bingbot';
453
+ if (data.collection === null)
454
+ return [...access, `No ${crawler} requests collected yet. Collection runs with the Site's Cloudflare sync.`].join('\n');
455
+ const { totals } = data;
456
+ const statusLine = (statuses) => `2xx ${statuses['2xx']} 304 ${statuses['304']} 3xx ${statuses.other3xx} 404 ${statuses['404']} 410 ${statuses['410']} 4xx ${statuses.other4xx} 429 ${statuses['429']} 5xx ${statuses['5xx']}`;
457
+ const first = data.days[0]?.day;
458
+ const last = data.days.at(-1)?.day;
459
+ return [
460
+ ...access,
461
+ `Bot Traffic, ${crawler}${first && last ? `, ${first} to ${last}` : ''}`,
462
+ fields([
463
+ ['Collected', `${data.collection.fromDay} to ${data.collection.toDay}`],
464
+ ['Complete through', data.collection.completeThroughDay ?? ''],
465
+ ['Requests', totals.requests],
466
+ ['Statuses', statusLine(totals.statuses)],
467
+ ['Content', `html ${totals.contents.html} js ${totals.contents.js} css ${totals.contents.css} image ${totals.contents.image} other ${totals.contents.other}`],
468
+ ['Wasted share', percent(totals.wastedShare)],
469
+ ['Cache hit share', percent(totals.cacheHitShare)],
470
+ ['Origin avg', totals.originAvgMs === null ? '' : `${totals.originAvgMs} ms`],
471
+ ]),
472
+ data.days.length === 0 ? 'No collected days in this window.' : 'Days',
473
+ ...data.days.map(day => ` ${day.day}${day.complete ? '' : ' (partial)'} ${day.requests} requests ${statusLine(day.statuses)}`),
474
+ data.errorPaths.length === 0 ? '' : 'Error paths',
475
+ ...data.errorPaths.map(path => ` ${path.status} ${path.requests} ${path.path} last ${path.lastDay}`),
476
+ ].filter(Boolean).join('\n');
477
+ }
436
478
  export function renderAnalytics(view, data) {
437
479
  const delta = (label, value) => [
438
480
  label,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.5.10",
4
+ "version": "0.5.11",
5
5
  "description": "Command line interface for the Nuxt SEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.8.0",
36
- "@nuxtseo/protocol": "^0.5.10",
37
- "@nuxtseo/sdk": "^0.5.10",
36
+ "@nuxtseo/protocol": "^0.5.11",
37
+ "@nuxtseo/sdk": "^0.5.11",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -254,6 +254,7 @@ you got:
254
254
  | `page issues` | `availableKeys` |
255
255
  | `search cohorts` | `analysed: false`: no completed crawl, no Google index state, or `reason: "sampled-index-state"` |
256
256
  | `vitals summary` | `available: false` |
257
+ | `bot-traffic` | `collection: null`; `access._tag` other than `collecting` |
257
258
  | `research keywords`, `research rankings`, `research domain-*`, `page issues`, `search cohorts`, `vitals findings` | `data.message`, and `data.tip` for the repair |
258
259
 
259
260
  Read `data.message` before the list. For example, `research keywords` refuses
@@ -291,6 +292,9 @@ Do not compare this population with a summary or cohort from another capture.
291
292
  is normal.
292
293
  - `status` is the verdict, `performance` the lab score, `vitals` what real
293
294
  users measured. Never substitute one for another.
295
+ - `bot-traffic --crawler googlebot` or `--crawler bingbot` counts verified search engine requests at the Site's Cloudflare edge. It is
296
+ not Search Console's Crawl Stats report, so its totals can differ. A high
297
+ `wastedShare` during a planned URL retirement is expected.
294
298
  - `page inspect` reads the Nuxt SEO observation store. `search inspect` reads
295
299
  the latest retained Google URL Inspection for the same URL.
296
300
  `search inspect <url> --fresh --yes` requests Google URL Inspection through
@@ -62,6 +62,7 @@ a research unit.
62
62
  | Which route family is Google declining | `search cohorts` |
63
63
  | Why is this one URL not indexed | `search inspect <url>` |
64
64
  | When did this URL change index state | `search index-history`, with `--url` |
65
+ | Which URLs answer search engines with errors | `bot-traffic` with the Googlebot or Bingbot crawler filter |
65
66
  | Which pages earn the clicks | `search analytics pages` |
66
67
  | Which terms is the Site visible for | `search analytics keywords` |
67
68
  | Which pages are losing clicks | `audit content-decay` |
@@ -142,6 +143,12 @@ lines.
142
143
  `failureKind` says why a Scan failed, for example `timeout`, `rejected`, or `quota`.
143
144
  It is null on a Scan that did not fail, and on a Scan that failed before kinds were recorded.
144
145
 
146
+ Reuse saved successful Scan details. Read only the details needed for the missing proof.
147
+ For persistent Poor metrics, start with the latest two completed Scans per canonical Page and strategy.
148
+ Read older details only when the comparison needs them. Missing details mean incomplete coverage, never healthy performance.
149
+ If the server reports a retry delay, respect it across the work queue.
150
+ If throttling repeats, stop parallel reads and preserve the failures for a later paced retry.
151
+
145
152
  `comparison` has one of three tags.
146
153
  `compared` lists each metric both Scans measured, with `previous`, `current`, `delta`, and `severity`.
147
154
  `incompatible` names why the two Scans differ, for example `lighthouse-version`. Do not compare their numbers.
@@ -263,6 +270,7 @@ A missing query row is never evidence of no demand.
263
270
 
264
271
  | Command | What it returns, and its flags |
265
272
  | --- | --- |
273
+ | `bot-traffic` | Bot Traffic counts verified Googlebot or Bingbot requests per UTC day at the Site's Cloudflare zone. `--crawler googlebot\|bingbot` defaults to Googlebot. Statuses split into `2xx`, `304`, `other3xx`, `404`, `410`, `other4xx`, `429`, `5xx`; content into `html`, `js`, `css`, `image`, `other`. `totals.wastedShare` counts every answer except 2xx and 304. `errorPaths` lists the most requested 4xx, 429 and 5xx paths, without 410. `--days 1..90` (default 28), today included. `days` lists only collected days, and a `complete: false` day is partial. `collection: null` means no collection ran yet. Read `access` first: `zone_access_missing` lists the Cloudflare permissions (`Zone: Read`, `Analytics: Read`) and OAuth scopes to grant. `collection_failed` names the reason. Stored days stop at the last collection. A Site without a Cloudflare zone returns `conflict`. These edge counts can differ from Search Console's Crawl Stats report. Source: `packages/protocol/src/v1/analytics.ts`, `packages/cli/src/commands.ts` |
266
274
  | `analytics <view>` | Web Analytics from the connected provider. Views: `performance`, `top-pages`, `source-medium`, `key-events`, `countries`, `devices`, `dimension`. `--period` is a free string, for example `28d` (default `28d`); an empty value exits `2`. `--compare previous\|year\|none` (default `previous`), `--dimension` (required for the `dimension` view; one of `date`, `page`, `hostname`, `source`, `medium`, `campaign`, `country`, `region`, `city`, `deviceCategory`, `browser`, `operatingSystem`), `--filters` as a JSON array, `--phase current\|prior\|full` (default `full`), `--no-stable-data` to include unstable recent days, `--no-host-scope` to include every host, `--no-compare-prior` to skip the prior window, `--fresh` to bypass the server cache |
267
275
 
268
276
  ## Research, backlinks, and mentions
@@ -332,9 +340,9 @@ already answer.
332
340
 
333
341
  ### Claim response
334
342
 
335
- `actions resolve` saves your Claim. It returns before a requested scan finishes starting.
343
+ `actions resolve` saves your Claim. It returns before verification completes.
336
344
  `status: "open"` stays unchanged until the engine verifies the Action.
337
- Human output shows `Claimed, verifying` and the suggested check-back time.
345
+ Human output shows `Claimed, verifying`.
338
346
  Claimed Actions leave `actions list`. Use `actions show <action-id>` to check them.
339
347
 
340
348
  Both `resolve` and `show` return these additive fields:
@@ -342,8 +350,12 @@ Both `resolve` and `show` return these additive fields:
342
350
  | Field | Meaning |
343
351
  | --- | --- |
344
352
  | `markedAt` | ISO timestamp when your Claim was saved, or `null` before a Claim. |
345
- | `checkBackAt` | Suggested ISO timestamp to check again, or `null` when no automated check applies. |
346
-
347
- A check-back time does not promise a completed check.
348
- `scan-queued` can mean a re-assessment using scheduled crawl results, without a new scan.
353
+ | `checkBackAt` | Supported ISO timestamp to check again, or `null` when the schedule is unknown. |
354
+
355
+ `recheck-queued` requests a fresh check of the cited URLs.
356
+ `scan-queued` means a source refresh is queued or already running.
357
+ `next-assessment` waits for a site check, which can reuse stored results.
358
+ An assessment can already be queued without requesting fresh Crawl collection.
359
+ Unknown check times stay null, including retries and later reads.
360
+ A supported check-back time does not promise completed verification.
349
361
  A self-resolved Claim returns `status: "verified"` and `verification: "user-resolved"`.
@@ -18,6 +18,22 @@ Protocol failure, still on stdout, with recovery details on stderr:
18
18
 
19
19
  Read `data`. The CLI does not unwrap, rename, rank, or enrich fields.
20
20
 
21
+ ## Provisional Site verdicts
22
+
23
+ If `status` returns degraded data quality, read `dataQuality.reason` with `unavailableProofs`.
24
+ The reason records the unavailable stage. It does not identify the underlying database or network fault.
25
+
26
+ | Reason | Recorded outcome |
27
+ | --- | --- |
28
+ | `completed-crawl-unavailable` | The loader found no completed Crawl evidence with a completion clock. |
29
+ | `crawl-read-failed` | The collection read failed. |
30
+ | `crawl-snapshot-unavailable` | Generation could not reread Findings for the pinned Crawl. |
31
+ | `crawl-snapshot-read-failed` | The pinned Findings read failed. |
32
+
33
+ A null reason can mean an older assessment or another missing proof.
34
+ It never clears the missing proof or proves zero blockers.
35
+ A completed Crawl elsewhere does not identify which stage failed for this assessment.
36
+
21
37
  ## A soft failure looks like an empty result
22
38
 
23
39
  Some commands refuse the argument and still exit `0` with an empty list. The
@@ -138,6 +154,7 @@ command returns; never guess a chain. Verified against the response schemas.
138
154
  | `sitemaps submission` | no list; `data.state` | none |
139
155
  | `sitemaps inspect` | no list; `data.capture`, `data.state` | none |
140
156
  | `search watches` | `data.watched`; each item's `checkpoints` | none |
157
+ | `bot-traffic` | `data.days` and `data.errorPaths` | none |
141
158
  | `sitemaps urls` | `data.items` | `data.page.nextCursor`, `data.page.limit` |
142
159
  | `audit content-decay` | `data.rows` | `data.page`; `data.total`, `data.truncated` |
143
160
  | `audit link-opportunities` | `data.rows` | `data.page`; `data.total` |
@@ -182,5 +199,10 @@ may also include the retry delay, the rate policy, the requests remaining, the
182
199
  rate limit, the window, the reset delay, and structured details. Keep the
183
200
  request ID when reporting a problem.
184
201
 
202
+ If its retry wait reaches the request deadline, the CLI preserves the last verified API failure.
203
+ That preserves the server code, request ID, and retry metadata.
204
+ Respect the reported retry delay before starting more calls.
205
+ Network-only timeouts still return `request_timeout`. Explicit cancellation returns exit `130`.
206
+
185
207
  Only exit `6` means the same command may work later. The SDK already retried
186
208
  HTTP 429 responses. Mutations are never retried automatically.