@nuxtseo/cli 0.5.4 → 0.5.7
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 +3 -0
- package/dist/commands.js +100 -1
- package/dist/failures.d.ts +1 -1
- package/dist/render.js +22 -2
- package/dist/skill.js +11 -1
- package/package.json +3 -3
- package/skills/nuxtseo-cli/SKILL.md +6 -3
- package/skills/nuxtseo-cli/references/commands.md +13 -2
- package/skills/nuxtseo-cli/references/protocol.md +8 -0
package/README.md
CHANGED
|
@@ -206,6 +206,9 @@ nuxtseo sitemaps urls
|
|
|
206
206
|
nuxtseo sitemaps submit <sitemap-url>
|
|
207
207
|
nuxtseo sitemaps delete <sitemap-url>
|
|
208
208
|
nuxtseo research overview
|
|
209
|
+
nuxtseo research competitors list
|
|
210
|
+
nuxtseo research competitors add <domain> --yes
|
|
211
|
+
nuxtseo research competitors remove <competitor-id> --yes
|
|
209
212
|
nuxtseo research domain-traffic <domain>
|
|
210
213
|
nuxtseo research domain-availability <domains>
|
|
211
214
|
nuxtseo research keywords <topic>
|
package/dist/commands.js
CHANGED
|
@@ -55,6 +55,7 @@ function present(runtime, globals, result, render) {
|
|
|
55
55
|
export const ALL_PAGES_REQUEST_CAP = 50;
|
|
56
56
|
async function collect(runtime, globals, options) {
|
|
57
57
|
let position = options.start;
|
|
58
|
+
const seenKeys = new Set();
|
|
58
59
|
for (let request = 1;; request++) {
|
|
59
60
|
const response = await withSpinner(runtime, options.message, () => options.fetch(position));
|
|
60
61
|
if (response._tag === 'Err')
|
|
@@ -62,6 +63,12 @@ async function collect(runtime, globals, options) {
|
|
|
62
63
|
emit(runtime, globals, response.value, options.render);
|
|
63
64
|
if (!options.all)
|
|
64
65
|
return ok(undefined);
|
|
66
|
+
for (const key of options.rowKeys?.(response.value) ?? []) {
|
|
67
|
+
if (seenKeys.has(key)) {
|
|
68
|
+
return fail(EXIT_CODE.infrastructure, 'paging_unstable: Search Console pages repeated row keys. Export coverage is incomplete. Preserve these responses for investigation.', undefined, 'paging_unstable');
|
|
69
|
+
}
|
|
70
|
+
seenKeys.add(key);
|
|
71
|
+
}
|
|
65
72
|
const next = options.next(response.value, position);
|
|
66
73
|
if (next._tag === 'Done')
|
|
67
74
|
return ok(undefined);
|
|
@@ -647,6 +654,24 @@ async function searchAnalytics(runtime, globals, args) {
|
|
|
647
654
|
},
|
|
648
655
|
}, { signal: runtime.requestSignal }),
|
|
649
656
|
render: value => renderSearchAnalytics(value.data),
|
|
657
|
+
rowKeys: (value) => {
|
|
658
|
+
const data = value.data;
|
|
659
|
+
if (!('rows' in data) || view.value === 'analysis')
|
|
660
|
+
return [];
|
|
661
|
+
return data.rows.flatMap((row) => {
|
|
662
|
+
// Canonical URLs can repeat legitimately. Only Google's exact page
|
|
663
|
+
// dimension is unique; older responses can omit it.
|
|
664
|
+
if (view.value === 'pages')
|
|
665
|
+
return 'gscPage' in row && typeof row.gscPage === 'string' ? [row.gscPage] : [];
|
|
666
|
+
if ('keyword' in row && typeof row.keyword === 'string')
|
|
667
|
+
return [row.keyword];
|
|
668
|
+
if ('country' in row)
|
|
669
|
+
return [row.country];
|
|
670
|
+
if ('device' in row)
|
|
671
|
+
return [row.device];
|
|
672
|
+
return [];
|
|
673
|
+
});
|
|
674
|
+
},
|
|
650
675
|
next: (value, position) => {
|
|
651
676
|
const data = value.data;
|
|
652
677
|
if (!('total' in data) || !('rows' in data))
|
|
@@ -762,6 +787,49 @@ async function researchOverview(runtime, globals) {
|
|
|
762
787
|
}, { signal: runtime.requestSignal }));
|
|
763
788
|
return present(runtime, globals, response, value => renderResearchOverview(value.data));
|
|
764
789
|
}
|
|
790
|
+
async function competitorsList(runtime, globals) {
|
|
791
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
792
|
+
if (resolved._tag === 'Err')
|
|
793
|
+
return resolved;
|
|
794
|
+
const response = await withSpinner(runtime, 'Loading competitors', () => resolved.value.api.client.research.listCompetitors({
|
|
795
|
+
params: { siteId: resolved.value.siteId },
|
|
796
|
+
}, { signal: runtime.requestSignal }));
|
|
797
|
+
return present(runtime, globals, response, value => value.data.competitors.map(competitor => `${competitor.id}\t${competitor.domain}`).join('\n') || '0 tracked competitors');
|
|
798
|
+
}
|
|
799
|
+
async function competitorWrite(runtime, globals, action) {
|
|
800
|
+
const confirmed = await confirmMutation(runtime, globals, action._tag === 'Add'
|
|
801
|
+
? `Track ${action.domain}? This uses the research limit.`
|
|
802
|
+
: `Remove competitor ${action.competitorId}? This cannot be undone.`);
|
|
803
|
+
if (confirmed._tag === 'Err')
|
|
804
|
+
return confirmed;
|
|
805
|
+
if (!confirmed.value) {
|
|
806
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
807
|
+
return ok(undefined);
|
|
808
|
+
}
|
|
809
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
810
|
+
if (resolved._tag === 'Err')
|
|
811
|
+
return resolved;
|
|
812
|
+
if (action._tag === 'Add') {
|
|
813
|
+
const response = await withSpinner(runtime, 'Adding competitor', () => resolved.value.api.client.research.addCompetitor({
|
|
814
|
+
params: { siteId: resolved.value.siteId },
|
|
815
|
+
body: { domain: action.domain },
|
|
816
|
+
}, { signal: runtime.requestSignal }));
|
|
817
|
+
// The add only queues the analysis. Name the read that shows it, so the
|
|
818
|
+
// caller is not left with "queued" and no way to see the result.
|
|
819
|
+
const overview = `\`nuxtseo research overview --site ${resolved.value.siteId}\``;
|
|
820
|
+
const result = present(runtime, globals, response, value => [
|
|
821
|
+
`${value.data.competitor.id}\t${value.data.competitor.domain}\tAnalysis queued`,
|
|
822
|
+
`Run ${overview} in a few minutes to read the analysis.`,
|
|
823
|
+
].join('\n'));
|
|
824
|
+
if (result._tag === 'Ok' && globals.json)
|
|
825
|
+
writeDiagnostic(runtime, `Note: Analysis runs in the background. Read it with ${overview}. The competitor's collectedAt stays null until the analysis finishes.`);
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
const response = await withSpinner(runtime, 'Removing competitor', () => resolved.value.api.client.research.removeCompetitor({
|
|
829
|
+
params: { siteId: resolved.value.siteId, competitorId: action.competitorId },
|
|
830
|
+
}, { signal: runtime.requestSignal }));
|
|
831
|
+
return present(runtime, globals, response, value => `Removed competitor ${value.data.competitorId}`);
|
|
832
|
+
}
|
|
765
833
|
/**
|
|
766
834
|
* DataForSEO refuses these characters, so the research provider removes them
|
|
767
835
|
* before it counts the words of a seed. Mirrored from
|
|
@@ -989,7 +1057,19 @@ async function sitemapsAction(runtime, globals, action, rawUrl) {
|
|
|
989
1057
|
params: { siteId: resolved.value.siteId },
|
|
990
1058
|
body: { action, sitemapUrl: sitemapUrl.value },
|
|
991
1059
|
}, { signal: runtime.requestSignal }));
|
|
992
|
-
|
|
1060
|
+
if (action === 'delete')
|
|
1061
|
+
return present(runtime, globals, response, value => renderSitemapAction(value.data));
|
|
1062
|
+
// Success means Search Console accepted the submit, not that Google read the
|
|
1063
|
+
// file. Name the read that shows the download, so the caller does not take
|
|
1064
|
+
// the submit for a fetch.
|
|
1065
|
+
const list = `\`nuxtseo sitemaps list --site ${resolved.value.siteId}\``;
|
|
1066
|
+
const result = present(runtime, globals, response, value => [
|
|
1067
|
+
renderSitemapAction(value.data),
|
|
1068
|
+
`Google downloads it later. Run ${list} to see when.`,
|
|
1069
|
+
].join('\n'));
|
|
1070
|
+
if (result._tag === 'Ok' && globals.json)
|
|
1071
|
+
writeDiagnostic(runtime, `Note: Search Console accepted the sitemap. Google downloads it later. Run ${list} and read lastDownloaded and isPending.`);
|
|
1072
|
+
return result;
|
|
993
1073
|
}
|
|
994
1074
|
async function analyticsQuery(runtime, globals, args) {
|
|
995
1075
|
const view = parseChoice(args.view, { name: 'view', choices: ANALYTICS_VIEWS });
|
|
@@ -1833,6 +1913,25 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1833
1913
|
const research = defineCommand({
|
|
1834
1914
|
meta: { name: 'research', description: 'Read stored and live search research' },
|
|
1835
1915
|
subCommands: {
|
|
1916
|
+
'competitors': defineCommand({
|
|
1917
|
+
meta: { name: 'competitors', description: 'Manage tracked competitors for the Site' },
|
|
1918
|
+
subCommands: {
|
|
1919
|
+
list: defineCommand({
|
|
1920
|
+
meta: { name: 'list', description: 'List tracked competitor domains and ids' },
|
|
1921
|
+
run: ({ args }) => capture(execution, () => competitorsList(runtime, globals), args._, 0)(),
|
|
1922
|
+
}),
|
|
1923
|
+
add: defineCommand({
|
|
1924
|
+
meta: { name: 'add', description: 'Track one competitor and queue its analysis' },
|
|
1925
|
+
args: { domain: positional('domain', 'Public competitor domain') },
|
|
1926
|
+
run: ({ args }) => capture(execution, () => competitorWrite(runtime, globals, { _tag: 'Add', domain: args.domain }), args._, 1)(),
|
|
1927
|
+
}),
|
|
1928
|
+
remove: defineCommand({
|
|
1929
|
+
meta: { name: 'remove', description: 'Remove one competitor using its listed id' },
|
|
1930
|
+
args: { competitorId: positional('competitorId', 'Competitor id from research competitors list') },
|
|
1931
|
+
run: ({ args }) => capture(execution, () => competitorWrite(runtime, globals, { _tag: 'Remove', competitorId: args.competitorId }), args._, 1)(),
|
|
1932
|
+
}),
|
|
1933
|
+
},
|
|
1934
|
+
}),
|
|
1836
1935
|
'overview': defineCommand({
|
|
1837
1936
|
meta: { name: 'overview', description: 'Read stored Site metrics and competitor research' },
|
|
1838
1937
|
run: ({ args }) => capture(execution, () => researchOverview(runtime, globals), args._, 0)(),
|
package/dist/failures.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const EXIT_CODE: {
|
|
|
12
12
|
readonly interrupted: 130;
|
|
13
13
|
};
|
|
14
14
|
export type ExitCode = typeof EXIT_CODE[keyof typeof EXIT_CODE];
|
|
15
|
-
export type LocalFailureCode = 'authentication_required' | 'authorization_required' | 'confirmation_required' | 'conflict' | 'contract_violation' | 'infrastructure_failure' | 'interrupted' | 'invalid_cli_input' | 'not_found' | 'paging_cap_reached' | 'pull_incomplete' | 'request_timeout' | 'retryable_failure' | 'site_ambiguous' | 'site_empty' | 'site_not_accessible' | 'state_error' | 'token_input_required' | 'transport_failure' | 'unexpected';
|
|
15
|
+
export type LocalFailureCode = 'authentication_required' | 'authorization_required' | 'confirmation_required' | 'conflict' | 'contract_violation' | 'infrastructure_failure' | 'interrupted' | 'invalid_cli_input' | 'not_found' | 'paging_cap_reached' | 'paging_unstable' | 'pull_incomplete' | 'request_timeout' | 'retryable_failure' | 'site_ambiguous' | 'site_empty' | 'site_not_accessible' | 'state_error' | 'token_input_required' | 'transport_failure' | 'unexpected';
|
|
16
16
|
export type CliFailureCode = LocalFailureCode | Extract<SdkFailure, {
|
|
17
17
|
_tag: 'ApiFailure';
|
|
18
18
|
}>['code'];
|
package/dist/render.js
CHANGED
|
@@ -469,6 +469,16 @@ export function renderIndexingDiagnostics(data) {
|
|
|
469
469
|
data.hasMore ? `More URLs available after offset ${data.offset + data.limit}.` : '',
|
|
470
470
|
].filter(Boolean).join('\n');
|
|
471
471
|
}
|
|
472
|
+
function renderResearchCompetitor(row) {
|
|
473
|
+
// A competitor without a collection time is still queued; its zero counts
|
|
474
|
+
// mean nothing yet.
|
|
475
|
+
if (row.collectedAt === null)
|
|
476
|
+
return `${row.domain} analysis pending`;
|
|
477
|
+
// Quick wins come from shared keywords. With none shared, "0 quick wins"
|
|
478
|
+
// says nothing about positions, so name the reason.
|
|
479
|
+
const overlap = row.sharedKeywords.length ? '' : ' no shared keywords';
|
|
480
|
+
return `${row.domain} ${row.metrics.organicTraffic ?? 'unknown'} traffic ${row.quickWins.length} quick wins${overlap}`;
|
|
481
|
+
}
|
|
472
482
|
export function renderResearchOverview(data) {
|
|
473
483
|
const subject = data.subject
|
|
474
484
|
? fields([
|
|
@@ -480,13 +490,23 @@ export function renderResearchOverview(data) {
|
|
|
480
490
|
: 'No stored Site research.';
|
|
481
491
|
const competitors = data.competitors.length === 0
|
|
482
492
|
? 'No stored competitors.'
|
|
483
|
-
: data.competitors.map(
|
|
493
|
+
: data.competitors.map(renderResearchCompetitor).join('\n');
|
|
484
494
|
return [subject, `Competitors: ${data.summary.competitorCount}`, competitors].join('\n');
|
|
485
495
|
}
|
|
486
496
|
export function renderKeywordResearch(data) {
|
|
497
|
+
const measured = data.topicMetrics.filter(row => row._tag === 'measured').length;
|
|
498
|
+
const topics = data.topicMetrics.length
|
|
499
|
+
? [
|
|
500
|
+
`Requested topics (${measured}/${data.topicMetrics.length} measured)`,
|
|
501
|
+
...data.topicMetrics.map(row => row._tag === 'measured'
|
|
502
|
+
? `${row.metrics.volume ?? 'unknown'} volume ${row.metrics.difficulty ?? 'unknown'} difficulty ${row.metrics.intent ?? 'unknown intent'}\n ${row.topic}`
|
|
503
|
+
: `No data returned\n ${row.topic}`),
|
|
504
|
+
]
|
|
505
|
+
: [];
|
|
487
506
|
if (data.keywords.length === 0)
|
|
488
|
-
return data.message ?? `No keyword ideas found for ${data.topic}
|
|
507
|
+
return [...topics, data.message ?? `No keyword ideas found for ${data.topic}.`, ...(data.tip ? [`Tip: ${data.tip}`] : [])].join('\n');
|
|
489
508
|
return [
|
|
509
|
+
...topics,
|
|
490
510
|
`Keyword ideas for ${data.topic} (${data.keywords.length} of ${data.totalFound})${data.evidence._tag === 'cache' ? ' cached' : ''}`,
|
|
491
511
|
...data.keywords.map(row => `${row.volume ?? 'unknown'} volume ${row.difficulty ?? 'unknown'} difficulty ${row.intent ?? 'unknown intent'}\n ${row.keyword}`),
|
|
492
512
|
...(data.tip ? [`Tip: ${data.tip}`] : []),
|
package/dist/skill.js
CHANGED
|
@@ -32,6 +32,15 @@ const AGENT_DIRECTORIES = {
|
|
|
32
32
|
claude: '.claude',
|
|
33
33
|
codex: '.codex',
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The notice names the agent. The check reads every agent's install, so a
|
|
37
|
+
* line without the name reads as the caller's own skill when another agent's
|
|
38
|
+
* copy is the stale one.
|
|
39
|
+
*/
|
|
40
|
+
const AGENT_NAMES = {
|
|
41
|
+
claude: 'Claude Code',
|
|
42
|
+
codex: 'Codex',
|
|
43
|
+
};
|
|
35
44
|
/**
|
|
36
45
|
* The skill shipped inside this package. A global install puts it outside the
|
|
37
46
|
* project, so the documented `cp -R node_modules/...` path does not exist for
|
|
@@ -210,7 +219,8 @@ export function skillNoticeFor(installed, current, agent) {
|
|
|
210
219
|
return installed.version === current ? null : { agent, installed: installed.version, current };
|
|
211
220
|
}
|
|
212
221
|
export function skillNoticeLine(notice) {
|
|
213
|
-
const
|
|
222
|
+
const agent = AGENT_NAMES[notice.agent];
|
|
223
|
+
const installed = notice.installed === null ? `${agent} skill version unknown` : `${agent} skill ${notice.installed} installed`;
|
|
214
224
|
if (notice.installed !== null) {
|
|
215
225
|
const installedParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.installed);
|
|
216
226
|
const currentParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.current);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.7",
|
|
5
5
|
"description": "Command line interface for the Nuxt SEO public API.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://nuxtseo.com/pro",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^1.8.0",
|
|
36
|
-
"@nuxtseo/protocol": "^0.5.
|
|
37
|
-
"@nuxtseo/sdk": "^0.5.
|
|
36
|
+
"@nuxtseo/protocol": "^0.5.7",
|
|
37
|
+
"@nuxtseo/sdk": "^0.5.7",
|
|
38
38
|
"citty": "^0.2.2",
|
|
39
39
|
"pathe": "^2.0.3"
|
|
40
40
|
},
|
|
@@ -44,9 +44,11 @@ Read the matching file before you act on its subject:
|
|
|
44
44
|
2. **Skill version.** Run `nuxtseo --version --json`. Compare the CLI version
|
|
45
45
|
with this file's frontmatter version. If the CLI is older, update the CLI
|
|
46
46
|
first. If the skill is older, run `nuxtseo skill install --agent claude` and
|
|
47
|
-
re-read this file. An older skill hides commands.
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
re-read this file. An older skill hides commands. The stderr line names
|
|
48
|
+
the agent whose copy differs, such as `Codex skill 0.5.3 installed`.
|
|
49
|
+
Refresh only that agent. If `NUXTSEO_NO_UPDATE_CHECK` is set, the line
|
|
50
|
+
never appears; compare this file's frontmatter `version` with the binary
|
|
51
|
+
instead.
|
|
50
52
|
|
|
51
53
|
3. **Token.** Run `nuxtseo whoami --json`. It returns the Team, role, scopes,
|
|
52
54
|
and token expiry. Exit `3` means no working token. Ask the user to run
|
|
@@ -64,6 +66,7 @@ Read the matching file before you act on its subject:
|
|
|
64
66
|
| `page scan` | `page:write` |
|
|
65
67
|
| `actions resolve`, `actions dismiss` | `actions:write` |
|
|
66
68
|
| `annotations` writes | `timeline:write` |
|
|
69
|
+
| `research competitors add`, `research competitors remove` | `research:write` |
|
|
67
70
|
| `sitemaps submit`, `sitemaps delete` | `sites:write` |
|
|
68
71
|
|
|
69
72
|
If a write scope is missing, do the reads, then hand the user the exact
|
|
@@ -199,7 +199,7 @@ The monitored Page then follows its own recurring schedule.
|
|
|
199
199
|
| `search cohorts` | Not-indexed rate per route family. Returns `analysed: false` when the not-indexed list is a sample (`reason: sampled-index-state`). `--limit 1..50` (default 12), `--min-pages 1..500` (default 5). Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
|
|
200
200
|
| `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
|
|
201
201
|
| `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
|
|
202
|
-
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential |
|
|
202
|
+
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential. Success means Search Console accepted it, not that Google downloaded it. Read `lastDownloaded` and `isPending` in `sitemaps list` |
|
|
203
203
|
| `sitemaps delete <sitemap-url>` | Mutation. Removes one sitemap through the Site's stored Search Console credential |
|
|
204
204
|
|
|
205
205
|
Analysis presets for `search analytics analysis` are `striking-distance`,
|
|
@@ -252,12 +252,16 @@ A missing query row is never evidence of no demand.
|
|
|
252
252
|
## Research, backlinks, and mentions
|
|
253
253
|
|
|
254
254
|
Every command here can start live research, except `research overview`,
|
|
255
|
+
`research competitors list`, `research competitors remove`,
|
|
255
256
|
`backlinks recoverable` and `mentions list`, which read retained rows. Read the
|
|
256
257
|
research boundary in SKILL.md first.
|
|
257
258
|
|
|
258
259
|
| Command | What it returns, and its flags |
|
|
259
260
|
| --- | --- |
|
|
260
|
-
| `research overview` | Stored Site and competitor research: metrics, history, gaps, quick wins. Takes no flags. Returns `data.competitors` and `data.summary` |
|
|
261
|
+
| `research overview` | Stored Site and competitor research: metrics, history, gaps, quick wins. Takes no flags. Returns `data.competitors` and `data.summary`. A competitor with `collectedAt: null` is still queued, so its empty lists mean nothing yet. Quick wins come from `sharedKeywords`; when that list is empty, zero quick wins says nothing about positions |
|
|
262
|
+
| `research competitors list` | Tracked competitor domains and ids for this Site. Suggested competitors stay excluded. Reads start no provider work. |
|
|
263
|
+
| `research competitors add <domain>` | Mutation, requires `research:write` and `--yes`. Accepts a public domain or URL. Queues analysis within existing Site and Team limits. Returns `data.competitor.id` and `data.status: queued`. The analysis runs in the background; read it with `research overview` |
|
|
264
|
+
| `research competitors remove <competitor-id>` | Mutation, requires `research:write` and `--yes`. Use an id from the list. Removes that Site's competitor without provider work. |
|
|
261
265
|
| `research keywords <topic>` | Live keyword ideas, with volume, difficulty, intent, and cost data. The seed must be 1 to 3 words. The server refuses a longer seed. The refusal arrives as exit `0` with an empty `data.keywords`, so read `data.message` first. Comma-separate several short seeds instead. Special characters are stripped before the lookup. If a seed is under 2 characters after stripping, the server refuses it the same way. The server applies default filters and echoes them in `data.filters`: `--min-volume` (default 10), `--max-volume` (default 10000), `--min-difficulty 0..100` (default 0), `--max-difficulty 0..100` (default 60). So a default run hides keywords over difficulty 60 or under 10 searches. Also `--limit 1..100` (default 20), `--intent`, `--location-code` (default 2840), `--no-related` to drop related keywords, and `--verbose` for full evidence. If `--max-volume` is under `--min-volume`, the run exits `2`. The same holds for `--max-difficulty` under `--min-difficulty` |
|
|
262
266
|
| `research serp <keyword>` | A live SERP snapshot: results, features, fetch time. `--depth 1..20` (default 10) sets how many organic results return. `--location-code` (default 2840). The keyword must be 2 to 200 characters. `data.cached` reports cache use |
|
|
263
267
|
| `research rankings <domain>` | Live domain rankings: current keywords and domain metrics. `--limit 1..100` (default 50), `--min-position 1..100` (default 1), `--max-position 1..100` (default 20), `--location-code` (default 2840), `--order position\|traffic` (default `position`). So a default run returns only the top 20 positions. If `--max-position` is under `--min-position`, the run exits `2`. An unusable domain also exits `2`, with `invalid_request`. An empty `data.keywords` with exit `0` carries the reason in `data.message`. `data.cached` reports cache use |
|
|
@@ -270,6 +274,13 @@ research boundary in SKILL.md first.
|
|
|
270
274
|
| `backlinks recoverable` | Stored recoverable backlinks. `--limit 1..200`, `--offset`. Reads retained rows |
|
|
271
275
|
| `mentions list` | Stored mentions. `--limit 1..200` (default 100), `--include-filtered` to keep the Mentions AI triage marked a false positive. Reads retained rows |
|
|
272
276
|
|
|
277
|
+
Keyword research returns `data.topicMetrics` for every accepted comma-separated topic.
|
|
278
|
+
Read it separately from filtered `data.keywords`, which contains keyword ideas.
|
|
279
|
+
A `measured` row preserves exact metrics, including zero volume and values outside idea filters.
|
|
280
|
+
A `no-data` row means the provider returned no exact metrics. It does not mean zero volume.
|
|
281
|
+
If any topic becomes shorter than two characters after sanitization, the server refuses the whole request.
|
|
282
|
+
Cached reads preserve all topic metrics without another provider call.
|
|
283
|
+
|
|
273
284
|
## Crawl audit
|
|
274
285
|
|
|
275
286
|
| Command | What it returns, and its flags |
|
|
@@ -89,6 +89,14 @@ The `--all` loop stops after 50 requests. If pages remain, the CLI exits `9`
|
|
|
89
89
|
and stderr names the argument that resumes the read, for example `--offset 50`.
|
|
90
90
|
Treat exit `9` as a stop, never as the end of the data.
|
|
91
91
|
|
|
92
|
+
Search Console breakdown exports stop with exit `7` and `paging_unstable`
|
|
93
|
+
when an exact row key repeats. The CLI preserves the offending response.
|
|
94
|
+
The export is incomplete. Preserve the responses and report the ordering issue.
|
|
95
|
+
Do not resume from a later page, because overlapping pages can also omit rows.
|
|
96
|
+
Canonical page URLs can repeat. The check uses `gscPage`, Google's exact page string.
|
|
97
|
+
Older page responses can omit `gscPage`, so those rows have no overlap check.
|
|
98
|
+
Analysis shortlists have no unique row contract and are excluded from this check.
|
|
99
|
+
|
|
92
100
|
Pass the reported `offset` or opaque `cursor` for another page. Never edit or
|
|
93
101
|
infer a cursor. Keep server order for actions. Never re-rank merged results.
|
|
94
102
|
|