outrank-cli 0.5.0 → 0.6.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.
Files changed (58) hide show
  1. package/README.md +317 -20
  2. package/SKILL.md +20 -2
  3. package/bin/outrank-mcp.js +3 -0
  4. package/package.json +21 -5
  5. package/src/adapters/cli/jsonInput.js +42 -0
  6. package/src/adapters/cli/parse.js +124 -0
  7. package/src/adapters/cli/run.js +139 -0
  8. package/src/adapters/mcp/constants.js +49 -0
  9. package/src/adapters/mcp/descriptions.js +87 -0
  10. package/src/adapters/mcp/help.js +34 -0
  11. package/src/adapters/mcp/input.js +195 -0
  12. package/src/adapters/mcp/instructions.js +139 -0
  13. package/src/adapters/mcp/logger.js +45 -0
  14. package/src/adapters/mcp/main.js +36 -0
  15. package/src/adapters/mcp/options.js +118 -0
  16. package/src/adapters/mcp/result.js +33 -0
  17. package/src/adapters/mcp/server.js +174 -0
  18. package/src/adapters/mcp/session.js +274 -0
  19. package/src/adapters/mcp/toolNotes.js +426 -0
  20. package/src/adapters/mcp/toolsets.js +106 -0
  21. package/src/commands/articles.js +285 -301
  22. package/src/commands/auth.js +204 -86
  23. package/src/commands/billing.js +102 -104
  24. package/src/commands/improvements.js +319 -211
  25. package/src/commands/index.js +22 -0
  26. package/src/commands/keywords.js +266 -249
  27. package/src/commands/marketplace/buyer.js +386 -0
  28. package/src/commands/marketplace/index.js +83 -0
  29. package/src/commands/marketplace/notifications.js +60 -0
  30. package/src/commands/marketplace/seller.js +595 -0
  31. package/src/commands/marketplace/shared.js +27 -0
  32. package/src/commands/products/backlinks.js +181 -0
  33. package/src/commands/products/core.js +149 -0
  34. package/src/commands/products/gsc.js +250 -0
  35. package/src/commands/products/index.js +174 -0
  36. package/src/commands/products/integrations.js +343 -0
  37. package/src/commands/products/linking.js +192 -0
  38. package/src/commands/products/shared.js +8 -0
  39. package/src/commands/registry.js +272 -0
  40. package/src/commands/subscription.js +20 -30
  41. package/src/commands/usage.js +17 -30
  42. package/src/core/config.js +243 -62
  43. package/src/core/constants.js +101 -0
  44. package/src/core/context.js +311 -0
  45. package/src/core/errors.js +39 -0
  46. package/src/core/execute.js +71 -0
  47. package/src/core/files.js +186 -0
  48. package/src/core/http.js +393 -0
  49. package/src/core/profileSelection.js +7 -0
  50. package/src/core/query.js +11 -0
  51. package/src/core/redact.js +27 -0
  52. package/src/core/schema.js +222 -0
  53. package/src/core/version.js +12 -0
  54. package/src/index.js +30 -58
  55. package/src/commands/marketplace.js +0 -740
  56. package/src/commands/products.js +0 -1185
  57. package/src/core/api.js +0 -82
  58. package/src/core/args.js +0 -144
@@ -0,0 +1,181 @@
1
+ import { LIST_MAX_LIMIT, LIST_MIN_LIMIT } from '../../core/constants.js';
2
+ import { bool, int, oneOf, string, uuid } from '../../core/schema.js';
3
+ import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from '../registry.js';
4
+ import { PRODUCT_CLI, productInput, productParams, withProduct } from './shared.js';
5
+
6
+ const BACKLINKS_USAGE = `Usage:
7
+ outrank-cli products backlinks get <product-id>
8
+ outrank-cli products backlinks update <product-id> --json <object>
9
+ outrank-cli products backlinks credits <product-id>
10
+ outrank-cli products backlinks earned <product-id> [--limit <n>] [--cursor <iso>] [--status <status>] [--sort-by created_at|status|credits_applied] [--sort-order desc] [--include-article] [--include-summary]
11
+ outrank-cli products backlinks unverified <product-id> [--limit <n>] [--cursor <iso>] [--status <status>] [--sort-by created_at|verification_error_reason] [--sort-order desc]
12
+ outrank-cli products backlinks check-removal <product-id> <backlink-id>
13
+ outrank-cli products backlinks reverify <product-id> <backlink-id>
14
+ outrank-cli products backlinks subscription <product-id>
15
+ outrank-cli products backlinks subscribe-quote <product-id> --plan premium|enterprise`;
16
+
17
+ const TOOLSETS = [Toolset.BACKLINKS];
18
+ const BACKLINKS_PATH = '/products/{productId}/backlinks';
19
+ const BACKLINK_PATH = `${BACKLINKS_PATH}/{backlinkId}`;
20
+ const BACKLINK_CLI = withProduct(['backlinkId']);
21
+ const BACKLINK_PLANS = Object.freeze(['premium', 'enterprise']);
22
+ const PLAN_PLACEHOLDERS = { plan: BACKLINK_PLANS.join('|') };
23
+ const PLAN_REQUIRED = {
24
+ guidance:
25
+ 'Run `outrank-cli products backlinks subscribe-quote <product-id> --plan premium|enterprise` to inspect the ' +
26
+ 'dashboard-equivalent quote.',
27
+ };
28
+
29
+ const backlinkInput = () => ({ ...productInput(), backlinkId: uuid().required() });
30
+ const backlinkParams = (input) => ({ productId: input.productId, backlinkId: input.backlinkId });
31
+
32
+ const listInput = () => ({
33
+ ...productInput(),
34
+ limit: int({ min: LIST_MIN_LIMIT, max: LIST_MAX_LIMIT }),
35
+ cursor: string(),
36
+ status: string(),
37
+ sortBy: string(),
38
+ sortOrder: string(),
39
+ });
40
+ const listQuery = (input) => ({
41
+ limit: input.limit,
42
+ cursor: input.cursor,
43
+ status: input.status,
44
+ sort_by: input.sortBy,
45
+ sort_order: input.sortOrder,
46
+ });
47
+
48
+ const get = defineCommand({
49
+ id: 'products.backlinks.get',
50
+ summary: 'Get the backlink exchange settings of a product.',
51
+ kind: CommandKind.READ,
52
+ toolsets: TOOLSETS,
53
+ input: productInput(),
54
+ cli: PRODUCT_CLI,
55
+ run: (input, ctx) => ctx.http({ method: 'GET', path: BACKLINKS_PATH, pathParams: productParams(input) }),
56
+ });
57
+
58
+ const update = defineCommand({
59
+ id: 'products.backlinks.update',
60
+ summary: 'Update the backlink exchange settings of a product from a JSON body.',
61
+ kind: CommandKind.WRITE,
62
+ toolsets: TOOLSETS,
63
+ input: productInput(),
64
+ cli: PRODUCT_CLI,
65
+ body: BodyMode.JSON,
66
+ run: (input, ctx) =>
67
+ ctx.http({ method: 'PATCH', path: BACKLINKS_PATH, pathParams: productParams(input), body: input.body }),
68
+ });
69
+
70
+ const credits = defineCommand({
71
+ id: 'products.backlinks.credits',
72
+ summary: 'Show the backlink credit balance of a product.',
73
+ kind: CommandKind.READ,
74
+ toolsets: TOOLSETS,
75
+ input: productInput(),
76
+ cli: PRODUCT_CLI,
77
+ run: (input, ctx) => ctx.http({ method: 'GET', path: `${BACKLINKS_PATH}/credits`, pathParams: productParams(input) }),
78
+ });
79
+
80
+ const earned = defineCommand({
81
+ id: 'products.backlinks.earned',
82
+ summary: 'List earned backlinks with optional article and summary details.',
83
+ kind: CommandKind.READ,
84
+ toolsets: TOOLSETS,
85
+ input: { ...listInput(), includeArticle: bool(), includeSummary: bool() },
86
+ cli: PRODUCT_CLI,
87
+ run: (input, ctx) =>
88
+ ctx.http({
89
+ method: 'GET',
90
+ path: `${BACKLINKS_PATH}/earned`,
91
+ pathParams: productParams(input),
92
+ query: {
93
+ ...listQuery(input),
94
+ include_article: input.includeArticle ? true : undefined,
95
+ include_summary: input.includeSummary ? true : undefined,
96
+ },
97
+ }),
98
+ });
99
+
100
+ const unverified = defineCommand({
101
+ id: 'products.backlinks.unverified',
102
+ summary: 'List backlinks that failed verification.',
103
+ kind: CommandKind.READ,
104
+ toolsets: TOOLSETS,
105
+ input: listInput(),
106
+ cli: PRODUCT_CLI,
107
+ run: (input, ctx) =>
108
+ ctx.http({
109
+ method: 'GET',
110
+ path: `${BACKLINKS_PATH}/unverified`,
111
+ pathParams: productParams(input),
112
+ query: listQuery(input),
113
+ }),
114
+ });
115
+
116
+ const backlinkAction = ({ id, summary, suffix }) =>
117
+ defineCommand({
118
+ id: `products.backlinks.${id}`,
119
+ summary,
120
+ kind: CommandKind.WRITE,
121
+ toolsets: TOOLSETS,
122
+ input: backlinkInput(),
123
+ cli: BACKLINK_CLI,
124
+ run: (input, ctx) =>
125
+ ctx.http({ method: 'POST', path: `${BACKLINK_PATH}/${suffix}`, pathParams: backlinkParams(input) }),
126
+ });
127
+
128
+ const checkRemoval = backlinkAction({
129
+ id: 'check-removal',
130
+ summary: 'Check whether an earned backlink was removed.',
131
+ suffix: 'check-removal',
132
+ });
133
+ const reverify = backlinkAction({
134
+ id: 'reverify',
135
+ summary: 'Re-run verification for one backlink.',
136
+ suffix: 'reverify',
137
+ });
138
+
139
+ const subscription = defineCommand({
140
+ id: 'products.backlinks.subscription',
141
+ summary: 'Show the backlink plan subscription of a product.',
142
+ kind: CommandKind.READ,
143
+ toolsets: TOOLSETS,
144
+ input: productInput(),
145
+ cli: PRODUCT_CLI,
146
+ entryPoint: true,
147
+ run: (input, ctx) =>
148
+ ctx.http({ method: 'GET', path: `${BACKLINKS_PATH}/subscription`, pathParams: productParams(input) }),
149
+ });
150
+
151
+ const subscribeQuote = defineCommand({
152
+ id: 'products.backlinks.subscribe-quote',
153
+ summary: 'Quote a backlink plan subscription for a product.',
154
+ kind: CommandKind.READ,
155
+ toolsets: TOOLSETS,
156
+ input: { ...productInput(), plan: oneOf(BACKLINK_PLANS).required(PLAN_REQUIRED) },
157
+ cli: { ...PRODUCT_CLI, placeholders: PLAN_PLACEHOLDERS },
158
+ entryPoint: true,
159
+ run: (input, ctx) =>
160
+ ctx.http({
161
+ method: 'GET',
162
+ path: `${BACKLINKS_PATH}/subscribe-quote`,
163
+ pathParams: productParams(input),
164
+ query: { plan: input.plan },
165
+ }),
166
+ });
167
+
168
+ export const backlinksGroup = defineGroup({
169
+ id: 'products.backlinks',
170
+ help: {
171
+ usage: BACKLINKS_USAGE,
172
+ guidance:
173
+ 'Use `get` and `update` for product backlink settings, `credits` and `subscription` for status, `earned` and ' +
174
+ '`unverified` for review queues, and the action commands for verification follow-up.',
175
+ },
176
+ unknown: (word) => ({
177
+ message: `Unknown products backlinks subcommand: ${word}.`,
178
+ guidance: 'Run `outrank-cli products backlinks --help` to see the available backlink commands.',
179
+ }),
180
+ children: [get, update, credits, earned, unverified, checkRemoval, reverify, subscription, subscribeQuote],
181
+ });
@@ -0,0 +1,149 @@
1
+ import { LIST_MAX_LIMIT, LIST_MIN_LIMIT } from '../../core/constants.js';
2
+ import { int, string } from '../../core/schema.js';
3
+ import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from '../registry.js';
4
+ import { PRODUCT_CLI, productInput, productParams } from './shared.js';
5
+
6
+ const TOOLSETS = [Toolset.CORE];
7
+ const PRODUCT_PATH = '/products/{productId}';
8
+
9
+ export const productsList = defineCommand({
10
+ id: 'products.list',
11
+ summary: 'List products with optional status filter and cursor pagination.',
12
+ kind: CommandKind.READ,
13
+ toolsets: TOOLSETS,
14
+ input: { status: string(), limit: int({ min: LIST_MIN_LIMIT, max: LIST_MAX_LIMIT }), cursor: string() },
15
+ run: (input, ctx) =>
16
+ ctx.http({
17
+ method: 'GET',
18
+ path: '/products',
19
+ query: { status: input.status, limit: input.limit, cursor: input.cursor },
20
+ }),
21
+ });
22
+
23
+ export const productsGet = defineCommand({
24
+ id: 'products.get',
25
+ summary: 'Get one product.',
26
+ kind: CommandKind.READ,
27
+ toolsets: TOOLSETS,
28
+ input: productInput(),
29
+ cli: PRODUCT_CLI,
30
+ run: (input, ctx) => ctx.http({ method: 'GET', path: PRODUCT_PATH, pathParams: productParams(input) }),
31
+ });
32
+
33
+ export const productsCreate = defineCommand({
34
+ id: 'products.create',
35
+ summary: 'Create a product from a JSON body. The server queues an initial keyword plan.',
36
+ kind: CommandKind.WRITE,
37
+ toolsets: TOOLSETS,
38
+ body: BodyMode.JSON,
39
+ run: (input, ctx) => ctx.http({ method: 'POST', path: '/products', body: input.body }),
40
+ });
41
+
42
+ export const productsUpdate = defineCommand({
43
+ id: 'products.update',
44
+ summary: 'Update a product from a JSON body.',
45
+ kind: CommandKind.WRITE,
46
+ toolsets: TOOLSETS,
47
+ input: productInput(),
48
+ cli: PRODUCT_CLI,
49
+ body: BodyMode.JSON,
50
+ run: (input, ctx) =>
51
+ ctx.http({ method: 'PATCH', path: PRODUCT_PATH, pathParams: productParams(input), body: input.body }),
52
+ });
53
+
54
+ export const productsPause = defineCommand({
55
+ id: 'products.pause',
56
+ summary: 'Pause a product and free its slot.',
57
+ kind: CommandKind.DESTRUCTIVE,
58
+ toolsets: TOOLSETS,
59
+ input: productInput(),
60
+ cli: PRODUCT_CLI,
61
+ run: (input, ctx) => ctx.http({ method: 'POST', path: `${PRODUCT_PATH}/pause`, pathParams: productParams(input) }),
62
+ });
63
+
64
+ export const productsResume = defineCommand({
65
+ id: 'products.resume',
66
+ summary: 'Resume a paused product.',
67
+ kind: CommandKind.WRITE,
68
+ toolsets: TOOLSETS,
69
+ input: productInput(),
70
+ cli: PRODUCT_CLI,
71
+ run: (input, ctx) =>
72
+ ctx.http({ method: 'POST', path: `${PRODUCT_PATH}/resume`, pathParams: productParams(input) }),
73
+ });
74
+
75
+ const settingsPair = ({ id, noun, resource, getSummary, replaceSummary, replaceMethod, replaceKind, guidance }) =>
76
+ defineGroup({
77
+ id: `products.${id}`,
78
+ help: {
79
+ usage: `Usage:\n outrank-cli products ${id} ${noun} <product-id>\n outrank-cli products ${id} ${
80
+ replaceMethod === 'PATCH' ? 'update' : 'replace'
81
+ } <product-id> --json <object>`,
82
+ guidance,
83
+ },
84
+ unknown: (word) => ({
85
+ message: `Unknown products ${id} subcommand: ${word}.`,
86
+ guidance: `Run \`outrank-cli products ${id} --help\` to see the available ${resource} commands.`,
87
+ }),
88
+ children: [
89
+ defineCommand({
90
+ id: `products.${id}.${noun}`,
91
+ summary: getSummary,
92
+ kind: CommandKind.READ,
93
+ toolsets: TOOLSETS,
94
+ input: productInput(),
95
+ cli: PRODUCT_CLI,
96
+ run: (input, ctx) =>
97
+ ctx.http({ method: 'GET', path: `${PRODUCT_PATH}/${id}`, pathParams: productParams(input) }),
98
+ }),
99
+ defineCommand({
100
+ id: `products.${id}.${replaceMethod === 'PATCH' ? 'update' : 'replace'}`,
101
+ summary: replaceSummary,
102
+ kind: replaceKind,
103
+ toolsets: TOOLSETS,
104
+ input: productInput(),
105
+ cli: PRODUCT_CLI,
106
+ body: BodyMode.JSON,
107
+ run: (input, ctx) =>
108
+ ctx.http({
109
+ method: replaceMethod,
110
+ path: `${PRODUCT_PATH}/${id}`,
111
+ pathParams: productParams(input),
112
+ body: input.body,
113
+ }),
114
+ }),
115
+ ],
116
+ });
117
+
118
+ export const outputSettingsGroup = settingsPair({
119
+ id: 'output-settings',
120
+ noun: 'get',
121
+ resource: 'output-settings',
122
+ getSummary: 'Get the article output settings of a product.',
123
+ replaceSummary: 'Patch the article output settings of a product from a JSON body.',
124
+ replaceMethod: 'PATCH',
125
+ replaceKind: CommandKind.WRITE,
126
+ guidance: 'Use `get` to inspect current article settings and `update` to patch them.',
127
+ });
128
+
129
+ export const targetAudiencesGroup = settingsPair({
130
+ id: 'target-audiences',
131
+ noun: 'list',
132
+ resource: 'target-audience',
133
+ getSummary: 'List the target audiences of a product.',
134
+ replaceSummary: 'Replace the full target-audience list of a product from a JSON body.',
135
+ replaceMethod: 'PUT',
136
+ replaceKind: CommandKind.DESTRUCTIVE,
137
+ guidance: 'Use `list` to inspect the target-audience list and `replace` to submit the full replacement set.',
138
+ });
139
+
140
+ export const competitorsGroup = settingsPair({
141
+ id: 'competitors',
142
+ noun: 'list',
143
+ resource: 'competitor',
144
+ getSummary: 'List the competitors of a product.',
145
+ replaceSummary: 'Replace the full competitor list of a product from a JSON body (3-7 entries, server-enforced).',
146
+ replaceMethod: 'PUT',
147
+ replaceKind: CommandKind.DESTRUCTIVE,
148
+ guidance: 'Use `list` to inspect the competitor list and `replace` to submit the full replacement set.',
149
+ });
@@ -0,0 +1,250 @@
1
+ import {
2
+ GSC_MAX_MONTHS,
3
+ GSC_MIN_MONTHS,
4
+ GSC_SEARCH_ANALYTICS_MAX_LIMIT,
5
+ LIST_MIN_LIMIT,
6
+ } from '../../core/constants.js';
7
+ import { Surface } from '../../core/constants.js';
8
+ import { MCP_USAGE_GUIDANCE, usageError } from '../../core/errors.js';
9
+ import { int, string } from '../../core/schema.js';
10
+ import { CommandKind, Toolset, defineCommand, defineGroup } from '../registry.js';
11
+ import { PRODUCT_CLI, productInput, productParams } from './shared.js';
12
+
13
+ const GSC_USAGE = `Usage:
14
+ outrank-cli products gsc status <product-id>
15
+ outrank-cli products gsc connect <product-id>
16
+ outrank-cli products gsc poll <product-id> --attempt-id <attempt-id>
17
+ outrank-cli products gsc reconnect <product-id>
18
+ outrank-cli products gsc disconnect <product-id>
19
+ outrank-cli products gsc accounts <product-id>
20
+ outrank-cli products gsc properties <product-id> --email <email>
21
+ outrank-cli products gsc set-property <product-id> --email <email> --site-url <url>
22
+ outrank-cli products gsc performance <product-id> [--months <n>]
23
+ outrank-cli products gsc search-analytics <product-id> [--dimensions <csv>] [--limit <n>] [--months <n>] [--start-date <yyyy-mm-dd>] [--end-date <yyyy-mm-dd>] [--page <text>] [--query <text>] [--country <cc>] [--device <device>]
24
+ outrank-cli products gsc inspect-url <product-id> --url <url> [--language-code <code>]
25
+ outrank-cli products gsc cannibalization <product-id>`;
26
+
27
+ const TOOLSETS = [Toolset.GSC];
28
+ const GSC_PATH = '/products/{productId}/gsc';
29
+ const ATTEMPT_ID_REQUIRED = {
30
+ guidance: 'Run `outrank-cli products gsc poll <product-id> --attempt-id <attempt-id>` after connect starts.',
31
+ };
32
+ const EMAIL_REQUIRED = {
33
+ guidance:
34
+ 'Run `outrank-cli products gsc properties <product-id> --email <email>` to list properties for a connected ' +
35
+ 'Google account.',
36
+ };
37
+ const SET_PROPERTY_GUIDANCE =
38
+ 'Run `outrank-cli products gsc set-property <product-id> --email <email> --site-url <url>` to save the ' +
39
+ 'verified property.';
40
+ const URL_REQUIRED = {
41
+ guidance:
42
+ 'Run `outrank-cli products gsc inspect-url <product-id> --url <url>` to inspect indexing details for one page.',
43
+ };
44
+ const ATTEMPT_ID_PLACEHOLDERS = { attemptId: '<attempt-id>' };
45
+ const EMAIL_PLACEHOLDERS = { email: '<email>' };
46
+ const URL_PLACEHOLDERS = { url: '<url>' };
47
+
48
+ const simple = ({ id, summary, kind, method, suffix = '' }) =>
49
+ defineCommand({
50
+ id: `products.gsc.${id}`,
51
+ summary,
52
+ kind,
53
+ toolsets: TOOLSETS,
54
+ input: productInput(),
55
+ cli: PRODUCT_CLI,
56
+ run: (input, ctx) => ctx.http({ method, path: `${GSC_PATH}${suffix}`, pathParams: productParams(input) }),
57
+ });
58
+
59
+ const status = simple({
60
+ id: 'status',
61
+ summary: 'Show the Google Search Console connection status of a product.',
62
+ kind: CommandKind.READ,
63
+ method: 'GET',
64
+ });
65
+ const connect = simple({
66
+ id: 'connect',
67
+ summary: 'Start the Google Search Console OAuth handoff and return the connect URL.',
68
+ kind: CommandKind.WRITE,
69
+ method: 'POST',
70
+ suffix: '/connect',
71
+ });
72
+ const reconnect = simple({
73
+ id: 'reconnect',
74
+ summary: 'Restart the Google Search Console OAuth handoff for a product.',
75
+ kind: CommandKind.WRITE,
76
+ method: 'POST',
77
+ suffix: '/reconnect',
78
+ });
79
+ const disconnect = simple({
80
+ id: 'disconnect',
81
+ summary: 'Disconnect Google Search Console from a product.',
82
+ kind: CommandKind.DESTRUCTIVE,
83
+ method: 'DELETE',
84
+ });
85
+ const accounts = simple({
86
+ id: 'accounts',
87
+ summary: 'List the connected Google accounts of a product.',
88
+ kind: CommandKind.READ,
89
+ method: 'GET',
90
+ suffix: '/accounts',
91
+ });
92
+ const cannibalization = simple({
93
+ id: 'cannibalization',
94
+ summary: 'Report keyword cannibalization across the product pages.',
95
+ kind: CommandKind.READ,
96
+ method: 'GET',
97
+ suffix: '/cannibalization',
98
+ });
99
+
100
+ const poll = defineCommand({
101
+ id: 'products.gsc.poll',
102
+ summary: 'Poll a Google Search Console connect attempt.',
103
+ kind: CommandKind.READ,
104
+ toolsets: TOOLSETS,
105
+ input: { ...productInput(), attemptId: string().required(ATTEMPT_ID_REQUIRED) },
106
+ cli: { ...PRODUCT_CLI, placeholders: ATTEMPT_ID_PLACEHOLDERS },
107
+ run: (input, ctx) =>
108
+ ctx.http({
109
+ method: 'GET',
110
+ path: `${GSC_PATH}/connect`,
111
+ pathParams: productParams(input),
112
+ query: { attempt_id: input.attemptId },
113
+ }),
114
+ });
115
+
116
+ const properties = defineCommand({
117
+ id: 'products.gsc.properties',
118
+ summary: 'List the Search Console properties of a connected Google account.',
119
+ kind: CommandKind.READ,
120
+ toolsets: TOOLSETS,
121
+ input: { ...productInput(), email: string().required(EMAIL_REQUIRED) },
122
+ cli: { ...PRODUCT_CLI, placeholders: EMAIL_PLACEHOLDERS },
123
+ run: (input, ctx) =>
124
+ ctx.http({
125
+ method: 'GET',
126
+ path: `${GSC_PATH}/properties`,
127
+ pathParams: productParams(input),
128
+ query: { email: input.email },
129
+ }),
130
+ });
131
+
132
+ const setProperty = defineCommand({
133
+ id: 'products.gsc.set-property',
134
+ summary: 'Save the verified Search Console property for a product.',
135
+ kind: CommandKind.WRITE,
136
+ toolsets: TOOLSETS,
137
+ input: { ...productInput(), email: string(), siteUrl: string() },
138
+ cli: PRODUCT_CLI,
139
+ refine: (input, ctx) => {
140
+ if (input.email && input.siteUrl) return;
141
+ if (ctx.surface === Surface.MCP) {
142
+ throw usageError('Missing required arguments: email and site_url.', MCP_USAGE_GUIDANCE);
143
+ }
144
+ throw usageError('Missing required options: --email <email> and --site-url <url>.', SET_PROPERTY_GUIDANCE);
145
+ },
146
+ run: (input, ctx) =>
147
+ ctx.http({
148
+ method: 'PUT',
149
+ path: `${GSC_PATH}/property`,
150
+ pathParams: productParams(input),
151
+ body: { email: input.email, site_url: input.siteUrl },
152
+ }),
153
+ });
154
+
155
+ const performance = defineCommand({
156
+ id: 'products.gsc.performance',
157
+ summary: 'Show Search Console performance for the last months.',
158
+ kind: CommandKind.READ,
159
+ toolsets: TOOLSETS,
160
+ input: { ...productInput(), months: int({ min: GSC_MIN_MONTHS, max: GSC_MAX_MONTHS }) },
161
+ cli: PRODUCT_CLI,
162
+ run: (input, ctx) =>
163
+ ctx.http({
164
+ method: 'GET',
165
+ path: `${GSC_PATH}/performance`,
166
+ pathParams: productParams(input),
167
+ query: { months: input.months },
168
+ }),
169
+ });
170
+
171
+ const searchAnalytics = defineCommand({
172
+ id: 'products.gsc.search-analytics',
173
+ summary: 'Query Search Console search analytics with dimensions and filters.',
174
+ kind: CommandKind.READ,
175
+ toolsets: TOOLSETS,
176
+ input: {
177
+ ...productInput(),
178
+ dimensions: string(),
179
+ limit: int({ min: LIST_MIN_LIMIT, max: GSC_SEARCH_ANALYTICS_MAX_LIMIT }),
180
+ months: int({ min: GSC_MIN_MONTHS, max: GSC_MAX_MONTHS }),
181
+ startDate: string(),
182
+ endDate: string(),
183
+ page: string(),
184
+ query: string(),
185
+ country: string(),
186
+ device: string(),
187
+ },
188
+ cli: PRODUCT_CLI,
189
+ run: (input, ctx) =>
190
+ ctx.http({
191
+ method: 'GET',
192
+ path: `${GSC_PATH}/search-analytics`,
193
+ pathParams: productParams(input),
194
+ query: {
195
+ dimensions: input.dimensions,
196
+ limit: input.limit,
197
+ months: input.months,
198
+ start_date: input.startDate,
199
+ end_date: input.endDate,
200
+ page: input.page,
201
+ query: input.query,
202
+ country: input.country,
203
+ device: input.device,
204
+ },
205
+ }),
206
+ });
207
+
208
+ const inspectUrl = defineCommand({
209
+ id: 'products.gsc.inspect-url',
210
+ summary: 'Inspect the indexing status of one URL.',
211
+ kind: CommandKind.READ,
212
+ toolsets: TOOLSETS,
213
+ input: { ...productInput(), url: string().required(URL_REQUIRED), languageCode: string() },
214
+ cli: { ...PRODUCT_CLI, placeholders: URL_PLACEHOLDERS },
215
+ run: (input, ctx) =>
216
+ ctx.http({
217
+ method: 'POST',
218
+ path: `${GSC_PATH}/url-inspection`,
219
+ pathParams: productParams(input),
220
+ body: { inspection_url: input.url, ...(input.languageCode ? { language_code: input.languageCode } : {}) },
221
+ }),
222
+ });
223
+
224
+ export const gscGroup = defineGroup({
225
+ id: 'products.gsc',
226
+ help: {
227
+ usage: GSC_USAGE,
228
+ guidance:
229
+ 'Use connect plus poll for the OAuth handoff, then inspect accounts, properties, performance, indexing, ' +
230
+ 'and keyword data from the same subtree.',
231
+ },
232
+ unknown: (word) => ({
233
+ message: `Unknown products gsc subcommand: ${word}.`,
234
+ guidance: 'Run `outrank-cli products gsc --help` to see the available Google Search Console commands.',
235
+ }),
236
+ children: [
237
+ status,
238
+ connect,
239
+ poll,
240
+ reconnect,
241
+ disconnect,
242
+ accounts,
243
+ properties,
244
+ setProperty,
245
+ performance,
246
+ searchAnalytics,
247
+ inspectUrl,
248
+ cannibalization,
249
+ ],
250
+ });