outrank-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ import {
2
+ parseArgs,
3
+ getRequiredPositional,
4
+ readJsonInput,
5
+ hasFlag,
6
+ parseIntegerOption,
7
+ getOption,
8
+ getOptions,
9
+ } from '../core/args.js';
10
+ import { requestAgentApi } from '../core/api.js';
11
+ import { cliSuccess } from '../core/errors.js';
12
+
13
+ export const KEYWORDS_HELP = `Usage:
14
+ outrank-cli keywords list --product <product-id> [--scope scheduled|stored|all] [--status published|scheduled|generating_article|pregenerated|disabled] [--q <text>] [--article-type <value>] [--article-subtype <value>] [--scheduled-from <date>] [--scheduled-to <date>] [--search-volume-min <n>] [--search-volume-max <n>] [--difficulty-min <n>] [--difficulty-max <n>] [--limit <n>] [--offset <n>]
15
+ outrank-cli keywords get <keyword-id> [--kind scheduled|stored]
16
+ outrank-cli keywords export --product <product-id>
17
+ outrank-cli keywords generation-status --product <product-id>
18
+ outrank-cli keywords add --product <product-id> --json <object>
19
+ outrank-cli keywords suggest --product <product-id> (--keyword <text> | --theme <text>) [--keyword-region <code>] [--limit <n>]
20
+ outrank-cli keywords generate --product <product-id> --count <n> [--source competitors|audiences|both] [--competitor <competitor-id> ...] [--audience <text> ...]
21
+ outrank-cli keywords bulk-process --product <product-id> --json <object> [--keyword-region <code>]
22
+ outrank-cli keywords reschedule <keyword-id> --scheduled-for <date>
23
+ outrank-cli keywords bulk-reschedule --json <object>
24
+ outrank-cli keywords delete <keyword-id>
25
+ outrank-cli keywords bulk-delete --json <object>
26
+
27
+ JSON Input:
28
+ Use one of: --json '<object>', --json-file payload.json, or --json-stdin
29
+
30
+ Examples:
31
+ outrank-cli keywords list --product 00000000-0000-0000-0000-000000000000 --scope all
32
+ outrank-cli keywords export --product 00000000-0000-0000-0000-000000000000
33
+ outrank-cli keywords generation-status --product 00000000-0000-0000-0000-000000000000
34
+ outrank-cli keywords add --product 00000000-0000-0000-0000-000000000000 --json-file keywords.json
35
+ outrank-cli keywords suggest --product 00000000-0000-0000-0000-000000000000 --theme "email marketing" --limit 8
36
+ outrank-cli keywords generate --product 00000000-0000-0000-0000-000000000000 --count 10 --source both
37
+ outrank-cli keywords generate --product 00000000-0000-0000-0000-000000000000 --count 5 --source competitors --competitor 11111111-1111-1111-1111-111111111111
38
+ outrank-cli keywords bulk-process --product 00000000-0000-0000-0000-000000000000 --json '{"keywords":["keyword one","keyword two"]}'
39
+ outrank-cli keywords reschedule 00000000-0000-0000-0000-000000000000 --scheduled-for 2026-05-20
40
+ outrank-cli keywords bulk-reschedule --json-file keyword-reschedule.json
41
+ outrank-cli keywords delete 00000000-0000-0000-0000-000000000000
42
+ `;
43
+
44
+ const buildQueryPath = (basePath, searchParams) => {
45
+ const url = new URL(`https://local${basePath}`);
46
+ for (const [key, value] of Object.entries(searchParams)) {
47
+ if (value !== undefined && value !== null && value !== '') {
48
+ url.searchParams.set(key, String(value));
49
+ }
50
+ }
51
+ return `${url.pathname}${url.search}`;
52
+ };
53
+
54
+ const getRequiredProductOption = (parsed) => {
55
+ const productId = getOption(parsed, 'product');
56
+ if (!productId) {
57
+ throw Object.assign(new Error('Missing required option: --product <product-id>.'), {
58
+ cliCode: 'CLI_INVALID_USAGE',
59
+ guidance: 'Pass the target product UUID with `--product <product-id>`. Use `outrank-cli products list` if you need to look it up first.',
60
+ });
61
+ }
62
+ return productId;
63
+ };
64
+
65
+ const handleKeywordsList = async (parsed) => {
66
+ const path = buildQueryPath('/keywords', {
67
+ product_id: getRequiredProductOption(parsed),
68
+ scope: getOption(parsed, 'scope'),
69
+ status: getOption(parsed, 'status'),
70
+ q: getOption(parsed, 'q'),
71
+ article_type: getOption(parsed, 'article-type'),
72
+ article_subtype: getOption(parsed, 'article-subtype'),
73
+ scheduled_from: getOption(parsed, 'scheduled-from'),
74
+ scheduled_to: getOption(parsed, 'scheduled-to'),
75
+ search_volume_min: parseIntegerOption(parsed, 'search-volume-min', { min: 0 }),
76
+ search_volume_max: parseIntegerOption(parsed, 'search-volume-max', { min: 0 }),
77
+ difficulty_min: parseIntegerOption(parsed, 'difficulty-min', { min: 0, max: 100 }),
78
+ difficulty_max: parseIntegerOption(parsed, 'difficulty-max', { min: 0, max: 100 }),
79
+ limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 100 }),
80
+ offset: parseIntegerOption(parsed, 'offset', { min: 0 }),
81
+ });
82
+ const response = await requestAgentApi(parsed, {
83
+ method: 'GET',
84
+ path,
85
+ });
86
+ return response.payload;
87
+ };
88
+
89
+ const handleKeywordsGet = async (parsed) => {
90
+ const keywordId = getRequiredPositional(parsed, 0, '<keyword-id>');
91
+ const path = buildQueryPath(`/keywords/${keywordId}`, {
92
+ kind: getOption(parsed, 'kind'),
93
+ });
94
+ const response = await requestAgentApi(parsed, {
95
+ method: 'GET',
96
+ path,
97
+ });
98
+ return response.payload;
99
+ };
100
+
101
+ const handleKeywordsExport = async (parsed) => {
102
+ const path = buildQueryPath('/keywords/export', {
103
+ product_id: getRequiredProductOption(parsed),
104
+ });
105
+ const response = await requestAgentApi(parsed, {
106
+ method: 'GET',
107
+ path,
108
+ });
109
+ return response.payload;
110
+ };
111
+
112
+ const handleKeywordsGenerationStatus = async (parsed) => {
113
+ const path = buildQueryPath('/keywords/generation-status', {
114
+ product_id: getRequiredProductOption(parsed),
115
+ });
116
+ const response = await requestAgentApi(parsed, {
117
+ method: 'GET',
118
+ path,
119
+ });
120
+ return response.payload;
121
+ };
122
+
123
+ const handleKeywordsAdd = async (parsed) => {
124
+ const body = await readJsonInput(parsed);
125
+ body.product_id = getRequiredProductOption(parsed);
126
+ const response = await requestAgentApi(parsed, {
127
+ method: 'POST',
128
+ path: '/keywords',
129
+ body,
130
+ });
131
+ return response.payload;
132
+ };
133
+
134
+ const handleKeywordsSuggest = async (parsed) => {
135
+ const keyword = getOption(parsed, 'keyword');
136
+ const theme = getOption(parsed, 'theme');
137
+ if (!keyword && !theme) {
138
+ throw Object.assign(new Error('Missing required option: --keyword <text> or --theme <text>.'), {
139
+ cliCode: 'CLI_INVALID_USAGE',
140
+ guidance: 'Pass either `--keyword <text>` or `--theme <text>` when requesting keyword suggestions.',
141
+ });
142
+ }
143
+
144
+ const body = {
145
+ product_id: getRequiredProductOption(parsed),
146
+ ...(keyword ? { keyword } : {}),
147
+ ...(theme ? { theme } : {}),
148
+ ...(getOption(parsed, 'keyword-region') ? { keyword_region: getOption(parsed, 'keyword-region') } : {}),
149
+ ...(parseIntegerOption(parsed, 'limit', { min: 1, max: 20 }) !== undefined
150
+ ? { limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 20 }) }
151
+ : {}),
152
+ };
153
+
154
+ const response = await requestAgentApi(parsed, {
155
+ method: 'POST',
156
+ path: '/keywords/suggest',
157
+ body,
158
+ });
159
+ return response.payload;
160
+ };
161
+
162
+ const handleKeywordsGenerate = async (parsed) => {
163
+ if (getOption(parsed, 'theme') || getOption(parsed, 'country')) {
164
+ throw Object.assign(
165
+ new Error('The generate command no longer accepts --theme or --country.'),
166
+ {
167
+ cliCode: 'CLI_INVALID_USAGE',
168
+ guidance:
169
+ 'Use `--count`, `--source`, repeated `--competitor`, and repeated `--audience` to match the dashboard Generate Keywords flow.',
170
+ },
171
+ );
172
+ }
173
+
174
+ const source = getOption(parsed, 'source') || 'both';
175
+ if (!['competitors', 'audiences', 'both'].includes(source)) {
176
+ throw Object.assign(new Error('--source must be one of: competitors, audiences, both.'), {
177
+ cliCode: 'CLI_INVALID_USAGE',
178
+ guidance:
179
+ 'Choose a dashboard generation source with `--source competitors`, `--source audiences`, or `--source both`.',
180
+ });
181
+ }
182
+
183
+ const competitors = getOptions(parsed, 'competitor');
184
+ const audiences = getOptions(parsed, 'audience');
185
+ const count = parseIntegerOption(parsed, 'count', { min: 5, max: 30 });
186
+
187
+ if (count === undefined) {
188
+ throw Object.assign(new Error('Missing required option: --count <n>.'), {
189
+ cliCode: 'CLI_INVALID_USAGE',
190
+ guidance:
191
+ 'Ask the human how many keywords to generate first. The dashboard-equivalent flow allows 5-30 keywords when enough scheduler slots are free. Use `outrank-cli keywords generation-status --product <product-id>` to inspect current slot capacity.',
192
+ });
193
+ }
194
+
195
+ const body = {
196
+ product_id: getRequiredProductOption(parsed),
197
+ keywords_count: count,
198
+ filters: {
199
+ isGenerateByCompetitors: source === 'competitors' || source === 'both',
200
+ isGenerateByAudiences: source === 'audiences' || source === 'both',
201
+ ...(competitors.length > 0 ? { competitors } : {}),
202
+ ...(audiences.length > 0 ? { target_audiences: audiences } : {}),
203
+ },
204
+ };
205
+
206
+ const response = await requestAgentApi(parsed, {
207
+ method: 'POST',
208
+ path: '/keywords/generate',
209
+ body,
210
+ });
211
+ return response.payload;
212
+ };
213
+
214
+ const handleKeywordsBulkProcess = async (parsed) => {
215
+ const body = await readJsonInput(parsed);
216
+ body.product_id = getRequiredProductOption(parsed);
217
+
218
+ const keywordRegion = getOption(parsed, 'keyword-region');
219
+ if (keywordRegion) {
220
+ body.keyword_region = keywordRegion;
221
+ }
222
+
223
+ const response = await requestAgentApi(parsed, {
224
+ method: 'POST',
225
+ path: '/keywords/bulk-process',
226
+ body,
227
+ });
228
+ return response.payload;
229
+ };
230
+
231
+ const handleKeywordsReschedule = async (parsed) => {
232
+ const keywordId = getRequiredPositional(parsed, 0, '<keyword-id>');
233
+ const scheduledFor = getOption(parsed, 'scheduled-for');
234
+ if (!scheduledFor) {
235
+ throw Object.assign(new Error('Missing required option: --scheduled-for <date>.'), {
236
+ cliCode: 'CLI_INVALID_USAGE',
237
+ guidance: 'Pass the target schedule date with `--scheduled-for YYYY-MM-DD` or an ISO timestamp.',
238
+ });
239
+ }
240
+
241
+ const response = await requestAgentApi(parsed, {
242
+ method: 'PATCH',
243
+ path: `/keywords/${keywordId}`,
244
+ body: {
245
+ scheduled_for: scheduledFor,
246
+ },
247
+ });
248
+ return response.payload;
249
+ };
250
+
251
+ const handleKeywordsBulkReschedule = async (parsed) => {
252
+ const body = await readJsonInput(parsed);
253
+ const response = await requestAgentApi(parsed, {
254
+ method: 'POST',
255
+ path: '/keywords/bulk-reschedule',
256
+ body,
257
+ });
258
+ return response.payload;
259
+ };
260
+
261
+ const handleKeywordsDelete = async (parsed) => {
262
+ const keywordId = getRequiredPositional(parsed, 0, '<keyword-id>');
263
+ const response = await requestAgentApi(parsed, {
264
+ method: 'DELETE',
265
+ path: `/keywords/${keywordId}`,
266
+ });
267
+ return response.payload;
268
+ };
269
+
270
+ const handleKeywordsBulkDelete = async (parsed) => {
271
+ const body = await readJsonInput(parsed);
272
+ const response = await requestAgentApi(parsed, {
273
+ method: 'POST',
274
+ path: '/keywords/bulk-delete',
275
+ body,
276
+ });
277
+ return response.payload;
278
+ };
279
+
280
+ export const handleKeywordsCommand = async (argv) => {
281
+ const subcommand = argv[0];
282
+ const parsed = parseArgs(argv.slice(1));
283
+ const wantsHelp = !subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
284
+
285
+ if (wantsHelp) {
286
+ return cliSuccess(
287
+ {
288
+ usage: KEYWORDS_HELP.trim(),
289
+ },
290
+ 'Choose one of: list, get, export, generation-status, add, suggest, generate, bulk-process, reschedule, bulk-reschedule, delete, bulk-delete.',
291
+ );
292
+ }
293
+
294
+ if (subcommand === 'list') return handleKeywordsList(parsed);
295
+ if (subcommand === 'get') return handleKeywordsGet(parsed);
296
+ if (subcommand === 'export') return handleKeywordsExport(parsed);
297
+ if (subcommand === 'generation-status') return handleKeywordsGenerationStatus(parsed);
298
+ if (subcommand === 'add') return handleKeywordsAdd(parsed);
299
+ if (subcommand === 'suggest') return handleKeywordsSuggest(parsed);
300
+ if (subcommand === 'generate') return handleKeywordsGenerate(parsed);
301
+ if (subcommand === 'bulk-process') return handleKeywordsBulkProcess(parsed);
302
+ if (subcommand === 'reschedule') return handleKeywordsReschedule(parsed);
303
+ if (subcommand === 'bulk-reschedule') return handleKeywordsBulkReschedule(parsed);
304
+ if (subcommand === 'delete') return handleKeywordsDelete(parsed);
305
+ if (subcommand === 'bulk-delete') return handleKeywordsBulkDelete(parsed);
306
+
307
+ throw Object.assign(new Error(`Unknown keywords subcommand: ${subcommand}.`), {
308
+ cliCode: 'CLI_INVALID_USAGE',
309
+ guidance: 'Run `outrank-cli keywords --help` to see available keyword commands.',
310
+ });
311
+ };