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
@@ -1,301 +1,285 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- import {
5
- parseArgs,
6
- getRequiredPositional,
7
- readJsonInput,
8
- hasFlag,
9
- parseIntegerOption,
10
- getOption,
11
- } from '../core/args.js';
12
- import { requestAgentApi } from '../core/api.js';
13
- import { cliSuccess } from '../core/errors.js';
14
-
15
- export const ARTICLES_HELP = `Usage:
16
- outrank-cli articles list [--product <product-id>] [--status generated|approved|published|failed|deleted] [--title <text>] [--keyword <text>] [--content <text>] [--slug <text>] [--created-from <date>] [--created-to <date>] [--scheduled-from <date>] [--scheduled-to <date>] [--sort-by created_at|status|title] [--sort-order asc|desc] [--limit <n>] [--offset <n>]
17
- outrank-cli articles get <article-id>
18
- outrank-cli articles get-content <article-id> [--format markdown|html]
19
- outrank-cli articles update <article-id> --json <object>
20
- outrank-cli articles update-content <article-id> --json <object>
21
- outrank-cli articles replace <article-id> --json <object>
22
- outrank-cli articles bulk-replace --json <object>
23
- outrank-cli articles upload-image --file <path> [--article <article-id>] [--keyword <text>]
24
- outrank-cli articles upload-featured-image <article-id> --file <path>
25
- outrank-cli articles retry-publish <article-id>
26
- outrank-cli articles delete <article-id>
27
- outrank-cli articles precreate <keyword-id> # eligible future scheduled keyword only
28
- outrank-cli articles generate <keyword-id> # scheduled for today only
29
-
30
- JSON Input:
31
- Use one of: --json '<object>', --json-file payload.json, or --json-stdin
32
-
33
- Examples:
34
- outrank-cli articles list --product 00000000-0000-0000-0000-000000000000 --status published --limit 20
35
- outrank-cli articles get 00000000-0000-0000-0000-000000000000
36
- outrank-cli articles get-content 00000000-0000-0000-0000-000000000000 --format html
37
- outrank-cli articles update 00000000-0000-0000-0000-000000000000 --json '{"title":"New title"}'
38
- outrank-cli articles update 00000000-0000-0000-0000-000000000000 --json '{"image_url":"https://example.com/featured.jpg"}'
39
- outrank-cli articles update-content 00000000-0000-0000-0000-000000000000 --json-file article-content.json
40
- outrank-cli articles replace 00000000-0000-0000-0000-000000000000 --json '{"find":"Old brand","replace_with":"New brand","preview_only":true}'
41
- outrank-cli articles bulk-replace --json-file bulk-replace.json
42
- outrank-cli articles upload-image --file .\\diagram.png --article 00000000-0000-0000-0000-000000000000
43
- outrank-cli articles upload-featured-image 00000000-0000-0000-0000-000000000000 --file .\\hero.jpg
44
- outrank-cli articles retry-publish 00000000-0000-0000-0000-000000000000
45
- outrank-cli articles precreate 00000000-0000-0000-0000-000000000000
46
- outrank-cli articles generate 00000000-0000-0000-0000-000000000000
47
- `;
48
-
49
- const IMAGE_MIME_BY_EXTENSION = {
50
- '.jpg': 'image/jpeg',
51
- '.jpeg': 'image/jpeg',
52
- '.png': 'image/png',
53
- '.webp': 'image/webp',
54
- '.gif': 'image/gif',
55
- };
56
-
57
- const buildQueryPath = (basePath, searchParams) => {
58
- const url = new URL(`https://local${basePath}`);
59
- for (const [key, value] of Object.entries(searchParams)) {
60
- if (value !== undefined && value !== null && value !== '') {
61
- url.searchParams.set(key, String(value));
62
- }
63
- }
64
- return `${url.pathname}${url.search}`;
65
- };
66
-
67
- const resolveImageUploadPayload = async (parsed) => {
68
- const filePath = getOption(parsed, 'file');
69
- if (!filePath) {
70
- throw Object.assign(new Error('Missing required option: --file <path>.'), {
71
- cliCode: 'CLI_INVALID_USAGE',
72
- guidance: 'Pass a local image path with `--file <path>`.',
73
- });
74
- }
75
-
76
- const absolutePath = path.resolve(process.cwd(), filePath);
77
- const buffer = await fs.readFile(absolutePath).catch((error) => {
78
- throw Object.assign(new Error(`Failed to read file: ${error.message}`), {
79
- cliCode: 'CLI_INVALID_USAGE',
80
- guidance: `Confirm that ${absolutePath} exists and is readable, then retry.`,
81
- });
82
- });
83
-
84
- const fileName = path.basename(absolutePath);
85
- const extension = path.extname(fileName).toLowerCase();
86
- const fileType = IMAGE_MIME_BY_EXTENSION[extension];
87
-
88
- if (!fileType) {
89
- throw Object.assign(new Error(`Unsupported image file extension: ${extension || '(none)'}.`), {
90
- cliCode: 'CLI_INVALID_USAGE',
91
- guidance: 'Use one of: .jpg, .jpeg, .png, .webp, .gif.',
92
- });
93
- }
94
-
95
- return {
96
- file_base64: buffer.toString('base64'),
97
- file_name: fileName,
98
- file_type: fileType,
99
- };
100
- };
101
-
102
- const handleArticlesList = async (parsed) => {
103
- const path = buildQueryPath('/articles', {
104
- product_id: getOption(parsed, 'product'),
105
- status: getOption(parsed, 'status'),
106
- title: getOption(parsed, 'title'),
107
- keyword: getOption(parsed, 'keyword'),
108
- content: getOption(parsed, 'content'),
109
- slug: getOption(parsed, 'slug'),
110
- created_from: getOption(parsed, 'created-from'),
111
- created_to: getOption(parsed, 'created-to'),
112
- scheduled_from: getOption(parsed, 'scheduled-from'),
113
- scheduled_to: getOption(parsed, 'scheduled-to'),
114
- sort_by: getOption(parsed, 'sort-by'),
115
- sort_order: getOption(parsed, 'sort-order'),
116
- limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 100 }),
117
- offset: parseIntegerOption(parsed, 'offset', { min: 0 }),
118
- });
119
- const response = await requestAgentApi(parsed, {
120
- method: 'GET',
121
- path,
122
- });
123
- return response.payload;
124
- };
125
-
126
- const handleArticlesGet = async (parsed) => {
127
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
128
- const response = await requestAgentApi(parsed, {
129
- method: 'GET',
130
- path: `/articles/${articleId}`,
131
- });
132
- return response.payload;
133
- };
134
-
135
- const handleArticlesGetContent = async (parsed) => {
136
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
137
- const path = buildQueryPath(`/articles/${articleId}/content`, {
138
- format: getOption(parsed, 'format'),
139
- });
140
- const response = await requestAgentApi(parsed, {
141
- method: 'GET',
142
- path,
143
- });
144
- return response.payload;
145
- };
146
-
147
- const handleArticlesUpdate = async (parsed) => {
148
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
149
- const body = await readJsonInput(parsed);
150
- const response = await requestAgentApi(parsed, {
151
- method: 'PATCH',
152
- path: `/articles/${articleId}`,
153
- body,
154
- });
155
- return response.payload;
156
- };
157
-
158
- const handleArticlesUpdateContent = async (parsed) => {
159
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
160
- const body = await readJsonInput(parsed);
161
- const response = await requestAgentApi(parsed, {
162
- method: 'PUT',
163
- path: `/articles/${articleId}/content`,
164
- body,
165
- });
166
- return response.payload;
167
- };
168
-
169
- const handleArticlesReplace = async (parsed) => {
170
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
171
- const body = await readJsonInput(parsed);
172
- const response = await requestAgentApi(parsed, {
173
- method: 'POST',
174
- path: `/articles/${articleId}/replace`,
175
- body,
176
- });
177
- return response.payload;
178
- };
179
-
180
- const handleArticlesBulkReplace = async (parsed) => {
181
- const body = await readJsonInput(parsed);
182
- const response = await requestAgentApi(parsed, {
183
- method: 'POST',
184
- path: '/articles/bulk-replace',
185
- body,
186
- });
187
- return response.payload;
188
- };
189
-
190
- const handleArticlesUploadImage = async (parsed) => {
191
- const imagePayload = await resolveImageUploadPayload(parsed);
192
- const articleId = getOption(parsed, 'article');
193
- const keyword = getOption(parsed, 'keyword');
194
-
195
- if (!articleId && !keyword) {
196
- throw Object.assign(new Error('Provide at least one of: --article <article-id> or --keyword <text>.'), {
197
- cliCode: 'CLI_INVALID_USAGE',
198
- guidance:
199
- 'Pass `--article <article-id>` for an existing article editor-style upload, or `--keyword <text>` when you want keyword-driven image metadata without an existing article.',
200
- });
201
- }
202
-
203
- const response = await requestAgentApi(parsed, {
204
- method: 'POST',
205
- path: '/articles/images/upload',
206
- body: {
207
- ...imagePayload,
208
- ...(articleId ? { article_id: articleId } : {}),
209
- ...(keyword ? { keyword } : {}),
210
- },
211
- });
212
- return response.payload;
213
- };
214
-
215
- const handleArticlesUploadFeaturedImage = async (parsed) => {
216
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
217
- const imagePayload = await resolveImageUploadPayload(parsed);
218
- const response = await requestAgentApi(parsed, {
219
- method: 'POST',
220
- path: `/articles/${articleId}/featured-image/upload`,
221
- body: imagePayload,
222
- });
223
- return response.payload;
224
- };
225
-
226
- const handleArticlesRetryPublish = async (parsed) => {
227
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
228
- const response = await requestAgentApi(parsed, {
229
- method: 'POST',
230
- path: `/articles/${articleId}/retry-publish`,
231
- });
232
- return response.payload;
233
- };
234
-
235
- const handleArticlesDelete = async (parsed) => {
236
- const articleId = getRequiredPositional(parsed, 0, '<article-id>');
237
- const response = await requestAgentApi(parsed, {
238
- method: 'DELETE',
239
- path: `/articles/${articleId}`,
240
- });
241
- return response.payload;
242
- };
243
-
244
- const handleArticlesPrecreate = async (parsed) => {
245
- const keywordId = getRequiredPositional(parsed, 0, '<keyword-id>');
246
- const response = await requestAgentApi(parsed, {
247
- method: 'POST',
248
- path: '/articles/precreate',
249
- body: {
250
- keyword_id: keywordId,
251
- },
252
- });
253
- return response.payload;
254
- };
255
-
256
- const handleArticlesGenerate = async (parsed) => {
257
- const keywordId = getRequiredPositional(parsed, 0, '<keyword-id>');
258
- const response = await requestAgentApi(parsed, {
259
- method: 'POST',
260
- path: '/articles/generate',
261
- body: {
262
- keyword_id: keywordId,
263
- },
264
- });
265
- return response.payload;
266
- };
267
-
268
- export const handleArticlesCommand = async (argv) => {
269
- const subcommand = argv[0];
270
- const parsed = parseArgs(argv.slice(1));
271
- const wantsHelp = !subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
272
-
273
- if (wantsHelp) {
274
- return cliSuccess(
275
- {
276
- usage: ARTICLES_HELP.trim(),
277
- },
278
- 'Choose one of: list, get, get-content, update, update-content, replace, bulk-replace, upload-image, upload-featured-image, retry-publish, delete, precreate, generate.',
279
- );
280
- }
281
-
282
- if (subcommand === 'list') return handleArticlesList(parsed);
283
- if (subcommand === 'get') return handleArticlesGet(parsed);
284
- if (subcommand === 'get-content') return handleArticlesGetContent(parsed);
285
- if (subcommand === 'update') return handleArticlesUpdate(parsed);
286
- if (subcommand === 'update-content') return handleArticlesUpdateContent(parsed);
287
- if (subcommand === 'replace') return handleArticlesReplace(parsed);
288
- if (subcommand === 'bulk-replace') return handleArticlesBulkReplace(parsed);
289
- if (subcommand === 'upload-image') return handleArticlesUploadImage(parsed);
290
- if (subcommand === 'upload-featured-image') return handleArticlesUploadFeaturedImage(parsed);
291
- if (subcommand === 'retry-publish') return handleArticlesRetryPublish(parsed);
292
- if (subcommand === 'delete') return handleArticlesDelete(parsed);
293
- if (subcommand === 'precreate') return handleArticlesPrecreate(parsed);
294
- if (subcommand === 'generate') return handleArticlesGenerate(parsed);
295
-
296
- throw Object.assign(new Error(`Unknown articles subcommand: ${subcommand}.`), {
297
- cliCode: 'CLI_INVALID_USAGE',
298
- guidance: 'Run `outrank-cli articles --help` to see available article commands.',
299
- });
300
- };
301
-
1
+ import { LIST_MAX_LIMIT, LIST_MIN_LIMIT, MIN_OFFSET, Surface } from '../core/constants.js';
2
+ import { MCP_USAGE_GUIDANCE, usageError } from '../core/errors.js';
3
+ import { image, int, string, uuid } from '../core/schema.js';
4
+ import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
5
+
6
+ export const ARTICLES_HELP = `Usage:
7
+ outrank-cli articles list [--product <product-id>] [--status generated|approved|published|failed|deleted] [--title <text>] [--keyword <text>] [--content <text>] [--slug <text>] [--created-from <date>] [--created-to <date>] [--scheduled-from <date>] [--scheduled-to <date>] [--sort-by created_at|status|title] [--sort-order asc|desc] [--limit <n>] [--offset <n>]
8
+ outrank-cli articles get <article-id>
9
+ outrank-cli articles get-content <article-id> [--format markdown|html]
10
+ outrank-cli articles update <article-id> --json <object>
11
+ outrank-cli articles update-content <article-id> --json <object>
12
+ outrank-cli articles replace <article-id> --json <object>
13
+ outrank-cli articles bulk-replace --json <object>
14
+ outrank-cli articles upload-image --file <path> [--article <article-id>] [--keyword <text>]
15
+ outrank-cli articles upload-featured-image <article-id> --file <path>
16
+ outrank-cli articles retry-publish <article-id>
17
+ outrank-cli articles delete <article-id>
18
+ outrank-cli articles precreate <keyword-id> # eligible future scheduled keyword only
19
+ outrank-cli articles generate <keyword-id> # scheduled for today only
20
+
21
+ JSON Input:
22
+ Use one of: --json '<object>', --json-file payload.json, or --json-stdin
23
+
24
+ Examples:
25
+ outrank-cli articles list --product 00000000-0000-0000-0000-000000000000 --status published --limit 20
26
+ outrank-cli articles get 00000000-0000-0000-0000-000000000000
27
+ outrank-cli articles get-content 00000000-0000-0000-0000-000000000000 --format html
28
+ outrank-cli articles update 00000000-0000-0000-0000-000000000000 --json '{"title":"New title"}'
29
+ outrank-cli articles update 00000000-0000-0000-0000-000000000000 --json '{"image_url":"https://example.com/featured.jpg"}'
30
+ outrank-cli articles update-content 00000000-0000-0000-0000-000000000000 --json-file article-content.json
31
+ outrank-cli articles replace 00000000-0000-0000-0000-000000000000 --json '{"find":"Old brand","replace_with":"New brand","preview_only":true}'
32
+ outrank-cli articles bulk-replace --json-file bulk-replace.json
33
+ outrank-cli articles upload-image --file .\\diagram.png --article 00000000-0000-0000-0000-000000000000
34
+ outrank-cli articles upload-featured-image 00000000-0000-0000-0000-000000000000 --file .\\hero.jpg
35
+ outrank-cli articles retry-publish 00000000-0000-0000-0000-000000000000
36
+ outrank-cli articles precreate 00000000-0000-0000-0000-000000000000
37
+ outrank-cli articles generate 00000000-0000-0000-0000-000000000000
38
+ `;
39
+
40
+ const TOOLSETS = [Toolset.ARTICLES];
41
+ const ARTICLE_PATH = '/articles/{articleId}';
42
+ const ARTICLE_ID_CLI = { positionals: ['articleId'] };
43
+ const KEYWORD_ID_CLI = { positionals: ['keywordId'] };
44
+ const FILE_PLACEHOLDERS = { file: '<path>' };
45
+ const FILE_REQUIRED = { guidance: 'Pass a local image path with `--file <path>`.' };
46
+ const UPLOAD_TARGET_GUIDANCE =
47
+ 'Pass `--article <article-id>` for an existing article editor-style upload, or `--keyword <text>` when you ' +
48
+ 'want keyword-driven image metadata without an existing article.';
49
+
50
+ const articleIdInput = () => ({ articleId: uuid().required() });
51
+ const articleIdParams = (input) => ({ articleId: input.articleId });
52
+
53
+ const listCommand = defineCommand({
54
+ id: 'articles.list',
55
+ summary: 'List articles with optional filters, sorting and pagination.',
56
+ kind: CommandKind.READ,
57
+ toolsets: TOOLSETS,
58
+ input: {
59
+ productId: uuid(),
60
+ status: string(),
61
+ title: string(),
62
+ keyword: string(),
63
+ content: string(),
64
+ slug: string(),
65
+ createdFrom: string(),
66
+ createdTo: string(),
67
+ scheduledFrom: string(),
68
+ scheduledTo: string(),
69
+ sortBy: string(),
70
+ sortOrder: string(),
71
+ limit: int({ min: LIST_MIN_LIMIT, max: LIST_MAX_LIMIT }),
72
+ offset: int({ min: MIN_OFFSET }),
73
+ },
74
+ cli: { options: { product: 'productId' } },
75
+ run: (input, ctx) =>
76
+ ctx.http({
77
+ method: 'GET',
78
+ path: '/articles',
79
+ query: {
80
+ product_id: input.productId,
81
+ status: input.status,
82
+ title: input.title,
83
+ keyword: input.keyword,
84
+ content: input.content,
85
+ slug: input.slug,
86
+ created_from: input.createdFrom,
87
+ created_to: input.createdTo,
88
+ scheduled_from: input.scheduledFrom,
89
+ scheduled_to: input.scheduledTo,
90
+ sort_by: input.sortBy,
91
+ sort_order: input.sortOrder,
92
+ limit: input.limit,
93
+ offset: input.offset,
94
+ },
95
+ }),
96
+ });
97
+
98
+ const getCommand = defineCommand({
99
+ id: 'articles.get',
100
+ summary: 'Get article metadata by id.',
101
+ kind: CommandKind.READ,
102
+ toolsets: TOOLSETS,
103
+ input: articleIdInput(),
104
+ cli: ARTICLE_ID_CLI,
105
+ run: (input, ctx) => ctx.http({ method: 'GET', path: ARTICLE_PATH, pathParams: articleIdParams(input) }),
106
+ });
107
+
108
+ const getContentCommand = defineCommand({
109
+ id: 'articles.get-content',
110
+ summary: 'Get the article body as markdown or HTML.',
111
+ kind: CommandKind.READ,
112
+ toolsets: TOOLSETS,
113
+ input: { ...articleIdInput(), format: string() },
114
+ cli: ARTICLE_ID_CLI,
115
+ run: (input, ctx) =>
116
+ ctx.http({
117
+ method: 'GET',
118
+ path: `${ARTICLE_PATH}/content`,
119
+ pathParams: articleIdParams(input),
120
+ query: { format: input.format },
121
+ }),
122
+ });
123
+
124
+ const bodyCommand = ({ id, summary, kind, method, pathSuffix }) =>
125
+ defineCommand({
126
+ id,
127
+ summary,
128
+ kind,
129
+ toolsets: TOOLSETS,
130
+ input: articleIdInput(),
131
+ cli: ARTICLE_ID_CLI,
132
+ body: BodyMode.JSON,
133
+ run: (input, ctx) =>
134
+ ctx.http({ method, path: `${ARTICLE_PATH}${pathSuffix}`, pathParams: articleIdParams(input), body: input.body }),
135
+ });
136
+
137
+ const updateCommand = bodyCommand({
138
+ id: 'articles.update',
139
+ summary: 'Update article metadata from a JSON body.',
140
+ kind: CommandKind.WRITE,
141
+ method: 'PATCH',
142
+ pathSuffix: '',
143
+ });
144
+
145
+ const updateContentCommand = bodyCommand({
146
+ id: 'articles.update-content',
147
+ summary: 'Replace the article body from a JSON body.',
148
+ kind: CommandKind.WRITE,
149
+ method: 'PUT',
150
+ pathSuffix: '/content',
151
+ });
152
+
153
+ const replaceCommand = bodyCommand({
154
+ id: 'articles.replace',
155
+ summary: 'Find and replace text inside one article.',
156
+ kind: CommandKind.WRITE,
157
+ method: 'POST',
158
+ pathSuffix: '/replace',
159
+ });
160
+
161
+ const bulkReplaceCommand = defineCommand({
162
+ id: 'articles.bulk-replace',
163
+ summary: 'Find and replace text across several articles from a JSON body.',
164
+ kind: CommandKind.WRITE,
165
+ toolsets: TOOLSETS,
166
+ body: BodyMode.JSON,
167
+ run: (input, ctx) => ctx.http({ method: 'POST', path: '/articles/bulk-replace', body: input.body }),
168
+ });
169
+
170
+ const uploadImageCommand = defineCommand({
171
+ id: 'articles.upload-image',
172
+ summary: 'Upload an image for an article or a keyword.',
173
+ kind: CommandKind.WRITE,
174
+ toolsets: TOOLSETS,
175
+ input: { file: image().required(FILE_REQUIRED), articleId: uuid(), keyword: string() },
176
+ cli: { options: { article: 'articleId' }, placeholders: FILE_PLACEHOLDERS },
177
+ refine: (input, ctx) => {
178
+ if (input.articleId || input.keyword) return;
179
+ if (ctx.surface === Surface.MCP) {
180
+ throw usageError('Provide at least one of: article_id or keyword.', MCP_USAGE_GUIDANCE);
181
+ }
182
+ throw usageError('Provide at least one of: --article <article-id> or --keyword <text>.', UPLOAD_TARGET_GUIDANCE);
183
+ },
184
+ run: (input, ctx) =>
185
+ ctx.http({
186
+ method: 'POST',
187
+ path: '/articles/images/upload',
188
+ body: {
189
+ ...input.file,
190
+ ...(input.articleId ? { article_id: input.articleId } : {}),
191
+ ...(input.keyword ? { keyword: input.keyword } : {}),
192
+ },
193
+ }),
194
+ });
195
+
196
+ const uploadFeaturedImageCommand = defineCommand({
197
+ id: 'articles.upload-featured-image',
198
+ summary: 'Upload the featured image of an article.',
199
+ kind: CommandKind.WRITE,
200
+ toolsets: TOOLSETS,
201
+ input: { ...articleIdInput(), file: image().required(FILE_REQUIRED) },
202
+ cli: { ...ARTICLE_ID_CLI, placeholders: FILE_PLACEHOLDERS },
203
+ run: (input, ctx) =>
204
+ ctx.http({
205
+ method: 'POST',
206
+ path: `${ARTICLE_PATH}/featured-image/upload`,
207
+ pathParams: articleIdParams(input),
208
+ body: input.file,
209
+ }),
210
+ });
211
+
212
+ const retryPublishCommand = defineCommand({
213
+ id: 'articles.retry-publish',
214
+ summary: 'Retry publishing an article to its integration.',
215
+ kind: CommandKind.WRITE,
216
+ toolsets: TOOLSETS,
217
+ input: articleIdInput(),
218
+ cli: ARTICLE_ID_CLI,
219
+ longRunning: { pollTool: 'articles.get' },
220
+ run: (input, ctx) =>
221
+ ctx.http({ method: 'POST', path: `${ARTICLE_PATH}/retry-publish`, pathParams: articleIdParams(input) }),
222
+ });
223
+
224
+ const deleteCommand = defineCommand({
225
+ id: 'articles.delete',
226
+ summary: 'Delete an article.',
227
+ kind: CommandKind.DESTRUCTIVE,
228
+ toolsets: TOOLSETS,
229
+ input: articleIdInput(),
230
+ cli: ARTICLE_ID_CLI,
231
+ run: (input, ctx) => ctx.http({ method: 'DELETE', path: ARTICLE_PATH, pathParams: articleIdParams(input) }),
232
+ });
233
+
234
+ const keywordActionCommand = ({ id, summary, actionPath }) =>
235
+ defineCommand({
236
+ id,
237
+ summary,
238
+ kind: CommandKind.WRITE,
239
+ toolsets: TOOLSETS,
240
+ input: { keywordId: uuid().required() },
241
+ cli: KEYWORD_ID_CLI,
242
+ longRunning: { pollTool: 'articles.list' },
243
+ run: (input, ctx) => ctx.http({ method: 'POST', path: actionPath, body: { keyword_id: input.keywordId } }),
244
+ });
245
+
246
+ const precreateCommand = keywordActionCommand({
247
+ id: 'articles.precreate',
248
+ summary: 'Pre-create the article for an eligible future scheduled keyword.',
249
+ actionPath: '/articles/precreate',
250
+ });
251
+
252
+ const generateCommand = keywordActionCommand({
253
+ id: 'articles.generate',
254
+ summary: 'Generate the article for a keyword scheduled for today.',
255
+ actionPath: '/articles/generate',
256
+ });
257
+
258
+ export const articlesGroup = defineGroup({
259
+ id: 'articles',
260
+ help: {
261
+ usage: ARTICLES_HELP,
262
+ guidance:
263
+ 'Choose one of: list, get, get-content, update, update-content, replace, bulk-replace, upload-image, ' +
264
+ 'upload-featured-image, retry-publish, delete, precreate, generate.',
265
+ },
266
+ unknown: (word) => ({
267
+ message: `Unknown articles subcommand: ${word}.`,
268
+ guidance: 'Run `outrank-cli articles --help` to see available article commands.',
269
+ }),
270
+ children: [
271
+ listCommand,
272
+ getCommand,
273
+ getContentCommand,
274
+ updateCommand,
275
+ updateContentCommand,
276
+ replaceCommand,
277
+ bulkReplaceCommand,
278
+ uploadImageCommand,
279
+ uploadFeaturedImageCommand,
280
+ retryPublishCommand,
281
+ deleteCommand,
282
+ precreateCommand,
283
+ generateCommand,
284
+ ],
285
+ });