@nuxtseo/cli 0.1.3 → 0.2.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.
@@ -0,0 +1,22 @@
1
+ import type { CliResult } from './failures.js';
2
+ export declare const SKILL_NAME = "nuxtseo-cli";
3
+ export declare const SKILL_AGENTS: readonly ['claude', 'codex'];
4
+ export type SkillAgent = typeof SKILL_AGENTS[number];
5
+ export interface SkillInstallation {
6
+ agent: SkillAgent;
7
+ source: string;
8
+ destination: string;
9
+ }
10
+ /**
11
+ * The skill shipped inside this package. A global install puts it outside the
12
+ * project, so the documented `cp -R node_modules/...` path does not exist for
13
+ * the user who most needs it. Resolve it from this module instead.
14
+ */
15
+ export declare function skillSourceDirectory(moduleUrl?: string): string;
16
+ export declare function skillDestination(homeDirectory: string, agent: SkillAgent): string;
17
+ export declare function installSkill(options: {
18
+ agent: SkillAgent;
19
+ homeDirectory: string;
20
+ target?: string;
21
+ sourceDirectory?: string;
22
+ }): Promise<CliResult<SkillInstallation>>;
package/dist/skill.js ADDED
@@ -0,0 +1,56 @@
1
+ import { cp, mkdir, stat } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join, relative } from 'pathe';
4
+ import { EXIT_CODE, fail, ok } from './failures.js';
5
+ export const SKILL_NAME = 'nuxtseo-cli';
6
+ /**
7
+ * Directories inside the packaged skill that exist for this repository and not
8
+ * for the person installing it. `npm` keeps them out of the tarball through the
9
+ * `files` negation; this keeps them out of an installed skill directory too,
10
+ * because a negation cannot reach a recursive copy.
11
+ */
12
+ const SKILL_LOCAL_ONLY = new Set(['evals']);
13
+ export const SKILL_AGENTS = ['claude', 'codex'];
14
+ const AGENT_DIRECTORIES = {
15
+ claude: '.claude',
16
+ codex: '.codex',
17
+ };
18
+ /**
19
+ * The skill shipped inside this package. A global install puts it outside the
20
+ * project, so the documented `cp -R node_modules/...` path does not exist for
21
+ * the user who most needs it. Resolve it from this module instead.
22
+ */
23
+ export function skillSourceDirectory(moduleUrl = import.meta.url) {
24
+ return join(dirname(fileURLToPath(moduleUrl)), '..', 'skills', SKILL_NAME);
25
+ }
26
+ export function skillDestination(homeDirectory, agent) {
27
+ return join(homeDirectory, AGENT_DIRECTORIES[agent], 'skills', SKILL_NAME);
28
+ }
29
+ export async function installSkill(options) {
30
+ const source = options.sourceDirectory ?? skillSourceDirectory();
31
+ const readable = await stat(source).catch(() => {
32
+ // A missing directory and an unreadable one need the same repair, and the
33
+ // next line reports the path either way.
34
+ return null;
35
+ });
36
+ if (!readable?.isDirectory()) {
37
+ return fail(EXIT_CODE.infrastructure, `skill_source_missing: The packaged skill is missing at ${source}.\nReinstall @nuxtseo/cli, then run the command again.`);
38
+ }
39
+ const destination = options.target
40
+ ? join(options.target, SKILL_NAME)
41
+ : skillDestination(options.homeDirectory, options.agent);
42
+ const written = await mkdir(dirname(destination), { recursive: true })
43
+ .then(() => cp(source, destination, {
44
+ recursive: true,
45
+ force: true,
46
+ filter: (entry) => {
47
+ const [segment] = relative(source, entry).split('/');
48
+ return !segment || !SKILL_LOCAL_ONLY.has(segment);
49
+ },
50
+ }))
51
+ .then(() => ok(undefined))
52
+ .catch(cause => fail(EXIT_CODE.infrastructure, `skill_install_failed: Could not write the skill to ${destination}.\nCheck the directory permissions, then run the command again.`, cause));
53
+ if (written._tag === 'Err')
54
+ return written;
55
+ return ok({ agent: options.agent, source, destination });
56
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.2.0",
5
5
  "description": "Command line interface for the NuxtSEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -25,26 +25,27 @@
25
25
  },
26
26
  "files": [
27
27
  "dist",
28
- "skills"
28
+ "skills",
29
+ "!skills/**/evals"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=22"
32
33
  },
33
34
  "dependencies": {
34
35
  "@clack/prompts": "^1.7.0",
36
+ "@nuxtseo/sdk": "^0.2.0",
35
37
  "citty": "^0.2.2",
36
- "pathe": "^2.0.3",
37
- "@nuxtseo/sdk": "^0.1.3"
38
+ "pathe": "^2.0.3"
38
39
  },
39
40
  "optionalDependencies": {
40
- "@napi-rs/keyring": "^1.3.0"
41
+ "@napi-rs/keyring": "^2.0.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@arethetypeswrong/cli": "^0.18.5",
44
- "@types/node": "^26.2.0",
45
+ "@nuxtseo/protocol": "0.2.0",
46
+ "@types/node": "^26.4.0",
45
47
  "publint": "^0.3.24",
46
- "typescript": "npm:typescript-native-bridge@6.0.3-bridge.10.tsgo.7.0.2",
47
- "@nuxtseo/protocol": "0.1.3"
48
+ "typescript": "npm:typescript-native-bridge@6.0.3-bridge.15.tsgo.7.0.2"
48
49
  },
49
50
  "publishConfig": {
50
51
  "access": "public"
@@ -1,75 +1,77 @@
1
1
  ---
2
2
  name: nuxtseo-cli
3
- description: Drive the `nuxtseo` CLI for Site triage, Search Console rows, keyword and competitor research, SERP analysis, rankings, link opportunities, Content Briefs, content decay, duplicate clusters, scans, and issue resolution. Use whenever the user mentions NuxtSEO, the `nuxtseo` command, Site SEO work, or NuxtSEO automation.
3
+ description: Drives the `nuxtseo` CLI to triage a Site through the NuxtSEO public API: Site status and ranked issues, Search Console rows, route-family indexing, field and lab Core Web Vitals, Lighthouse Scans, Page Issues, keyword competitor and domain research, backlinks, SERP analysis, rankings, link opportunities, Content Briefs, content decay, duplicate clusters, chart annotations, and issue resolution. Use whenever the user mentions NuxtSEO, the `nuxtseo` command, Site SEO work, or NuxtSEO automation.
4
4
  ---
5
5
 
6
6
  # NuxtSEO CLI
7
7
 
8
8
  `nuxtseo` reads a Site through the NuxtSEO public API. Every command goes
9
9
  through `@nuxtseo/sdk`. The CLI never calls MCP or private routes. Live
10
- research reaches providers only through a declared public API operation.
10
+ research reaches a provider only through a declared public API operation.
11
11
 
12
- Use it to answer "what is wrong with this site and what did I break", then fix
13
- the code in the repo you are working in.
12
+ Use it to answer "what is wrong with this site, and what did I break". Then fix
13
+ the code in the repository you are working in.
14
14
 
15
- ## Data boundaries
15
+ ## Reference files
16
16
 
17
- Most commands read stored Site evidence. Three commands can start live research:
17
+ Read each one before you act on its subject:
18
18
 
19
- - `research keywords`
20
- - `research serp`
21
- - `research rankings`
22
-
23
- These commands use the Team research allowance when the server misses its
24
- cache. Keyword JSON reports cache use in `evidence`. SERP and ranking JSON
25
- report `cached`.
26
-
27
- `search status` reads stored connection state. It never waits for Google.
28
- `search analytics` reads Search Console rows through the public API.
29
- `search indexing` reads retained URL Inspection coverage through the public API.
19
+ - [references/commands.md](references/commands.md): every command, with its
20
+ flags, ranges, and defaults. Read it instead of guessing a flag.
21
+ - [references/protocol.md](references/protocol.md): response envelopes, paging,
22
+ and exit codes. Read it before you parse output or handle a failure.
23
+ - [references/indexing.md](references/indexing.md): how to answer "is this
24
+ indexed". Read it before you report an indexed count, or name the URLs
25
+ Google indexed.
30
26
 
31
27
  ## Get the binary
32
28
 
33
29
  ```sh
34
- npx -y @nuxtseo/cli@latest --version # no install
30
+ pnpm dlx @nuxtseo/cli@latest --version # no install
35
31
  pnpm add -g @nuxtseo/cli # or npm install --global
36
32
  ```
37
33
 
38
- Node 22 or newer is required. Inside the NuxtSEO monorepo itself, the package is
39
- not published, so run the built entry directly:
34
+ Node 22 or newer is required.
35
+
36
+ Inside the NuxtSEO monorepo the package is not published. Run the built entry
37
+ directly:
40
38
 
41
39
  ```sh
42
40
  node packages/cli/dist/cli-entry.js --version
43
41
  ```
44
42
 
45
- Exit `127` or `command not found` means the CLI is absent. That is not exit `3`;
46
- no token has been checked yet. Install it, then continue.
43
+ Exit `127` or `command not found` means the CLI is absent. That is not exit
44
+ `3`; no token has been checked yet. Install it, then continue.
47
45
 
48
46
  ## Before the first command
49
47
 
50
48
  ```sh
51
- nuxtseo whoami --json # validates the token, lists Site count
49
+ nuxtseo whoami --json # validates the token and reports its scopes
52
50
  ```
53
51
 
52
+ `whoami` returns the Team, the role, the granted scopes, and the token expiry.
53
+ Read `data.scopes` before a write command. A missing scope is exit `4`, and the
54
+ scope list tells you that before you spend the request.
55
+
54
56
  Exit `3` means there is no working token. Ask the user to run `nuxtseo login`
55
- themselves: it opens a browser, they approve the pairing, and the CLI stores the
56
- token. Do not run `login` for them; it needs a person at the browser, and in a
57
- non-interactive shell it falls back to reading a token from stdin instead.
57
+ themselves. It opens a browser, they approve the pairing, and the CLI stores
58
+ the token. Do not run `login` for them. It needs a person at the browser, and
59
+ in a non-interactive shell it falls back to reading a token from stdin.
58
60
 
59
61
  The alternative is a token created at
60
62
  <https://nuxtseo.com/pro/dashboard/settings/api-tokens> and exported as
61
63
  `NUXTSEO_TOKEN`. Either way the token is bound to a Team role, and that role
62
64
  gates what it can do.
63
65
 
64
- Never put a token in a command argument, a file you write, or a commit. `login`
65
- reads a token from a hidden prompt or stdin only.
66
+ Never put a token in a command argument, a file you write, or a commit.
67
+ `login` reads a token from a hidden prompt or stdin only.
66
68
 
67
69
  Resolve the Site once and reuse it. `nuxtseo sites list --json` prints every
68
70
  accessible Site with its ID.
69
71
 
70
72
  ## How to call it as an agent
71
73
 
72
- Always pass `--json` and always pass `--site <site-id>`.
74
+ Always pass `--json` and `--site <site-id>`.
73
75
 
74
76
  ```sh
75
77
  nuxtseo actions list --site site_123 --json
@@ -79,96 +81,223 @@ nuxtseo actions list --site site_123 --json
79
81
  also disables prompts. Diagnostics, warnings, spinners, and failure messages go
80
82
  to stderr, so stdout stays parseable.
81
83
 
82
- `--site` matters for a second reason: an explicit Site ID goes straight to the
83
- operation and skips the Sites read, so a token whose role cannot list Sites
84
- still works. If more than one Site is accessible and `--site` is absent, the CLI
84
+ `--site` matters for a second reason. An explicit Site ID goes straight to the
85
+ operation and skips the Sites read. A token whose role cannot list Sites still
86
+ works. If more than one Site is accessible and `--site` is absent, the CLI
85
87
  exits `5` rather than guessing.
86
88
 
87
89
  Other flags worth knowing:
88
90
 
89
91
  | Flag | Use it when |
90
92
  | --- | --- |
91
- | `--yes`, `-y` | The command mutates. Without it, a non-interactive run exits `2`. |
92
- | `--timeout-ms <ms>` | The default 30000 ms deadline is too short. Maximum is 300000. |
93
- | `--api-url <url>` | Testing against a non-production host. |
94
- | `--no-input` | Running in a TTY but no prompt is wanted. `--json` already implies this. |
93
+ | `--yes`, `-y` | The command mutates. Without it, a non-interactive run exits `2` |
94
+ | `--timeout-ms <ms>` | The default 30000 ms deadline is too short. Maximum is 300000 |
95
+ | `--api-url <url>` | Testing against a non-production host |
96
+ | `--no-input` | Running in a TTY but no prompt is wanted. `--json` already implies this |
95
97
 
96
98
  Unknown options exit `2` before any network work, so a typo costs nothing.
97
99
 
98
- Parse JSON. Never scrape human output. Before parsing envelopes, paging, or
99
- handling failures, read [CLI protocol](references/protocol.md).
100
-
101
- ## Commands
102
-
103
- | Command | Reads | Notes |
104
- | --- | --- | --- |
105
- | `whoami` | Token validity, API host, Site count | Cheapest health check |
106
- | `sites list` | Every accessible Site | Source of Site IDs |
107
- | `sites use <site-id>` | – | Persists a default Site for the human, not for you |
108
- | `usage` | Plan and meters | `--group integrations\|compute\|capacity` |
109
- | `actions list` | Server-ranked issues and opportunities | `--limit 1..25` (default 10), `--offset`. JSON carries `evidence.freshness`; `verdict: "aged"` means the observation is over a day old, so live-check before fixing |
110
- | `actions show <action-id>` | One issue or opportunity plus evidence | `--limit 1..100` (default 50), `--group-id`, `--cursor` |
111
- | `actions resolve <action-id>` | – | Mutation. Claims the issue or opportunity and starts server verification |
112
- | `backlinks recoverable` | Stored recoverable backlinks | `--limit 1..200`, `--offset` |
113
- | `mentions list` | Stored mentions | `--limit 1..200` |
114
- | `page inspect <url>` | Stored observations, Lighthouse, keywords | `--limit 1..200`, `--offset`, `--include-resolved`. JSON carries `observations.coverage`: `never-scanned` means no source recorded this Page, `scanned-clear` means recorded and all clear |
115
- | `page scan <url>` | – | Mutation. Starts mobile and desktop scans |
116
- | `performance` | Site performance overview | Medians for perf, a11y, SEO, LCP, TBT, CLS |
117
- | `search status` | Stored Search Console connection | Provider free |
118
- | `search analytics <pages\|keywords>` | Search Console Page or query rows | `--period`, `--limit`, `--page`, `--search` |
119
- | `search indexing <summary\|urls>` | Retained URL Inspection coverage | `summary` returns indexed counts; `urls` supports `--issue`, `--status`, `--limit`, `--offset` |
120
- | `research overview` | Stored Site and competitor research | Metrics, history, gaps, and quick wins |
121
- | `research keywords <topic>` | Live keyword ideas | Volume, difficulty, intent, and cost data |
122
- | `research serp <keyword>` | Live SERP snapshot | Results, features, and fetch time |
123
- | `research rankings <domain>` | Live domain rankings | Current keywords and domain metrics |
124
- | `audit link-opportunities` | Stored internal link opportunities | Includes crawl coverage |
125
- | `audit content-decay` | Stored decaying Pages | Includes Search Console loss evidence |
126
- | `audit duplicates` | Stored duplicate clusters | Includes members and keep candidate |
127
- | `content briefs list` | Content Brief summaries | `--status`, `--limit`, `--offset` |
128
- | `content briefs show <brief-id>` | One Content Brief | Includes its grounded payload |
129
- | `content briefs create <keyword>` | – | Mutation. `--target-page` is optional |
130
- | `config` | Local config path, API host, selected Site | Local only |
131
-
132
- `page inspect` and `page scan` take an absolute URL, for example
133
- `https://example.com/about`.
134
-
135
- `--help --json` returns a `CliHelp` value for one command level: its
136
- `arguments`, the inherited `globalOptions`, and its `subcommands` as names and
137
- descriptions only. To learn a subcommand's own flags, ask it directly, for
138
- example `nuxtseo actions list --help --json`. Use this instead of guessing a
139
- flag, and prefer the table above for anything it already answers.
100
+ Parse JSON. Never scrape human output.
101
+
102
+ Every `--json` run prints one envelope. For example, `status` returns this,
103
+ abridged:
104
+
105
+ ```sh
106
+ nuxtseo status --site site_01JXYZ --json
107
+ ```
108
+
109
+ ```json
110
+ {
111
+ "data": {
112
+ "available": true,
113
+ "dataQuality": { "status": "complete", "reason": null },
114
+ "nextAction": { "kind": "action", "actionId": "act_01JXYZ" }
115
+ },
116
+ "meta": { "requestId": "req_01JXYZ", "version": "1.0" }
117
+ }
118
+ ```
119
+
120
+ Read `data.available` first; `false` means the first assessment has not run.
121
+ Read `data.dataQuality.status` before you quote the verdict. Pass
122
+ `data.nextAction.actionId` into `actions show`.
140
123
 
141
124
  ## The triage loop
142
125
 
143
- This is the sequence that turns CLI output into a code change:
144
-
145
- 1. `nuxtseo actions list --site <id> --json` gives ranked work with an ID, a
146
- diagnosis, an effort, and an affected page count. Check
147
- `evidence.freshness` per row: `verdict: "aged"` with a large `ageHours`
148
- means the evidence is old. Live-check a cheap sample before fixing, because
149
- the site may have moved on since the observation.
150
- 2. `nuxtseo actions show <action-id> --site <id> --json` gives the evidence:
151
- which pages, which finding type, and when it was observed.
152
- 3. Fix the cause in the repository. The evidence names URLs; map them back to
153
- routes, components, or config.
154
- 4. Deploy or preview the fix, then `nuxtseo page scan <url> --site <id> --yes
155
- --json` to ask for fresh scans of a page you changed.
156
- 5. `nuxtseo actions resolve <action-id> --site <id> --yes --json` claims the
157
- action. The server verifies it; the CLI does not mark anything fixed by
158
- itself.
159
-
160
- Resolve reads the action first and sends the current `artifactVersion` for you.
161
- Do not construct that field by hand. If the evidence moved under you, the
162
- command exits `5` with `stale_evidence`; re-run step 2 and decide again.
126
+ This sequence turns CLI output into a code change. Copy the checklist and track
127
+ your progress:
128
+
129
+ ```
130
+ Triage progress:
131
+ - [ ] 1. status: read the verdict and the Next Action
132
+ - [ ] 2. actions list: pick a ranked action, check its freshness
133
+ - [ ] 3. actions show: read the evidence behind it
134
+ - [ ] 4. Fix the cause in the repository
135
+ - [ ] 5. page scan: re-scan a page you changed
136
+ - [ ] 6. actions resolve: claim the action
137
+ - [ ] 7. annotations create: mark the day the fix shipped
138
+ ```
139
+
140
+ **Step 1. Read the Site verdict.**
141
+
142
+ ```sh
143
+ nuxtseo status --site <site-id> --json
144
+ ```
145
+
146
+ This is the cheapest orientation: the verdict, the single ranked Next Action,
147
+ and what changed recently. Read `dataQuality.status` before quoting it.
148
+ `degraded` means the assessment ran with proofs missing, so the verdict is real
149
+ but Provisional. `unavailable` means it has not run. If `nextAction.kind` is
150
+ `action`, its `actionId` goes straight into step 3.
151
+
152
+ **Step 2. Pick a ranked action.**
153
+
154
+ ```sh
155
+ nuxtseo actions list --site <site-id> --json
156
+ ```
157
+
158
+ Each row carries an ID, a diagnosis, an effort, and an affected page count.
159
+ Check `evidence.freshness` per row. If `verdict` is `aged` with a large
160
+ `ageHours`, live-check a cheap sample before fixing. The site may have moved on
161
+ since the observation.
162
+
163
+ **Step 3. Read the evidence.**
164
+
165
+ ```sh
166
+ nuxtseo actions show <action-id> --site <site-id> --json
167
+ ```
168
+
169
+ This returns which pages, which finding type, and when it was observed.
170
+
171
+ Two reads deepen this step when the action's own evidence is not enough:
172
+
173
+ - `nuxtseo page issues --action-id <action-id> --site <site-id> --json` returns
174
+ the raw observations behind that action: the exact URLs, status codes,
175
+ redirect targets, and for a Lighthouse row the failing selectors and DOM
176
+ snippets. These are the mutable observation store, never canonical action
177
+ membership.
178
+ - `nuxtseo scans show <scan-id> --site <site-id> --json` returns the failing
179
+ checks behind a Lighthouse score, so you can locate the cause without
180
+ re-scanning.
181
+
182
+ **Step 4. Fix the cause in the repository.**
183
+
184
+ The evidence names URLs. Map them back to routes, components, or config.
185
+
186
+ **Step 5. Re-scan a page you changed.**
187
+
188
+ Deploy or preview the fix first. Then ask for fresh Scans:
189
+
190
+ ```sh
191
+ nuxtseo page scan <url> --site <site-id> --yes --json
192
+ ```
193
+
194
+ **Step 6. Claim the action.**
195
+
196
+ ```sh
197
+ nuxtseo actions resolve <action-id> --site <site-id> --yes --json
198
+ ```
199
+
200
+ The server verifies it. The CLI never marks anything fixed by itself. Resolve
201
+ reads the action first and sends the current `artifactVersion` for you. Do not
202
+ construct that field by hand. If the evidence moved under you, the command
203
+ exits `5` with `stale_evidence`. Re-run step 3 and decide again.
204
+
205
+ When the fix is a deliberate removal rather than a repair, dismiss instead of
206
+ resolving:
207
+
208
+ ```sh
209
+ nuxtseo actions show <action-id> --site <site-id> --json # read artifactVersion
210
+ nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
211
+ ```
212
+
213
+ Dismiss is admitted only for a broken-page action with complete 404 or 410
214
+ evidence and no internal referrers. Anything else is rejected, so it cannot be
215
+ used to hide work.
216
+
217
+ **Step 7. Mark the day the fix shipped.**
218
+
219
+ ```sh
220
+ nuxtseo annotations create --site <site-id> --date "$(date -u +%F)" --title "Fixed canonicals on /docs" --yes --json
221
+ ```
222
+
223
+ The next traffic move then has a cause beside it.
224
+
225
+ ## Datasets that must not be conflated
226
+
227
+ Two datasets share the word "vitals":
228
+
229
+ - `performance` and `scans *` read Lighthouse **lab** Scans.
230
+ - `vitals summary`, `vitals trend`, and `vitals findings` read **field**
231
+ (real-user) data. `vitals` reads CrUX. `vitals findings` reads the Site's own
232
+ Web Analytics provider.
233
+
234
+ A lab score and a field p75 disagreeing is normal, not a fault.
235
+
236
+ Two commands inspect the same URL and answer different questions. `page inspect`
237
+ reads the NuxtSEO observation store. `search inspect` reads Google's index
238
+ verdict.
239
+
240
+ `status`, `performance`, and `vitals` answer different questions. `status` is
241
+ the verdict. `performance` is the lab score. `vitals` is what real users
242
+ measured. Never substitute one for another.
243
+
244
+ The Search Console reads also split:
245
+
246
+ - `search status` reads stored connection state. It never waits for Google.
247
+ - `search analytics`, `search indexing`, `search index-history`, and
248
+ `search inspect` read retained Search Console evidence through the public API.
249
+ - `sitemaps submit` and `sitemaps delete` reach Google through the Site's
250
+ stored Search Console credential. Both are mutations.
251
+
252
+ ## Route-family indexing: two lists, one claim
253
+
254
+ `search cohorts` answers "which template is Google declining" in one read,
255
+ instead of N page-level findings. Read it before you list individual
256
+ not-indexed URLs. Its output holds two lists that mean different things. Never
257
+ merge them.
258
+
259
+ - `established` holds only families whose Wilson score interval clears the
260
+ not-indexed rate of their **own complement** after a Bonferroni correction.
261
+ These are the only rows you may report as a finding or a cause.
262
+ - `ranked` orders every family by raw rate, with **no significance test**. It
263
+ always answers "where is indexing worst", even when nothing is provable. A
264
+ row with `statisticallyEstablished: false` is a lead to verify, never a
265
+ conclusion. Presenting one as established is a hard failure.
266
+
267
+ `analysed: false` means there is no completed crawl, or no Google index state
268
+ to join against. That is a coverage gap, not healthy indexing. Say which.
163
269
 
164
270
  ## Guardrails
165
271
 
166
- - Ask before you mutate. `actions resolve` and `page scan` consume quota and
167
- change server state. `--yes` is consent you are borrowing from the user, so
168
- get it first unless the user already asked for that exact action.
169
- - Do not loop over Pages, keywords, or domains unattended. Check `usage` first.
170
- - Live research can consume allowance. A cached result does not consume a unit.
171
- - Report failures as they are. The CLI has no MCP or private-route fallback.
172
- - Do not treat an empty result as clean. `page inspect` JSON says which empty
173
- it is through `observations.coverage`; other commands may still have
174
- incomplete evidence.
272
+ - **Get consent before a mutation.** `actions resolve`, `actions dismiss`,
273
+ `page scan`, `sitemaps submit`, `sitemaps delete`, `content briefs create`,
274
+ and the `annotations` writes all change server state. `page scan` also spends
275
+ against the Lighthouse limit. `--yes` is consent you borrow from the user, so
276
+ ask first, unless the user already asked for that exact action. `actions
277
+ dismiss` says the page stays removed, so use it only when that is the
278
+ decision.
279
+ - **Live research spends against the Team research limit.** `research
280
+ keywords`, `research serp`, `research rankings`, `research domain-traffic`,
281
+ `research domain-availability`, and the `backlinks` reads other than
282
+ `backlinks recoverable` can all start it. Each one reports what it did.
283
+ Keyword, domain, and backlink JSON report cache use in `evidence`; a `cache`
284
+ or `no-provider` tag means nothing was spent. SERP and ranking JSON report
285
+ `cached`. `backlinks recoverable` and `mentions list` read retained rows and
286
+ spend nothing.
287
+ - **Never loop unattended.** Check `usage` before any run over Pages, keywords,
288
+ or domains. Use `--all` rather than your own offset loop. It writes one
289
+ envelope per line and stops at 50 requests with exit `9`. Treat exit `9` as a
290
+ stop, then resume with the argument stderr names.
291
+ - **Report the result as the CLI gave it.** Report failures as they are; there
292
+ is no MCP or private-route fallback. Never treat an empty result as clean:
293
+ `page inspect` names the kind of empty through `observations.coverage`,
294
+ `page issues` through `availableKeys`, `search cohorts` through
295
+ `analysed: false`, and `vitals summary` through `available: false`. A refusal
296
+ reads differently again: `status`, `page issues` and `search cohorts` exit
297
+ `4` or `5` on an archived or paused Site, which is never an empty Site. Other
298
+ commands may still return Provisional evidence. Quote the counts a result
299
+ ships with, never the length of its list: `data.total` from `page issues`,
300
+ then `coverage.accessibilityChecksTotal` and
301
+ `coverage.bestPracticesChecksTotal` from `scans show`. Neither command
302
+ reports the dropped row count, so `possiblyTruncated: true` marks the list as
303
+ a floor.