@nuxtseo/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # NuxtSEO CLI
1
+ # `nuxtseo` CLI
2
2
 
3
- Use NuxtSEO public Site operations from a terminal, script, or coding agent.
3
+ Use Nuxt SEO public Site operations from a terminal, script, or coding agent.
4
4
  The CLI sends feature requests through `@nuxtseo/sdk`. It does not call MCP or
5
5
  private routes. Live research uses declared public API operations.
6
6
 
@@ -81,7 +81,7 @@ When keychain support is unavailable, it warns on stderr and uses
81
81
  scope before the request that needs it.
82
82
 
83
83
  `nuxtseo logout` removes the local credential. Revoke the token from the
84
- NuxtSEO dashboard when it must stop working everywhere. An environment token
84
+ Nuxt SEO dashboard when it must stop working everywhere. An environment token
85
85
  always wins and is unchanged by login or logout.
86
86
 
87
87
  API host resolution order is:
@@ -216,9 +216,6 @@ nuxtseo audit link-opportunities
216
216
  nuxtseo audit link-structure
217
217
  nuxtseo audit content-decay
218
218
  nuxtseo audit duplicates
219
- nuxtseo content briefs list
220
- nuxtseo content briefs show <brief-id>
221
- nuxtseo content briefs create <keyword>
222
219
  nuxtseo timeline list
223
220
  nuxtseo skill install
224
221
  ```
@@ -256,7 +253,7 @@ reads retained rows and spends nothing.
256
253
  `search indexing summary` reads retained URL Inspection coverage.
257
254
  `search index-history` dates an indexing change against known releases.
258
255
  `search inspect <url>` reads Google's own verdict for one URL.
259
- `page inspect <url>` reads the NuxtSEO observation store for the same URL.
256
+ `page inspect <url>` reads the Nuxt SEO observation store for the same URL.
260
257
  `research keywords`, `research serp`, and `research rankings` can use the Team
261
258
  research limit. Keyword responses report cache use in `evidence`. SERP and
262
259
  ranking responses report `cached: true`. A cached response spends nothing.
@@ -279,7 +276,6 @@ The CLI fetches one page per invocation by default. It never merges responses.
279
276
  | `search analytics` | `--limit 1..100`, `--page >=1` | `--limit 25 --page 1` | row views only |
280
277
  | `search indexing` | `--limit 1..500`, `--offset >=0` | `--limit 50 --offset 0` | `urls` view only |
281
278
  | `sitemaps urls` | `--cursor`, `--limit 1..1000` | `--limit 500` | yes |
282
- | `content briefs list` | `--limit 1..100`, `--offset >=0` | `--limit 25 --offset 0` | yes |
283
279
  | `timeline list` | `--kind`, `--feature`, `--severity`, `--since`, `--cursor`, `--limit 1..100` | `--limit 25` | no |
284
280
 
285
281
  Keep the server order for actions. For another page, pass the next offset or
package/dist/cli.js CHANGED
@@ -202,7 +202,7 @@ async function interactiveBare(runtime, globals) {
202
202
  return fromStateError(apiUrl.error);
203
203
  if (config._tag === 'Err')
204
204
  return fromStateError(config.error);
205
- prompts.intro(`NuxtSEO ${VERSION}`, { input: runtime.input, output: runtime.error });
205
+ prompts.intro(`nuxtseo ${VERSION}`, { input: runtime.input, output: runtime.error });
206
206
  prompts.note([
207
207
  `Authentication: ${auth.value.authenticated ? auth.value.source : 'not configured'}`,
208
208
  `API: ${apiUrl.value.apiUrl}`,
@@ -215,7 +215,7 @@ async function interactiveBare(runtime, globals) {
215
215
  { value: 'fix', label: 'Fix my Site', hint: 'Issues, Pages, and Scans' },
216
216
  { value: 'performance', label: 'Understand performance', hint: 'Performance and Search Console' },
217
217
  { value: 'research', label: 'Research growth', hint: 'Keywords, SERPs, and competitors' },
218
- { value: 'content', label: 'Plan content', hint: 'Briefs, decay, and duplicates' },
218
+ { value: 'content', label: 'Plan content', hint: 'Decay, duplicates, and annotations' },
219
219
  { value: 'account', label: 'Account and setup', hint: config.value.siteId ?? 'no Site selected' },
220
220
  { value: 'exit', label: 'Exit' },
221
221
  ],
@@ -356,8 +356,6 @@ async function interactiveBare(runtime, globals) {
356
356
  const task = await prompts.select({
357
357
  message: 'Plan content',
358
358
  options: [
359
- { value: 'briefs', label: 'Content Briefs' },
360
- { value: 'create', label: 'Create a Content Brief' },
361
359
  { value: 'decay', label: 'Content decay' },
362
360
  { value: 'duplicates', label: 'Duplicate clusters' },
363
361
  { value: 'annotations', label: 'Chart annotations', hint: 'mark what you shipped' },
@@ -368,22 +366,11 @@ async function interactiveBare(runtime, globals) {
368
366
  });
369
367
  if (typeof task === 'symbol')
370
368
  return ok(undefined);
371
- if (task === 'briefs')
372
- return runInteractiveCommand(['content', 'briefs', 'list'], runtime, globals);
373
369
  if (task === 'decay')
374
370
  return runInteractiveCommand(['audit', 'content-decay'], runtime, globals);
375
371
  if (task === 'duplicates')
376
372
  return runInteractiveCommand(['audit', 'duplicates'], runtime, globals);
377
- if (task === 'annotations')
378
- return runInteractiveCommand(['annotations', 'list'], runtime, globals);
379
- const keyword = await promptTextValue(runtime, {
380
- message: 'Which target keyword?',
381
- placeholder: 'nuxt seo',
382
- label: 'Keyword',
383
- });
384
- return keyword._tag === 'Err'
385
- ? keyword
386
- : runInteractiveCommand(['content', 'briefs', 'create', keyword.value], runtime, globals);
373
+ return runInteractiveCommand(['annotations', 'list'], runtime, globals);
387
374
  }
388
375
  const task = await prompts.select({
389
376
  message: 'Account and setup',
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, renderContentBrief, renderContentBriefCreated, renderContentBriefs, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemaps, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUsage, } from './render.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, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemaps, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, 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';
@@ -231,7 +231,7 @@ async function logout(runtime, globals) {
231
231
  else {
232
232
  writeOutput(runtime, cleared.value.removed ? 'Local credential removed.' : 'No stored credential.');
233
233
  }
234
- writeDiagnostic(runtime, 'Token revocation remains in the NuxtSEO dashboard. NUXTSEO_TOKEN, when set, is unchanged.');
234
+ writeDiagnostic(runtime, 'Token revocation remains in the Nuxt SEO dashboard. NUXTSEO_TOKEN, when set, is unchanged.');
235
235
  return ok(undefined);
236
236
  }
237
237
  async function whoami(runtime, globals) {
@@ -370,7 +370,7 @@ async function feedbackSubmit(runtime, globals, args) {
370
370
  const body = accountFeedbackBodySchema.safeParse({ ...args, cliVersion: VERSION });
371
371
  if (!body.success)
372
372
  return fail(EXIT_CODE.invalidInput, 'Pass valid --command, --comment, and --agent values. Read feedback submit --help for limits.', undefined, 'invalid_cli_input');
373
- const confirmed = await confirmMutation(runtime, globals, 'Submit this agent report to NuxtSEO feedback?');
373
+ const confirmed = await confirmMutation(runtime, globals, 'Submit this agent report to Nuxt SEO feedback?');
374
374
  if (confirmed._tag === 'Err')
375
375
  return confirmed;
376
376
  if (!confirmed.value) {
@@ -1088,65 +1088,6 @@ async function timelineEntries(runtime, globals, args) {
1088
1088
  }, { signal: runtime.requestSignal }));
1089
1089
  return present(runtime, globals, response, value => renderTimeline(value.data));
1090
1090
  }
1091
- async function contentBriefList(runtime, globals, args) {
1092
- const status = args.status === undefined
1093
- ? ok(undefined)
1094
- : parseChoice(args.status, {
1095
- name: '--status',
1096
- choices: ['queued', 'researching', 'ready', 'written', 'published', 'stale', 'failed', 'archived'],
1097
- });
1098
- if (status._tag === 'Err')
1099
- return status;
1100
- const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
1101
- if (limit._tag === 'Err')
1102
- return limit;
1103
- const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
1104
- if (offset._tag === 'Err')
1105
- return offset;
1106
- const resolved = await apiAndSite(runtime, globals);
1107
- if (resolved._tag === 'Err')
1108
- return resolved;
1109
- return collect(runtime, globals, {
1110
- all: args.all === true,
1111
- message: 'Loading Content Briefs',
1112
- start: { offset: offset.value },
1113
- fetch: position => resolved.value.api.client.content.listBriefs({
1114
- params: { siteId: resolved.value.siteId },
1115
- query: { status: status.value, limit: limit.value, offset: position.offset ?? 0 },
1116
- }, { signal: runtime.requestSignal }),
1117
- render: value => renderContentBriefs(value.data),
1118
- next: value => offsetPage(value.data.page),
1119
- });
1120
- }
1121
- async function contentBriefShow(runtime, globals, briefId) {
1122
- const resolved = await apiAndSite(runtime, globals);
1123
- if (resolved._tag === 'Err')
1124
- return resolved;
1125
- const response = await withSpinner(runtime, 'Loading Content Brief', () => resolved.value.api.client.content.showBrief({
1126
- params: { siteId: resolved.value.siteId, briefId },
1127
- }, { signal: runtime.requestSignal }));
1128
- return present(runtime, globals, response, value => renderContentBrief(value.data));
1129
- }
1130
- async function contentBriefCreate(runtime, globals, args) {
1131
- const targetPage = args.targetPage === undefined ? ok(undefined) : parseAbsolutePageUrl(args.targetPage);
1132
- if (targetPage._tag === 'Err')
1133
- return targetPage;
1134
- const resolved = await apiAndSite(runtime, globals);
1135
- if (resolved._tag === 'Err')
1136
- return resolved;
1137
- const confirmed = await confirmMutation(runtime, globals, `Create a Content Brief for ${args.keyword}?`);
1138
- if (confirmed._tag === 'Err')
1139
- return confirmed;
1140
- if (!confirmed.value) {
1141
- writeDiagnostic(runtime, 'Mutation cancelled.');
1142
- return ok(undefined);
1143
- }
1144
- const response = await withSpinner(runtime, 'Creating Content Brief', () => resolved.value.api.client.content.createBrief({
1145
- params: { siteId: resolved.value.siteId },
1146
- body: { keyword: args.keyword, targetPage: targetPage.value },
1147
- }, { signal: runtime.requestSignal }));
1148
- return present(runtime, globals, response, value => renderContentBriefCreated(value.data));
1149
- }
1150
1091
  const VITALS_FORM_FACTORS = ['phone', 'desktop', 'all'];
1151
1092
  const VITALS_METRICS = ['lcp', 'inp', 'cls'];
1152
1093
  const VITALS_FORM_FACTOR_WIRE = {
@@ -1969,51 +1910,6 @@ export function createRootCommand(runtime, globals, execution) {
1969
1910
  }),
1970
1911
  },
1971
1912
  });
1972
- const content = defineCommand({
1973
- meta: { name: 'content', description: 'Read and create Content Briefs' },
1974
- subCommands: {
1975
- briefs: defineCommand({
1976
- meta: { name: 'briefs', description: 'Manage Content Briefs' },
1977
- subCommands: {
1978
- list: defineCommand({
1979
- meta: { name: 'list', description: 'List Content Briefs' },
1980
- args: {
1981
- status: {
1982
- type: 'enum',
1983
- options: ['queued', 'researching', 'ready', 'written', 'published', 'stale', 'failed', 'archived'],
1984
- description: 'Filter by status',
1985
- },
1986
- limit: { type: 'string', description: 'Maximum Content Briefs, 1 to 100' },
1987
- offset: { type: 'string', description: 'Pagination offset' },
1988
- all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
1989
- },
1990
- run: ({ args }) => capture(execution, () => contentBriefList(runtime, globals, {
1991
- status: args.status,
1992
- limit: args.limit,
1993
- offset: args.offset,
1994
- all: args.all,
1995
- }), args._, 0)(),
1996
- }),
1997
- show: defineCommand({
1998
- meta: { name: 'show', description: 'Show one Content Brief' },
1999
- args: { briefId: positional('brief-id', 'Content Brief ID') },
2000
- run: ({ args }) => capture(execution, () => contentBriefShow(runtime, globals, args.briefId), args._, 1)(),
2001
- }),
2002
- create: defineCommand({
2003
- meta: { name: 'create', description: 'Create a Content Brief' },
2004
- args: {
2005
- 'keyword': positional('keyword', 'Target keyword'),
2006
- 'target-page': { type: 'string', description: 'Absolute target Page URL' },
2007
- },
2008
- run: ({ args }) => capture(execution, () => contentBriefCreate(runtime, globals, {
2009
- keyword: args.keyword,
2010
- targetPage: args['target-page'],
2011
- }), args._, 1)(),
2012
- }),
2013
- },
2014
- }),
2015
- },
2016
- });
2017
1913
  const search = defineCommand({
2018
1914
  meta: { name: 'search', description: 'Read Search Console state' },
2019
1915
  subCommands: {
@@ -2217,10 +2113,10 @@ export function createRootCommand(runtime, globals, execution) {
2217
2113
  meta: {
2218
2114
  name: 'nuxtseo',
2219
2115
  version: VERSION,
2220
- description: 'Use NuxtSEO from a terminal or automation',
2116
+ description: 'Use Nuxt SEO from a terminal or automation',
2221
2117
  },
2222
2118
  args: {
2223
- 'api-url': { type: 'string', description: 'Override the NuxtSEO API host' },
2119
+ 'api-url': { type: 'string', description: 'Override the Nuxt SEO API host' },
2224
2120
  'site': { type: 'string', description: 'Override the selected Site' },
2225
2121
  'json': { type: 'boolean', description: 'Write machine JSON; protocol envelopes remain unchanged' },
2226
2122
  'no-input': { type: 'boolean', description: 'Disable every interactive prompt' },
@@ -2255,7 +2151,7 @@ export function createRootCommand(runtime, globals, execution) {
2255
2151
  }),
2256
2152
  sites,
2257
2153
  feedback: defineCommand({
2258
- meta: { name: 'feedback', description: 'Report CLI problems to NuxtSEO' },
2154
+ meta: { name: 'feedback', description: 'Report CLI problems to Nuxt SEO' },
2259
2155
  subCommands: {
2260
2156
  submit: defineCommand({
2261
2157
  meta: { name: 'submit', description: 'Submit sanitized agent feedback without selecting a Site' },
@@ -2290,7 +2186,6 @@ export function createRootCommand(runtime, globals, execution) {
2290
2186
  actions,
2291
2187
  audit,
2292
2188
  backlinks,
2293
- content,
2294
2189
  mentions,
2295
2190
  page,
2296
2191
  performance: defineCommand({
package/dist/pull.js CHANGED
@@ -129,10 +129,6 @@ export const PULL_OPERATIONS = [
129
129
  params: { siteId: context.siteId },
130
130
  query: { limit: 100, includeFiltered: publicV1BooleanFlagValue(false) },
131
131
  }, { signal: context.signal })),
132
- operation('content briefs list', context => context.client.content.listBriefs({
133
- params: { siteId: context.siteId },
134
- query: { limit: 25, offset: 0 },
135
- }, { signal: context.signal })),
136
132
  operation('timeline list', context => context.client.timeline.entries({
137
133
  params: { siteId: context.siteId },
138
134
  query: { limit: 25 },
package/dist/render.d.ts CHANGED
@@ -4,7 +4,6 @@ import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxt
4
4
  import type { AnalyticsQueryData, AnalyticsView } from '@nuxtseo/protocol/v1/analytics';
5
5
  import type { AuditChanges, AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities, AuditLinkStructure } from '@nuxtseo/protocol/v1/audit';
6
6
  import type { BacklinkAnchors, BacklinksHistory, BacklinksSummary, MentionsData, RecoverableBacklinksData, ReferringDomains } from '@nuxtseo/protocol/v1/backlinks';
7
- import type { ContentBrief, ContentBriefCreateData, ContentBriefListData } from '@nuxtseo/protocol/v1/content';
8
7
  import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData } from '@nuxtseo/protocol/v1/gsc';
9
8
  import type { PageInspectData, PageIssues, PageScan } from '@nuxtseo/protocol/v1/pages';
10
9
  import type { FieldVitalFindings, FieldVitals, MonitoredPages, PerformanceScanDetail, PerformanceScans, SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
@@ -39,9 +38,6 @@ export declare function renderResearchOverview(data: StoredResearchOverview): st
39
38
  export declare function renderKeywordResearch(data: KeywordResearchData): string;
40
39
  export declare function renderSerpResearch(data: SerpResearchData): string;
41
40
  export declare function renderRankingsResearch(data: RankingsResearchData): string;
42
- export declare function renderContentBriefs(data: ContentBriefListData): string;
43
- export declare function renderContentBrief(data: ContentBrief): string;
44
- export declare function renderContentBriefCreated(data: ContentBriefCreateData): string;
45
41
  export declare function renderContentDecay(data: AuditContentDecay): string;
46
42
  export declare function renderDuplicateClusters(data: AuditDuplicateClusters): string;
47
43
  export declare function renderLinkOpportunities(data: AuditLinkOpportunities): string;
package/dist/render.js CHANGED
@@ -482,27 +482,6 @@ export function renderRankingsResearch(data) {
482
482
  ...data.keywords.map(row => `${row.position}. ${row.keyword} ${row.volume} volume ${row.traffic} traffic\n ${row.url}`),
483
483
  ].join('\n');
484
484
  }
485
- export function renderContentBriefs(data) {
486
- if (data.briefs.length === 0)
487
- return 'No Content Briefs match these filters.';
488
- return [
489
- `Content Briefs (${data.briefs.length} of ${data.page.total})`,
490
- ...data.briefs.map(brief => `${brief.id} ${brief.status}\n ${brief.keyword}`),
491
- ].join('\n');
492
- }
493
- export function renderContentBrief(data) {
494
- return fields([
495
- ['Content Brief', data.id],
496
- ['Keyword', data.keyword],
497
- ['Status', data.status],
498
- ['Generated', data.generatedAt],
499
- ['Updated', data.updatedAt],
500
- ['Error', data.error],
501
- ]);
502
- }
503
- export function renderContentBriefCreated(data) {
504
- return `${data.created ? 'Created' : 'Found existing'} Content Brief.\n${renderContentBrief(data.brief)}`;
505
- }
506
485
  export function renderContentDecay(data) {
507
486
  if (!data.connected)
508
487
  return 'Connect Search Console to find content decay.';
@@ -65,7 +65,7 @@ function keyringFailure(operation, paths, cause) {
65
65
  _tag: 'KeychainFailure',
66
66
  operation,
67
67
  path: paths.authFile,
68
- message: `Could not ${operation} the NuxtSEO credential in the OS keychain. Local auth state is recorded at ${paths.authFile}.`,
68
+ message: `Could not ${operation} the Nuxt SEO credential in the OS keychain. Local auth state is recorded at ${paths.authFile}.`,
69
69
  cause,
70
70
  });
71
71
  }
@@ -148,7 +148,7 @@ export async function saveCredential(value, options = {}) {
148
148
  return err({
149
149
  _tag: 'InvalidCredential',
150
150
  source: 'input',
151
- message: 'The NuxtSEO API token cannot be empty.',
151
+ message: 'The Nuxt SEO API token cannot be empty.',
152
152
  });
153
153
  }
154
154
  const paths = options.paths ?? defaultStatePaths();
@@ -19,7 +19,7 @@ function statePath(paths, kind) {
19
19
  function corruptState(kind, path, detail, cause) {
20
20
  const common = {
21
21
  path,
22
- message: `Could not parse NuxtSEO ${kind} state at ${path}: ${detail}`,
22
+ message: `Could not parse Nuxt SEO ${kind} state at ${path}: ${detail}`,
23
23
  ...(cause === undefined ? {} : { cause }),
24
24
  };
25
25
  return kind === 'auth'
@@ -37,7 +37,7 @@ export async function readStateJson(paths, kind) {
37
37
  kind,
38
38
  operation: 'read',
39
39
  path,
40
- message: `Could not read NuxtSEO ${kind} state at ${path}.`,
40
+ message: `Could not read Nuxt SEO ${kind} state at ${path}.`,
41
41
  cause: content,
42
42
  });
43
43
  }
@@ -76,7 +76,7 @@ export async function writeStateJson(paths, kind, value) {
76
76
  kind,
77
77
  operation: 'write',
78
78
  path,
79
- message: `Could not write NuxtSEO ${kind} state at ${path}: the filesystem is read-only or permission was denied.`,
79
+ message: `Could not write Nuxt SEO ${kind} state at ${path}: the filesystem is read-only or permission was denied.`,
80
80
  cause,
81
81
  }
82
82
  : {
@@ -84,7 +84,7 @@ export async function writeStateJson(paths, kind, value) {
84
84
  kind,
85
85
  operation: 'write',
86
86
  path,
87
- message: `Could not write NuxtSEO ${kind} state at ${path}.`,
87
+ message: `Could not write Nuxt SEO ${kind} state at ${path}.`,
88
88
  cause,
89
89
  }));
90
90
  if (written._tag === 'Err' && temporaryCreated) {
@@ -110,7 +110,7 @@ export async function removeStateFile(paths, kind) {
110
110
  kind,
111
111
  operation: 'delete',
112
112
  path,
113
- message: `Could not remove NuxtSEO ${kind} state at ${path}: the filesystem is read-only or permission was denied.`,
113
+ message: `Could not remove Nuxt SEO ${kind} state at ${path}: the filesystem is read-only or permission was denied.`,
114
114
  cause,
115
115
  }
116
116
  : {
@@ -118,7 +118,7 @@ export async function removeStateFile(paths, kind) {
118
118
  kind,
119
119
  operation: 'delete',
120
120
  path,
121
- message: `Could not remove NuxtSEO ${kind} state at ${path}.`,
121
+ message: `Could not remove Nuxt SEO ${kind} state at ${path}.`,
122
122
  cause,
123
123
  });
124
124
  });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.3.0",
5
- "description": "Command line interface for the NuxtSEO public API.",
4
+ "version": "0.4.0",
5
+ "description": "Command line interface for the Nuxt SEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
8
8
  "repository": {
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.8.0",
36
- "@nuxtseo/protocol": "^0.3.0",
37
- "@nuxtseo/sdk": "^0.3.0",
36
+ "@nuxtseo/protocol": "^0.4.0",
37
+ "@nuxtseo/sdk": "^0.4.0",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -1,149 +1,85 @@
1
1
  ---
2
2
  name: nuxtseo-cli
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.
3
+ description: Drives the `nuxtseo` CLI to triage a Site through the Nuxt SEO 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 decay, duplicate clusters, chart annotations, and issue resolution. Use whenever the user mentions NuxtSEO, the `nuxtseo` command, Site SEO work, or Nuxt SEO automation.
4
4
  ---
5
5
 
6
- # NuxtSEO CLI
6
+ # `nuxtseo` CLI
7
7
 
8
- `nuxtseo` reads a Site through the NuxtSEO public API. Every command goes
9
- through `@nuxtseo/sdk`. The CLI never calls MCP or private routes. Live
10
- research reaches a provider only through a declared public API operation.
11
-
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.
8
+ `nuxtseo` reads a Site through the Nuxt SEO public API. Use it to answer "what
9
+ is wrong with this site, and what did I break", then fix the cause in the
10
+ repository you are working in. There is no MCP or private-route fallback.
14
11
 
15
12
  ## Reference files
16
13
 
17
- Read each one before you act on its subject:
18
-
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.
26
-
27
- ## Get the binary
28
-
29
- ```sh
30
- pnpm dlx @nuxtseo/cli@latest --version # no install
31
- pnpm add -g @nuxtseo/cli # or npm install --global
32
- ```
33
-
34
- Node 22 or newer is required.
35
-
36
- Inside the NuxtSEO monorepo the package is not published. Run the built entry
37
- directly:
38
-
39
- ```sh
40
- node packages/cli/dist/cli-entry.js --version
41
- ```
42
-
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.
45
-
46
- ## Before the first command
47
-
48
- ```sh
49
- nuxtseo whoami --json # validates the token and reports its scopes
50
- ```
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
-
56
- Exit `3` means there is no working token. Ask the user to run `nuxtseo login`
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.
60
-
61
- The alternative is a token created at
62
- <https://nuxtseo.com/pro/dashboard/settings/api-tokens> and exported as
63
- `NUXTSEO_TOKEN`. Either way the token is bound to a Team role, and that role
64
- gates what it can do.
65
-
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.
68
-
69
- Resolve the Site once and reuse it. `nuxtseo sites list --json` prints every
70
- accessible Site with its ID.
71
-
72
- ## How to call it as an agent
14
+ Read the matching file before you act on its subject:
73
15
 
74
- Always pass `--json`. Pass `--site <site-id>` for Site commands.
75
- `feedback submit` needs no Site.
76
-
77
- ```sh
78
- nuxtseo actions list --site site_123 --json
79
- ```
80
-
81
- `--json` writes the complete protocol envelope to stdout and one newline. It
82
- also disables prompts. Diagnostics, warnings, spinners, and failure messages go
83
- to stderr, so stdout stays parseable.
84
-
85
- Read `data.scopes` from `whoami` before you plan a loop, not when a step fails.
86
- A Team token carries only the scopes its role allows. A `viewer` token carries
87
- every `*:read` scope plus `feedback:write`, and nothing else. That token reads
88
- the whole Site and cannot write, so steps 5, 6, and 7 of the triage loop below
89
- all exit `4`: `page scan` needs `page:write`, `actions resolve` and `actions
90
- dismiss` need `actions:write`, and the `annotations` writes need
91
- `timeline:write`. `content briefs create` needs `content:write`, and the
92
- `sitemaps` writes need `sites:write`. Live research reads need only
93
- `research:read`, so a `viewer` token can still run them.
94
-
95
- If a write scope is missing, do steps 1 to 4, then hand the user the exact
96
- commands for the rest. Do not spend calls discovering the block one step at a
97
- time.
98
-
99
- `--site` matters for a second reason. An explicit Site ID goes straight to the
100
- operation and skips the Sites read. A token whose role cannot list Sites still
101
- works. If more than one Site is accessible and `--site` is absent, the CLI
102
- exits `5` rather than guessing.
103
-
104
- Other flags worth knowing:
105
-
106
- | Flag | Use it when |
16
+ | File | Read it before you |
107
17
  | --- | --- |
108
- | `--yes`, `-y` | The command mutates. Without it, a non-interactive run exits `2` |
109
- | `--timeout-ms <ms>` | The default 30000 ms deadline is too short. Maximum is 300000 |
110
- | `--api-url <url>` | Testing against a non-production host |
111
- | `--no-input` | Running in a TTY but no prompt is wanted. `--json` already implies this |
112
-
113
- Unknown options exit `2` before any network work, so a typo costs nothing.
114
-
115
- Parse JSON. Never scrape human output.
116
-
117
- Every `--json` run prints one envelope. For example, `status` returns this,
118
- abridged:
119
-
120
- ```sh
121
- nuxtseo status --site site_01JXYZ --json
122
- ```
123
-
124
- ```json
125
- {
126
- "data": {
127
- "available": true,
128
- "dataQuality": { "status": "complete", "reason": null },
129
- "nextAction": { "kind": "action", "actionId": "act_01JXYZ" }
130
- },
131
- "meta": { "requestId": "req_01JXYZ", "version": "1.0" }
132
- }
133
- ```
134
-
135
- Read `data.available` first; `false` means the first assessment has not run.
136
- Read `data.dataQuality.status` before you quote the verdict. Pass
137
- `data.nextAction.actionId` into `actions show`.
18
+ | [references/commands.md](references/commands.md) | use a flag, range, or default. Never guess a flag |
19
+ | [references/protocol.md](references/protocol.md) | parse output, page through a list, or handle an exit code |
20
+ | [references/indexing.md](references/indexing.md) | report an indexed count or name indexed URLs |
21
+
22
+ ## Setup
23
+
24
+ 1. **Binary.** Exit `127` means the CLI is absent. Install it, then continue:
25
+
26
+ ```sh
27
+ pnpm dlx @nuxtseo/cli@latest --version # no install
28
+ pnpm add -g @nuxtseo/cli # global install, Node 22+
29
+ ```
30
+
31
+ Inside the Nuxt SEO monorepo, run `node packages/cli/dist/cli-entry.js`
32
+ instead.
33
+
34
+ 2. **Skill version.** Run `nuxtseo --version --json`. If stderr carries a line
35
+ like `skill 0.3.0 installed, CLI 0.4.0. Refresh with: nuxtseo skill install
36
+ --agent claude`, run that command and re-read this file. A stale skill hides
37
+ commands, so a missing command reads as a missing feature. If
38
+ `NUXTSEO_NO_UPDATE_CHECK` is set, the line never appears; compare this
39
+ file's frontmatter `version` with the binary instead.
40
+
41
+ 3. **Token.** Run `nuxtseo whoami --json`. It returns the Team, role, scopes,
42
+ and token expiry. Exit `3` means no working token. Ask the user to run
43
+ `nuxtseo login` themselves, because a person must approve it in a browser.
44
+ The other route is a token from
45
+ <https://nuxtseo.com/pro/dashboard/settings/api-tokens>, exported as
46
+ `NUXTSEO_TOKEN`. Never put a token in an argument, a file, or a commit.
47
+
48
+ 4. **Scopes.** Read `data.scopes` now, not when a step fails. A `viewer` token
49
+ carries every `*:read` scope plus `feedback:write`. It can run every read,
50
+ including live research, and no write:
51
+
52
+ | Write | Scope |
53
+ | --- | --- |
54
+ | `page scan` | `page:write` |
55
+ | `actions resolve`, `actions dismiss` | `actions:write` |
56
+ | `annotations` writes | `timeline:write` |
57
+ | `sitemaps submit`, `sitemaps delete` | `sites:write` |
58
+
59
+ If a write scope is missing, do the reads, then hand the user the exact
60
+ write commands. Do not discover the block one exit `4` at a time.
61
+
62
+ 5. **Site.** Run `nuxtseo sites list --json` once and reuse the ID. If more
63
+ than one Site is accessible and `--site` is absent, the CLI exits `5`
64
+ rather than guess.
65
+
66
+ ## Calling convention
67
+
68
+ - Always pass `--json` and `--site <site-id>`. `feedback submit` needs no Site.
69
+ - `--json` writes one envelope to stdout. Diagnostics go to stderr. Parse the
70
+ JSON; never scrape human output.
71
+ - A mutation needs `--yes`. Without it, a non-interactive run exits `2`.
72
+ - `--timeout-ms` raises the 30000 ms deadline, up to 300000.
73
+ - An unknown option exits `2` before any network work.
74
+ - Branch on the exit code, never on message text. The table is in
75
+ [references/protocol.md](references/protocol.md).
138
76
 
139
77
  ## The triage loop
140
78
 
141
- This sequence turns CLI output into a code change. Copy the checklist and track
142
- your progress:
79
+ Copy this checklist and track your progress:
143
80
 
144
81
  ```
145
82
  Triage progress:
146
- - [ ] 0. version check: this skill matches the binary
147
83
  - [ ] 1. status: read the verdict and the Next Action
148
84
  - [ ] 2. actions list: pick a ranked action, check its freshness
149
85
  - [ ] 3. actions show: read the evidence behind it
@@ -153,116 +89,90 @@ Triage progress:
153
89
  - [ ] 7. annotations create: mark the day the fix shipped
154
90
  ```
155
91
 
156
- **Step 0. Check this skill matches the binary.**
92
+ **1. Read the verdict.**
157
93
 
158
94
  ```sh
159
- nuxtseo --version --json
95
+ nuxtseo status --site <site-id> --json
160
96
  ```
161
97
 
162
- This writes a `CliVersion` value, so read `version` at the top level. There is
163
- no `data` wrapper. This skill records CLI 0.2.1: the `skill install` command
164
- copies this file out of that package release, so a matching binary means a
165
- matching skill. Compare the binary version with 0.2.1. If the binary differs,
166
- it may hold commands this skill never names. Refresh the skill first:
167
-
168
- ```sh
169
- nuxtseo skill install --agent claude
98
+ ```json
99
+ {
100
+ "data": {
101
+ "available": true,
102
+ "dataQuality": { "status": "complete", "reason": null },
103
+ "nextAction": { "kind": "action", "actionId": "act_01JXYZ" }
104
+ },
105
+ "meta": { "requestId": "req_01JXYZ", "version": "1.0" }
106
+ }
170
107
  ```
171
108
 
172
- Then re-read this file. The refreshed copy records its own CLI version, so the
173
- check passes on the re-read. A stale skill fails silently: it lists fewer
174
- commands, so a missing command reads as a missing feature.
175
-
176
- **Step 1. Read the Site verdict.**
177
-
178
- ```sh
179
- nuxtseo status --site <site-id> --json
180
- ```
109
+ - `available: false` means the first assessment has not run.
110
+ - Read `dataQuality.status` before you quote the verdict. `degraded` means
111
+ proofs were missing, so the verdict is Provisional. `unavailable` means it
112
+ has not run.
113
+ - If `nextAction.kind` is `action`, pass its `actionId` to step 3.
181
114
 
182
- If you need the whole Site rather than the next action, start with `pull`
183
- instead:
115
+ For the whole Site instead of the next action, run one `pull`. It performs
116
+ every spend-free Site read and writes one envelope per line:
184
117
 
185
118
  ```sh
186
119
  nuxtseo pull --site <site-id> --json > site.ndjson
187
120
  ```
188
121
 
189
- One invocation performs every stored-evidence read and writes one envelope per
190
- line. It spends nothing, and it never mutates. Read
191
- [references/commands.md](references/commands.md) for its filters and its
192
- failure behaviour. Then return to step 2 with the rows it gave you.
193
-
194
- This is the cheapest orientation: the verdict, the single ranked Next Action,
195
- and what changed recently. Read `dataQuality.status` before quoting it.
196
- `degraded` means the assessment ran with proofs missing, so the verdict is real
197
- but Provisional. `unavailable` means it has not run. If `nextAction.kind` is
198
- `action`, its `actionId` goes straight into step 3.
199
-
200
- **Step 2. Pick a ranked action.**
122
+ **2. Pick a ranked action.**
201
123
 
202
124
  ```sh
203
125
  nuxtseo actions list --site <site-id> --json
204
126
  ```
205
127
 
206
- Each row carries an ID, a diagnosis, an effort, and an affected page count.
207
- Check `evidence.freshness` per row. If `verdict` is `aged` with a large
208
- `ageHours`, live-check a cheap sample before fixing. The site may have moved on
209
- since the observation.
128
+ Keep server order. Check `evidence.freshness` on each row. If `verdict` is
129
+ `aged` with a large `ageHours`, live-check a cheap sample before you fix. The
130
+ Site may have changed since the observation.
210
131
 
211
- **Step 3. Read the evidence.**
132
+ **3. Read the evidence.**
212
133
 
213
134
  ```sh
214
135
  nuxtseo actions show <action-id> --site <site-id> --json
215
136
  ```
216
137
 
217
- This returns which pages, which finding type, and when it was observed.
218
-
219
- Two reads deepen this step when the action's own evidence is not enough:
138
+ It names the pages, the finding type, and when it was observed. Two reads go
139
+ deeper:
220
140
 
221
- - `nuxtseo page issues --action-id <action-id> --site <site-id> --json` returns
222
- the raw observations behind that action: the exact URLs, status codes,
223
- redirect targets, and for a Lighthouse row the failing selectors and DOM
224
- snippets. These are the mutable observation store, never canonical action
225
- membership.
226
- - `nuxtseo scans show <scan-id> --site <site-id> --json` returns the failing
227
- checks behind a Lighthouse score, so you can locate the cause without
228
- re-scanning.
141
+ - `page issues --action-id <action-id>`: the raw observations, with URLs,
142
+ status codes, redirect targets, and Lighthouse selectors. This is the
143
+ mutable observation store, never canonical action membership.
144
+ - `scans show <scan-id>`: the failing checks behind a Lighthouse score.
229
145
 
230
- **Step 4. Fix the cause in the repository.**
146
+ **4. Fix the cause.** Map the URLs in the evidence to routes, components, or
147
+ config.
231
148
 
232
- The evidence names URLs. Map them back to routes, components, or config.
233
-
234
- **Step 5. Re-scan a page you changed.**
235
-
236
- Deploy or preview the fix first. Then ask for fresh Scans:
149
+ **5. Re-scan.** Deploy or preview the fix first:
237
150
 
238
151
  ```sh
239
152
  nuxtseo page scan <url> --site <site-id> --yes --json
240
153
  ```
241
154
 
242
- **Step 6. Claim the action.**
155
+ **6. Claim the action.**
243
156
 
244
157
  ```sh
245
158
  nuxtseo actions resolve <action-id> --site <site-id> --yes --json
246
159
  ```
247
160
 
248
- The server verifies it. The CLI never marks anything fixed by itself. Resolve
249
- reads the action first and sends the current `artifactVersion` for you. Do not
250
- construct that field by hand. If the evidence moved under you, the command
251
- exits `5` with `stale_evidence`. Re-run step 3 and decide again.
161
+ The server verifies the claim; the CLI never marks anything fixed. Resolve
162
+ sends the current `artifactVersion` for you. If the evidence moved, it exits
163
+ `5` with `stale_evidence`: re-run step 3 and decide again.
252
164
 
253
- When the fix is a deliberate removal rather than a repair, dismiss instead of
254
- resolving:
165
+ If the fix is a deliberate removal, dismiss instead. Read `artifactVersion`
166
+ from `actions show`, then:
255
167
 
256
168
  ```sh
257
- nuxtseo actions show <action-id> --site <site-id> --json # read artifactVersion
258
169
  nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
259
170
  ```
260
171
 
261
- Dismiss is admitted only for a broken-page action with complete 404 or 410
262
- evidence and no internal referrers. Anything else is rejected, so it cannot be
263
- used to hide work.
172
+ The server admits a dismiss only for a broken-page action with complete 404 or
173
+ 410 evidence and no internal referrers.
264
174
 
265
- **Step 7. Mark the day the fix shipped.**
175
+ **7. Mark the day.**
266
176
 
267
177
  ```sh
268
178
  nuxtseo annotations create --site <site-id> --date "$(date -u +%F)" --title "Fixed canonicals on /docs" --yes --json
@@ -270,127 +180,93 @@ nuxtseo annotations create --site <site-id> --date "$(date -u +%F)" --title "Fix
270
180
 
271
181
  The next traffic move then has a cause beside it.
272
182
 
273
- ## Datasets that must not be conflated
274
-
275
- Two datasets share the word "vitals":
276
-
277
- - `performance` and `scans *` read Lighthouse **lab** Scans.
278
- - `vitals summary`, `vitals trend`, and `vitals findings` read **field**
279
- (real-user) data. `vitals` reads CrUX. `vitals findings` reads the Site's own
280
- Web Analytics provider.
281
-
282
- A lab score and a field p75 disagreeing is normal, not a fault.
283
-
284
- Two commands inspect the same URL and answer different questions. `page inspect`
285
- reads the NuxtSEO observation store. `search inspect` reads Google's index
286
- verdict.
287
-
288
- `status`, `performance`, and `vitals` answer different questions. `status` is
289
- the verdict. `performance` is the lab score. `vitals` is what real users
290
- measured. Never substitute one for another.
291
-
292
- The Search Console reads also split:
183
+ ## Reading results correctly
293
184
 
294
- - `search status` reads stored connection state. It never waits for Google.
295
- - `search analytics`, `search indexing`, `search index-history`, and
296
- `search inspect` read retained Search Console evidence through the public API.
297
- - `sitemaps submit` and `sitemaps delete` reach Google through the Site's
298
- stored Search Console credential. Both are mutations.
185
+ **Empty is not clean.** Each read names its own kind of empty. Say which one
186
+ you got:
299
187
 
300
- ## Route-family indexing: two lists, one claim
301
-
302
- `search cohorts` answers "which template is Google declining" in one read,
303
- instead of N page-level findings. Read it before you list individual
304
- not-indexed URLs. Its output holds two lists that mean different things. Never
305
- merge them.
188
+ | Command | Empty signal |
189
+ | --- | --- |
190
+ | `page inspect` | `observations.coverage` |
191
+ | `page issues` | `availableKeys` |
192
+ | `search cohorts` | `analysed: false`: no completed crawl, or no Google index state |
193
+ | `vitals summary` | `available: false` |
194
+ | `research keywords`, `research rankings`, `research domain-*`, `page issues`, `search cohorts`, `vitals findings` | `data.message`, and `data.tip` for the repair |
195
+
196
+ Read `data.message` before the list. For example, `research keywords` refuses
197
+ a seed over three words, still exits `0`, and returns `keywords: []` with
198
+ `evidence._tag: "no-provider"`. That is a refused argument, not zero demand.
199
+
200
+ **A refusal is not an empty Site.** Exit `4` is a plan, scope, or entitlement
201
+ blocker, for example `entitlement_required`. Exit `5` on `status`, `page
202
+ issues`, or `search cohorts` can mean an archived or paused Site. Report the
203
+ blocker and stop. Do not read it as "no data" or "nothing wrong", and do not
204
+ retry unchanged.
205
+
206
+ **Quote the count the result ships.** Never quote the length of a list. Use
207
+ `data.total` from `page issues`, and `coverage.accessibilityChecksTotal` and
208
+ `coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
209
+ true` marks the list as a floor.
210
+
211
+ **Keep these datasets apart:**
212
+
213
+ - `performance` and `scans *` read Lighthouse **lab** Scans. `vitals summary`
214
+ and `vitals trend` read CrUX **field** data. `vitals findings` reads the
215
+ Site's own Web Analytics provider. A lab score and a field p75 that disagree
216
+ is normal.
217
+ - `status` is the verdict, `performance` the lab score, `vitals` what real
218
+ users measured. Never substitute one for another.
219
+ - `page inspect` reads the Nuxt SEO observation store. `search inspect` reads
220
+ Google's index verdict for the same URL.
221
+ - `search status` reads the stored connection and never waits for Google.
222
+ `search analytics`, `search indexing`, `search index-history`, and
223
+ `search inspect` read retained Search Console evidence.
224
+ - An indexed count comes from `search indexing`, never from analytics rows.
225
+ See [references/indexing.md](references/indexing.md).
226
+
227
+ **Route families: `established` versus `ranked`.** Read `search cohorts`
228
+ before you list single not-indexed URLs. Never merge its two lists:
229
+
230
+ - `established` holds only families whose not-indexed rate is significant
231
+ against the rest of the Site, after a Bonferroni correction. Only these rows
232
+ are a finding or a cause.
233
+ - `ranked` orders every family by raw rate, with no significance test. A row
234
+ with `statisticallyEstablished: false` is a lead to verify, for example by
235
+ inspecting its URLs. Presenting it as a cause is a hard failure.
306
236
 
307
- - `established` holds only families whose Wilson score interval clears the
308
- not-indexed rate of their **own complement** after a Bonferroni correction.
309
- These are the only rows you may report as a finding or a cause.
310
- - `ranked` orders every family by raw rate, with **no significance test**. It
311
- always answers "where is indexing worst", even when nothing is provable. A
312
- row with `statisticallyEstablished: false` is a lead to verify, never a
313
- conclusion. Presenting one as established is a hard failure.
237
+ ## Guardrails
314
238
 
315
- `analysed: false` means there is no completed crawl, or no Google index state
316
- to join against. That is a coverage gap, not healthy indexing. Say which.
239
+ - **Get consent before a mutation.** `page scan`, `actions resolve`, `actions
240
+ dismiss`, `sitemaps submit`, `sitemaps delete`, and the `annotations` writes
241
+ change server state. `sitemaps` writes also reach Google. `--yes` is consent
242
+ you borrow from the user, so ask first unless they asked for that exact
243
+ action.
244
+ - **Spend limits.** `page scan` spends Lighthouse limit. `research *` and the
245
+ `backlinks` reads spend the Team research limit, except `backlinks
246
+ recoverable` and `mentions list`, which read retained rows. A `cache` or
247
+ `no-provider` evidence tag, or `cached: true`, means nothing was spent.
248
+ Check `usage` before any run over many Pages, keywords, or domains.
249
+ - **No shell loops.** Use `pull` for Site reads and `--all` for paging. `--all`
250
+ stops at 50 requests with exit `9`; treat that as a stop and resume with the
251
+ argument stderr names.
317
252
 
318
253
  ## Report CLI feedback
319
254
 
320
- If CLI behavior blocks or slows your work, submit feedback once per distinct problem during the task.
321
- Report confusing output, missing context, incorrect help, unexpected failures, and useful workarounds.
322
- Continue the user's task after reporting.
255
+ If the CLI blocks or slows your work, report it once per distinct problem, then
256
+ continue the task. This skill authorizes that; no confirmation is needed unless
257
+ the user forbids reporting.
323
258
 
324
259
  ```sh
325
- nuxtseo feedback submit --command "search cohorts" --agent "Codex" \
326
- --comment "Agent report by Codex. Expected a reason for missing data. Received an empty list. Used search status instead." \
260
+ nuxtseo feedback submit --command "search cohorts" --agent "<your agent name>" \
261
+ --comment "Agent report by <your agent name>. Expected a reason for missing data. Received an empty list. Used search status instead." \
327
262
  --yes --json
328
263
  ```
329
264
 
330
- Use your actual agent name. Start the comment with an agent disclosure.
331
- Include reproduction steps, expected behavior, actual behavior, and any workaround.
332
- Use `--intent improvement` for suggestions. The default is `bug`.
333
- If the affected response has a request ID, pass it with `--request-id`.
334
- The CLI adds its version automatically.
335
-
336
- Send sanitized details only. Remove tokens, cookies, personal data, private URLs, and customer content.
337
- Use command names and placeholder arguments. Never paste raw logs or full response bodies.
338
-
339
- Self-reporting sanitized CLI feedback is authorized by this skill. No separate confirmation is needed.
340
- This permission covers feedback only. Respect any user instruction that forbids reporting.
341
- The endpoint requires a working credential with `feedback:write`, available to every Team role.
342
- It uses no paid limit and allows ten reports per credential each hour.
343
-
344
- Success returns `data.id` and `data.status: "new"`. Keep the ID with your task notes.
345
- If reporting fails, mention the failure and continue. Never retry uncertain submissions or report failures recursively.
346
- If authentication is broken, describe the feedback to the user. Do not invent another endpoint.
347
-
348
- Reports use the existing feedback queue through `POST /api/v1/account/feedback`.
349
- Health checks show unresolved reports across all dates.
350
- Operators use **Pro feedback** in admin to record a fix reference and set `fixed` after verification.
351
- Fixed, replied, closed, and ignored reports leave the health check list. Reopening restores them.
352
-
353
- ## Guardrails
354
-
355
- - **Get consent before a mutation.** `actions resolve`, `actions dismiss`,
356
- `page scan`, `sitemaps submit`, `sitemaps delete`, `content briefs create`,
357
- and the `annotations` writes all change server state. `page scan` also spends
358
- against the Lighthouse limit. `--yes` is consent you borrow from the user, so
359
- ask first, unless the user already asked for that exact action. `actions
360
- dismiss` says the page stays removed, so use it only when that is the
361
- decision.
362
- - **Live research spends against the Team research limit.** `research
363
- keywords`, `research serp`, `research rankings`, `research domain-traffic`,
364
- `research domain-availability`, and the `backlinks` reads other than
365
- `backlinks recoverable` can all start it. Each one reports what it did.
366
- Keyword, domain, and backlink JSON report cache use in `evidence`; a `cache`
367
- or `no-provider` tag means nothing was spent. SERP and ranking JSON report
368
- `cached`. `backlinks recoverable` and `mentions list` read retained rows and
369
- spend nothing.
370
- - **Prefer one `pull` over a shell loop.** `pull` performs every spend-free
371
- Site read in one invocation. Never write a shell loop over `nuxtseo` calls.
372
- - **Never loop unattended.** Check `usage` before any run over Pages, keywords,
373
- or domains. Use `--all` rather than your own offset loop. It writes one
374
- envelope per line and stops at 50 requests with exit `9`. Treat exit `9` as a
375
- stop, then resume with the argument stderr names.
376
- - **Read `data.message` before you read a list.** `research keywords` refuses a
377
- seed over three words and still exits `0`, with `keywords: []` and
378
- `evidence._tag: "no-provider"`. The reason sits in `data.message`, and the
379
- repair sits in `data.tip`. A `no-provider` tag can mean a refused argument.
380
- Never read it as proof that the topic has no demand. `research rankings`,
381
- `research domain-traffic`, `research domain-availability`, `page issues`,
382
- `search cohorts`, and `vitals findings` carry the same `data.message`. The
383
- full list is in references/protocol.md.
384
- - **Report the result as the CLI gave it.** Report failures as they are; there
385
- is no MCP or private-route fallback. Never treat an empty result as clean:
386
- `page inspect` names the kind of empty through `observations.coverage`,
387
- `page issues` through `availableKeys`, `search cohorts` through
388
- `analysed: false`, and `vitals summary` through `available: false`. A refusal
389
- reads differently again: `status`, `page issues` and `search cohorts` exit
390
- `4` or `5` on an archived or paused Site, which is never an empty Site. Other
391
- commands may still return Provisional evidence. Quote the counts a result
392
- ships with, never the length of its list: `data.total` from `page issues`,
393
- then `coverage.accessibilityChecksTotal` and
394
- `coverage.bestPracticesChecksTotal` from `scans show`. Neither command
395
- reports the dropped row count, so `possiblyTruncated: true` marks the list as
396
- a floor.
265
+ - Start the comment with an agent disclosure. Give reproduction steps, the
266
+ expected and actual behaviour, and any workaround.
267
+ - Pass `--request-id` when the response had one. Use `--intent improvement`
268
+ for a suggestion; the default is `bug`.
269
+ - Send sanitized details only: no tokens, cookies, personal data, private URLs,
270
+ customer content, raw logs, or full response bodies.
271
+ - The limit is ten reports per credential per hour. If a report fails, say so
272
+ and continue. Never retry it or report the failure.
@@ -10,6 +10,11 @@ Pass `--json` and `--site <site-id>` on every command below, except `whoami`,
10
10
  `Mutation` marks a command that changes server state. A mutation needs `--yes`
11
11
  in a non-interactive shell.
12
12
 
13
+ Global options work on every command: `--site`, `--json`, `--yes` (`-y`),
14
+ `--timeout-ms` (1 to 300000, default 30000), `--api-url` for a non-production
15
+ host, and `--no-input` to disable prompts in a TTY. `--json` already implies
16
+ `--no-input`.
17
+
13
18
  `Needs a Site URL` marks a command that reads the Site's own domain. If the Site
14
19
  has no URL, the command exits `2`. Set the Site URL first; retrying will not
15
20
  help.
@@ -17,7 +22,7 @@ help.
17
22
  `--all` repeats a paged read until every page is written, one envelope per line.
18
23
  It is available on `actions list`, `page inspect`, `page issues`, `vitals
19
24
  findings`, `backlinks recoverable`, `search analytics` row views, `search
20
- indexing urls`, `sitemaps urls` and `content briefs list`.
25
+ indexing urls` and `sitemaps urls`.
21
26
 
22
27
  The `search analytics` row views are `pages`, `keywords`, `countries`,
23
28
  `devices`, and `analysis`. `--all` on any other view exits `2`.
@@ -34,7 +39,7 @@ The `search analytics` row views are `pages`, `keywords`, `countries`,
34
39
  - Web Analytics
35
40
  - Research, backlinks, and mentions
36
41
  - Crawl audit
37
- - Content Briefs, Timeline, and annotations
42
+ - Timeline and annotations
38
43
  - Ask the CLI for its own help
39
44
 
40
45
  ## Which command answers which question
@@ -51,7 +56,7 @@ a research unit.
51
56
  | What should I fix first | `actions list` |
52
57
  | Why does this action exist | `actions show <action-id>` |
53
58
  | Which exact URLs does it affect | `page issues`, with `--action-id` |
54
- | What does NuxtSEO know about one URL | `page inspect <url>` |
59
+ | What does Nuxt SEO know about one URL | `page inspect <url>` |
55
60
  | Did my last deploy break anything | `audit changes` |
56
61
  | How much of the Site is indexed | `search indexing summary` |
57
62
  | Which route family is Google declining | `search cohorts` |
@@ -80,7 +85,7 @@ reads with one invocation.
80
85
 
81
86
  `pull` excludes every mutation, and every read that needs an argument it cannot
82
87
  invent, such as `page inspect`, `page issues`, `search inspect`, `actions show`,
83
- `scans show` and `content briefs show`. Run those yourself with the IDs `pull`
88
+ and `scans show`. Run those yourself with the IDs `pull`
84
89
  returned.
85
90
 
86
91
  `pull` makes one request per read. It never follows pages, so a run costs
@@ -227,13 +232,10 @@ research boundary in SKILL.md first.
227
232
  | `audit content-decay` | Stored decaying Pages, with Search Console loss evidence. Takes no flags. If `connected` is `false`, the Site has no Search Console data. Never report that as clean. `truncated: true` marks the list as a floor |
228
233
  | `audit duplicates` | Stored duplicate clusters, with members and keep candidate. Takes no flags. `crawlSettingsId: null` means the Site never completed a crawl. Then `total: 0` proves nobody looked. Never report it as a clean Site |
229
234
 
230
- ## Content Briefs, Timeline, and annotations
235
+ ## Timeline and annotations
231
236
 
232
237
  | Command | What it returns, and its flags |
233
238
  | --- | --- |
234
- | `content briefs list` | Content Brief summaries. `--status`, `--limit`, `--offset` |
235
- | `content briefs show <brief-id>` | One Content Brief, including its grounded payload |
236
- | `content briefs create <keyword>` | Mutation. Creates one Content Brief. `--target-page` is optional |
237
239
  | `timeline list` | Stored Timeline Entries. `--kind`, `--feature`, `--severity`, `--since`, `--limit`, `--cursor`, `--include-closed` |
238
240
  | `annotations list` | This Site's own chart annotations: date-anchored markers on traffic and ranking charts |
239
241
  | `annotations create` | Mutation, so it needs `--yes`. Marks the day you shipped a fix. `--date YYYY-MM-DD` and `--title` are required. `--note` and `--url` are optional |
@@ -45,8 +45,9 @@ Page row:
45
45
  nuxtseo search analytics pages --site <site-id> --period 7d --all --json
46
46
  ```
47
47
 
48
- Keep rows where `row.impressions > 0`. Count those rows, and report `row.url`
49
- when the user asks which URLs appeared. Do not use `data.total` for this count.
48
+ Keep rows where `row.impressions > 0`. Count distinct `row.gscPage` values,
49
+ not rows. `row.url` is canonicalised, so one URL can span several rows. Report
50
+ distinct `row.url` values when the user asks which URLs appeared. Do not use `data.total` for this count.
50
51
  It also includes comparison-period rows with zero current impressions.
51
52
 
52
53
  Keep these reads separate:
@@ -94,31 +94,44 @@ infer a cursor. Keep server order for actions. Never re-rank merged results.
94
94
 
95
95
  ## List key and paging shape per command
96
96
 
97
- There is no single list key. Read the key this command returns; never guess a
98
- chain. Verified against the response schemas.
97
+ Lists are converging on one envelope: `data.rows` plus `data.page`, where
98
+ `data.page` holds `total`, `limit`, `offset`, and `hasMore`. `page.total` counts
99
+ the whole result set, not this page. Prefer that envelope when the command has
100
+ it. If `data.rows` is absent, the server is older than the CLI; read the domain
101
+ key in the table instead.
102
+
103
+ The domain keys `data.sites`, `data.actions`, `data.clusters`, `data.items` on
104
+ `backlinks recoverable` and `mentions list`, and `data.observations.pagination`
105
+ are deprecated aliases. They are removed in a later minor version. Do not build
106
+ on them.
107
+
108
+ Commands without the envelope have no single list key. Read the key this
109
+ command returns; never guess a chain. Verified against the response schemas.
99
110
 
100
111
  | Command | List key | Paging fields |
101
112
  | --- | --- | --- |
102
- | `sites list` | `data.sites` | none; `meta.total` |
113
+ | `sites list` | `data.rows` (alias `data.sites`) | `data.page`; `meta.total` |
103
114
  | `usage` | `data.meters` | none |
104
- | `actions list` | `data.actions` | `data.page.total`, `.limit`, `.offset`, `.hasMore` |
115
+ | `actions list` | `data.rows` (alias `data.actions`) | `data.page.total`, `.limit`, `.offset`, `.hasMore` |
105
116
  | `actions show` | `data.evidenceGroups[].items` | `data.evidenceGroups[].nextCursor` |
106
- | `page inspect` | `data.observations.rows` | `data.observations.total` and `data.observations.pagination.limit`, `.offset`, `.hasMore` |
117
+ | `page inspect` | `data.observations.rows` | `data.observations.page` (alias `data.observations.pagination`); `data.observations.total` |
107
118
  | `page issues` | `data.issues` | `data.total`, `data.limit`, `data.offset` |
108
119
  | `scans list` | `data.scans` | `data.total`, `data.limit` |
109
120
  | `scans pages` | `data.pages` | `data.total` |
110
121
  | `vitals findings` | `data.findings` | `data.total`, `data.limit`, `data.offset` |
111
- | `search analytics` row views | `data.rows` | `data.total`; page with `--page` |
122
+ | `search analytics` `pages`, `keywords`, `countries`, `devices` | `data.rows` | `data.page`; `data.total`; page with `--page` |
123
+ | `search analytics` `analysis` | `data.rows` | `data.total`; page with `--page` |
124
+ | `search analytics` `page-detail`, `keyword-detail` | `data.rows` | none |
112
125
  | `search analytics timeseries` | `data.daily` | none |
113
126
  | `search indexing urls` | `data.urls` | `data.total`, `data.limit`, `data.offset`, `data.hasMore` |
114
127
  | `search index-history` | `data.changes` | none |
115
128
  | `search cohorts` | `data.established` and `data.ranked` | `data.rankedTotal` |
116
129
  | `sitemaps list` | `data.sitemaps` | none |
117
130
  | `sitemaps urls` | `data.items` | `data.page.nextCursor`, `data.page.limit` |
118
- | `audit content-decay` | `data.rows` | `data.total`, `data.truncated` |
119
- | `audit link-opportunities` | `data.rows` | `data.total` |
120
- | `audit duplicates` | `data.clusters` | `data.total` |
121
- | `audit link-structure` | `data.templateSlots`, `data.deadEnds`, `data.genericAnchors` | one total per list |
131
+ | `audit content-decay` | `data.rows` | `data.page`; `data.total`, `data.truncated` |
132
+ | `audit link-opportunities` | `data.rows` | `data.page`; `data.total` |
133
+ | `audit duplicates` | `data.rows` (alias `data.clusters`) | `data.page`; `data.total` |
134
+ | `audit link-structure` | `data.templateSlots`, `data.deadEnds`, `data.genericAnchors` | `data.templateSlotTotal`; `data.deadEndGroupTotal` and `data.deadEndPageTotal`; `data.genericAnchorTotal` |
122
135
  | `research keywords` | `data.keywords` | `data.totalFound` |
123
136
  | `research serp` | `data.results` | none; `data.depth` |
124
137
  | `research rankings` | `data.keywords` | `data.totalFound` |
@@ -126,9 +139,8 @@ chain. Verified against the response schemas.
126
139
  | `backlinks referring-domains` | `data.items` | `data.total`, `data.limit` |
127
140
  | `backlinks anchors` | `data.items` | `data.total`, `data.limit` |
128
141
  | `backlinks history` | `data.items` | `data.total` |
129
- | `backlinks recoverable` | `data.items` | `data.total`, `data.limit`, `data.offset` |
130
- | `mentions list` | `data.items` | `data.limit` |
131
- | `content briefs list` | `data.briefs` | `data.page.total`, `.limit`, `.offset`, `.hasMore` |
142
+ | `backlinks recoverable` | `data.rows` (alias `data.items`) | `data.page`; `data.total`, `data.limit`, `data.offset` |
143
+ | `mentions list` | `data.rows` (alias `data.items`) | `data.page`; `data.total`, `data.limit` |
132
144
  | `timeline list` | `data.entries` | `data.nextCursor` |
133
145
  | `annotations list` | `data.annotations` | `data.total`, `data.limit` |
134
146