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,6 +1,6 @@
1
- import { parseArgs, getRequiredPositional, readJsonInput, hasFlag, parseIntegerOption, getOption } from '../core/args.js';
2
- import { requestAgentApi } from '../core/api.js';
3
- import { cliSuccess } from '../core/errors.js';
1
+ import { IMPROVEMENTS_CANDIDATES_MAX_LIMIT, LIST_MAX_LIMIT, LIST_MIN_LIMIT, MIN_OFFSET } from '../core/constants.js';
2
+ import { BoolSerialize, bool, int, string, uuid } from '../core/schema.js';
3
+ import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
4
4
 
5
5
  export const IMPROVEMENTS_HELP = `Usage:
6
6
  outrank-cli improvements settings get <product-id>
@@ -46,232 +46,340 @@ Examples:
46
46
  outrank-cli improvements executions push 00000000-0000-0000-0000-000000000000 --json '{"decisions":{"para-3":"rejected"}}'
47
47
  `;
48
48
 
49
- const invalidUsage = (message, guidance) =>
50
- Object.assign(new Error(message), {
51
- cliCode: 'CLI_INVALID_USAGE',
52
- guidance: guidance || 'Run `outrank-cli improvements --help` to see available improvement commands.',
53
- });
49
+ const SETTINGS_HELP = `Usage:
50
+ outrank-cli improvements settings get <product-id>
51
+ outrank-cli improvements settings update <product-id> --json <object>
52
+ `;
53
+ const CANDIDATES_HELP = `Usage:
54
+ outrank-cli improvements candidates list <product-id> [--type outsider|ctr_gap|declining|content_gap|thin_content] [--limit <n>] [--offset <n>]
55
+ outrank-cli improvements candidates brief <candidate-id> [--task <task-id>]
56
+ outrank-cli improvements candidates edit-brief <candidate-id> --json <object>
57
+ outrank-cli improvements candidates schedule <candidate-id> --scheduled-for <iso-date>
58
+ `;
59
+ const TASKS_HELP = `Usage:
60
+ outrank-cli improvements tasks list <product-id>
61
+ outrank-cli improvements tasks reschedule <task-id> --scheduled-for <iso-date>
62
+ outrank-cli improvements tasks cancel <task-id>
63
+ outrank-cli improvements tasks edit-brief <task-id> --json <object>
64
+ outrank-cli improvements tasks execute <task-id> [--improve-images true|false]
65
+ `;
66
+ const EXECUTIONS_HELP = `Usage:
67
+ outrank-cli improvements executions list <product-id> [--review-status awaiting_review|pushed|auto_pushed|rejected|all] [--limit <n>] [--offset <n>]
68
+ outrank-cli improvements executions get <execution-id>
69
+ outrank-cli improvements executions decisions <execution-id> --json <object>
70
+ outrank-cli improvements executions push <execution-id> [--json <object>]
71
+ `;
54
72
 
55
- const buildQueryPath = (basePath, searchParams) => {
56
- const url = new URL(`https://local${basePath}`);
57
- for (const [key, value] of Object.entries(searchParams)) {
58
- if (value !== undefined && value !== null && value !== '') {
59
- url.searchParams.set(key, String(value));
60
- }
61
- }
62
- return `${url.pathname}${url.search}`;
63
- };
73
+ const TOOLSETS = [Toolset.IMPROVEMENTS];
74
+ const PRODUCT_CLI = { positionals: ['productId'] };
75
+ const CANDIDATE_CLI = { positionals: ['candidateId'] };
76
+ const TASK_CLI = { positionals: ['taskId'] };
77
+ const EXECUTION_CLI = { positionals: ['executionId'] };
78
+ const SCHEDULED_FOR_PLACEHOLDERS = { scheduledFor: '<iso-date>' };
79
+ const PRODUCT_SETTINGS_PATH = '/products/{productId}/improvements/settings';
80
+ const CANDIDATE_BRIEF_PATH = '/improvements/candidates/{candidateId}/brief';
81
+ const TASK_PATH = '/improvements/tasks/{taskId}';
82
+ const EXECUTION_PATH = '/improvements/executions/{executionId}';
83
+ const POLL_EXECUTIONS_LIST = 'improvements.executions.list';
84
+ const POLL_CANDIDATES_LIST = 'improvements.candidates.list';
85
+ const POLL_EXECUTIONS_GET = 'improvements.executions.get';
64
86
 
65
- const hasJsonSource = (parsed) =>
66
- Boolean(getOption(parsed, 'json') || getOption(parsed, 'json-file') || hasFlag(parsed, 'json-stdin'));
87
+ const SCHEDULED_FOR_REQUIRED = { guidance: 'Pass a date or ISO timestamp, e.g. `--scheduled-for 2026-06-01`.' };
88
+ const IMPROVE_IMAGES_GUIDANCE =
89
+ 'Pass `--improve-images true` or `--improve-images false`, or omit it to use the default (images are regenerated).';
67
90
 
68
- const parseImproveImages = (parsed) => {
69
- const raw = getOption(parsed, 'improve-images');
70
- if (raw === undefined) return undefined;
71
- if (raw === 'true') return true;
72
- if (raw === 'false') return false;
73
- throw invalidUsage('--improve-images must be true or false.', 'Pass `--improve-images true` or `--improve-images false`, or omit it to use the default (images are regenerated).');
74
- };
91
+ const productInput = () => ({ productId: uuid().required() });
92
+ const productParams = (input) => ({ productId: input.productId });
75
93
 
76
- const requireScheduledFor = (parsed) => {
77
- const value = getOption(parsed, 'scheduled-for');
78
- if (!value) {
79
- throw invalidUsage('Missing required option: --scheduled-for <iso-date>.', 'Pass a date or ISO timestamp, e.g. `--scheduled-for 2026-06-01`.');
80
- }
81
- return value;
82
- };
94
+ const settingsGet = defineCommand({
95
+ id: 'improvements.settings.get',
96
+ summary: 'Get the article improvement settings of a product.',
97
+ kind: CommandKind.READ,
98
+ toolsets: TOOLSETS,
99
+ input: productInput(),
100
+ cli: PRODUCT_CLI,
101
+ run: (input, ctx) => ctx.http({ method: 'GET', path: PRODUCT_SETTINGS_PATH, pathParams: productParams(input) }),
102
+ });
83
103
 
84
- const handleAnalyze = async (parsed) => {
85
- const productId = getRequiredPositional(parsed, 0, '<product-id>');
86
- const response = await requestAgentApi(parsed, {
87
- method: 'POST',
88
- path: `/products/${productId}/improvements/analyze`,
89
- });
90
- return response.payload;
91
- };
104
+ const settingsUpdate = defineCommand({
105
+ id: 'improvements.settings.update',
106
+ summary: 'Update the article improvement settings of a product from a JSON body.',
107
+ kind: CommandKind.WRITE,
108
+ toolsets: TOOLSETS,
109
+ input: productInput(),
110
+ cli: PRODUCT_CLI,
111
+ body: BodyMode.JSON,
112
+ run: (input, ctx) =>
113
+ ctx.http({ method: 'PATCH', path: PRODUCT_SETTINGS_PATH, pathParams: productParams(input), body: input.body }),
114
+ });
115
+
116
+ const analyze = defineCommand({
117
+ id: 'improvements.analyze',
118
+ summary: 'Run improvement opportunity discovery for a product (multi-minute).',
119
+ kind: CommandKind.WRITE,
120
+ toolsets: TOOLSETS,
121
+ input: productInput(),
122
+ cli: PRODUCT_CLI,
123
+ longRunning: { pollTool: POLL_EXECUTIONS_LIST },
124
+ run: (input, ctx) =>
125
+ ctx.http({ method: 'POST', path: '/products/{productId}/improvements/analyze', pathParams: productParams(input) }),
126
+ });
92
127
 
93
- const handleSettings = async (action, parsed) => {
94
- const productId = getRequiredPositional(parsed, 0, '<product-id>');
95
- if (action === 'get') {
96
- const response = await requestAgentApi(parsed, {
128
+ const candidatesList = defineCommand({
129
+ id: 'improvements.candidates.list',
130
+ summary: 'List improvement candidates for a product.',
131
+ kind: CommandKind.READ,
132
+ toolsets: TOOLSETS,
133
+ input: {
134
+ ...productInput(),
135
+ type: string(),
136
+ limit: int({ min: LIST_MIN_LIMIT, max: IMPROVEMENTS_CANDIDATES_MAX_LIMIT }),
137
+ offset: int({ min: MIN_OFFSET }),
138
+ },
139
+ cli: PRODUCT_CLI,
140
+ run: (input, ctx) =>
141
+ ctx.http({
97
142
  method: 'GET',
98
- path: `/products/${productId}/improvements/settings`,
99
- });
100
- return response.payload;
101
- }
102
- if (action === 'update') {
103
- const body = await readJsonInput(parsed);
104
- const response = await requestAgentApi(parsed, {
105
- method: 'PATCH',
106
- path: `/products/${productId}/improvements/settings`,
107
- body,
108
- });
109
- return response.payload;
110
- }
111
- throw invalidUsage(`Unknown settings action: ${action ?? '(none)'}.`, 'Use: improvements settings get|update <product-id>.');
112
- };
143
+ path: '/products/{productId}/improvements/candidates',
144
+ pathParams: productParams(input),
145
+ query: { type: input.type, limit: input.limit, offset: input.offset },
146
+ }),
147
+ });
113
148
 
114
- const handleCandidates = async (action, parsed) => {
115
- if (action === 'list') {
116
- const productId = getRequiredPositional(parsed, 0, '<product-id>');
117
- const path = buildQueryPath(`/products/${productId}/improvements/candidates`, {
118
- type: getOption(parsed, 'type'),
119
- limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 50 }),
120
- offset: parseIntegerOption(parsed, 'offset', { min: 0 }),
121
- });
122
- const response = await requestAgentApi(parsed, { method: 'GET', path });
123
- return response.payload;
124
- }
125
- if (action === 'brief') {
126
- const candidateId = getRequiredPositional(parsed, 0, '<candidate-id>');
127
- const taskId = getOption(parsed, 'task');
128
- const response = await requestAgentApi(parsed, {
149
+ const candidatesBrief = defineCommand({
150
+ id: 'improvements.candidates.brief',
151
+ summary: 'Generate the improvement brief for a candidate (multi-minute).',
152
+ kind: CommandKind.WRITE,
153
+ toolsets: TOOLSETS,
154
+ input: { candidateId: uuid().required(), taskId: uuid() },
155
+ cli: { ...CANDIDATE_CLI, options: { task: 'taskId' } },
156
+ longRunning: { pollTool: POLL_CANDIDATES_LIST },
157
+ run: (input, ctx) =>
158
+ ctx.http({
129
159
  method: 'POST',
130
- path: `/improvements/candidates/${candidateId}/brief`,
131
- body: taskId ? { task_id: taskId } : {},
132
- });
133
- return response.payload;
134
- }
135
- if (action === 'edit-brief') {
136
- const candidateId = getRequiredPositional(parsed, 0, '<candidate-id>');
137
- const body = await readJsonInput(parsed);
138
- const response = await requestAgentApi(parsed, {
139
- method: 'PATCH',
140
- path: `/improvements/candidates/${candidateId}/brief`,
141
- body,
142
- });
143
- return response.payload;
144
- }
145
- if (action === 'schedule') {
146
- const candidateId = getRequiredPositional(parsed, 0, '<candidate-id>');
147
- const scheduledFor = requireScheduledFor(parsed);
148
- const response = await requestAgentApi(parsed, {
149
- method: 'POST',
150
- path: `/improvements/candidates/${candidateId}/schedule`,
151
- body: { scheduled_for: scheduledFor },
152
- });
153
- return response.payload;
154
- }
155
- throw invalidUsage(`Unknown candidates action: ${action ?? '(none)'}.`, 'Use: improvements candidates list|brief|edit-brief|schedule.');
156
- };
160
+ path: CANDIDATE_BRIEF_PATH,
161
+ pathParams: { candidateId: input.candidateId },
162
+ body: input.taskId ? { task_id: input.taskId } : {},
163
+ }),
164
+ });
157
165
 
158
- const handleTasks = async (action, parsed) => {
159
- if (action === 'list') {
160
- const productId = getRequiredPositional(parsed, 0, '<product-id>');
161
- const response = await requestAgentApi(parsed, {
162
- method: 'GET',
163
- path: `/products/${productId}/improvements/tasks`,
164
- });
165
- return response.payload;
166
- }
167
- if (action === 'reschedule') {
168
- const taskId = getRequiredPositional(parsed, 0, '<task-id>');
169
- const scheduledFor = requireScheduledFor(parsed);
170
- const response = await requestAgentApi(parsed, {
166
+ const candidatesEditBrief = defineCommand({
167
+ id: 'improvements.candidates.edit-brief',
168
+ summary: 'Edit the improvement brief of a candidate from a JSON body.',
169
+ kind: CommandKind.WRITE,
170
+ toolsets: TOOLSETS,
171
+ input: { candidateId: uuid().required() },
172
+ cli: CANDIDATE_CLI,
173
+ body: BodyMode.JSON,
174
+ run: (input, ctx) =>
175
+ ctx.http({
171
176
  method: 'PATCH',
172
- path: `/improvements/tasks/${taskId}`,
173
- body: { scheduled_for: scheduledFor },
174
- });
175
- return response.payload;
176
- }
177
- if (action === 'cancel') {
178
- const taskId = getRequiredPositional(parsed, 0, '<task-id>');
179
- const response = await requestAgentApi(parsed, {
180
- method: 'DELETE',
181
- path: `/improvements/tasks/${taskId}`,
182
- });
183
- return response.payload;
184
- }
185
- if (action === 'edit-brief') {
186
- const taskId = getRequiredPositional(parsed, 0, '<task-id>');
187
- const body = await readJsonInput(parsed);
188
- const response = await requestAgentApi(parsed, {
189
- method: 'PATCH',
190
- path: `/improvements/tasks/${taskId}/brief`,
191
- body,
192
- });
193
- return response.payload;
194
- }
195
- if (action === 'execute') {
196
- const taskId = getRequiredPositional(parsed, 0, '<task-id>');
197
- const improveImages = parseImproveImages(parsed);
198
- const response = await requestAgentApi(parsed, {
177
+ path: CANDIDATE_BRIEF_PATH,
178
+ pathParams: { candidateId: input.candidateId },
179
+ body: input.body,
180
+ }),
181
+ });
182
+
183
+ const candidatesSchedule = defineCommand({
184
+ id: 'improvements.candidates.schedule',
185
+ summary: 'Schedule an improvement task for a candidate.',
186
+ kind: CommandKind.WRITE,
187
+ toolsets: TOOLSETS,
188
+ input: { candidateId: uuid().required(), scheduledFor: string().required(SCHEDULED_FOR_REQUIRED) },
189
+ cli: { ...CANDIDATE_CLI, placeholders: SCHEDULED_FOR_PLACEHOLDERS },
190
+ run: (input, ctx) =>
191
+ ctx.http({
199
192
  method: 'POST',
200
- path: `/improvements/tasks/${taskId}/execute`,
201
- body: improveImages === undefined ? {} : { improve_images: improveImages },
202
- });
203
- return response.payload;
204
- }
205
- throw invalidUsage(`Unknown tasks action: ${action ?? '(none)'}.`, 'Use: improvements tasks list|reschedule|cancel|edit-brief|execute.');
206
- };
193
+ path: '/improvements/candidates/{candidateId}/schedule',
194
+ pathParams: { candidateId: input.candidateId },
195
+ body: { scheduled_for: input.scheduledFor },
196
+ }),
197
+ });
207
198
 
208
- const handleExecutions = async (action, parsed) => {
209
- if (action === 'list') {
210
- const productId = getRequiredPositional(parsed, 0, '<product-id>');
211
- const path = buildQueryPath(`/products/${productId}/improvements/executions`, {
212
- review_status: getOption(parsed, 'review-status'),
213
- limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 100 }),
214
- offset: parseIntegerOption(parsed, 'offset', { min: 0 }),
215
- });
216
- const response = await requestAgentApi(parsed, { method: 'GET', path });
217
- return response.payload;
218
- }
219
- if (action === 'get') {
220
- const executionId = getRequiredPositional(parsed, 0, '<execution-id>');
221
- const response = await requestAgentApi(parsed, {
222
- method: 'GET',
223
- path: `/improvements/executions/${executionId}`,
224
- });
225
- return response.payload;
226
- }
227
- if (action === 'decisions') {
228
- const executionId = getRequiredPositional(parsed, 0, '<execution-id>');
229
- const body = await readJsonInput(parsed);
230
- const response = await requestAgentApi(parsed, {
199
+ const tasksList = defineCommand({
200
+ id: 'improvements.tasks.list',
201
+ summary: 'List scheduled improvement tasks for a product.',
202
+ kind: CommandKind.READ,
203
+ toolsets: TOOLSETS,
204
+ input: productInput(),
205
+ cli: PRODUCT_CLI,
206
+ run: (input, ctx) =>
207
+ ctx.http({ method: 'GET', path: '/products/{productId}/improvements/tasks', pathParams: productParams(input) }),
208
+ });
209
+
210
+ const tasksReschedule = defineCommand({
211
+ id: 'improvements.tasks.reschedule',
212
+ summary: 'Reschedule an improvement task.',
213
+ kind: CommandKind.WRITE,
214
+ toolsets: TOOLSETS,
215
+ input: { taskId: uuid().required(), scheduledFor: string().required(SCHEDULED_FOR_REQUIRED) },
216
+ cli: { ...TASK_CLI, placeholders: SCHEDULED_FOR_PLACEHOLDERS },
217
+ run: (input, ctx) =>
218
+ ctx.http({
231
219
  method: 'PATCH',
232
- path: `/improvements/executions/${executionId}/decisions`,
233
- body,
234
- });
235
- return response.payload;
236
- }
237
- if (action === 'push') {
238
- const executionId = getRequiredPositional(parsed, 0, '<execution-id>');
239
- const body = hasJsonSource(parsed) ? await readJsonInput(parsed) : {};
240
- const response = await requestAgentApi(parsed, {
220
+ path: TASK_PATH,
221
+ pathParams: { taskId: input.taskId },
222
+ body: { scheduled_for: input.scheduledFor },
223
+ }),
224
+ });
225
+
226
+ const tasksCancel = defineCommand({
227
+ id: 'improvements.tasks.cancel',
228
+ summary: 'Cancel an improvement task.',
229
+ kind: CommandKind.DESTRUCTIVE,
230
+ toolsets: TOOLSETS,
231
+ input: { taskId: uuid().required() },
232
+ cli: TASK_CLI,
233
+ run: (input, ctx) => ctx.http({ method: 'DELETE', path: TASK_PATH, pathParams: { taskId: input.taskId } }),
234
+ });
235
+
236
+ const tasksEditBrief = defineCommand({
237
+ id: 'improvements.tasks.edit-brief',
238
+ summary: 'Edit the brief of an improvement task from a JSON body.',
239
+ kind: CommandKind.WRITE,
240
+ toolsets: TOOLSETS,
241
+ input: { taskId: uuid().required() },
242
+ cli: TASK_CLI,
243
+ body: BodyMode.JSON,
244
+ run: (input, ctx) =>
245
+ ctx.http({ method: 'PATCH', path: `${TASK_PATH}/brief`, pathParams: { taskId: input.taskId }, body: input.body }),
246
+ });
247
+
248
+ const tasksExecute = defineCommand({
249
+ id: 'improvements.tasks.execute',
250
+ summary: 'Execute an improvement task now (multi-minute).',
251
+ kind: CommandKind.WRITE,
252
+ toolsets: TOOLSETS,
253
+ input: {
254
+ taskId: uuid().required(),
255
+ improveImages: bool({ serialize: BoolSerialize.ALWAYS }).guidance(IMPROVE_IMAGES_GUIDANCE),
256
+ },
257
+ cli: { ...TASK_CLI, options: { 'improve-images': { field: 'improveImages', takesValue: true } } },
258
+ longRunning: { pollTool: POLL_EXECUTIONS_GET },
259
+ run: (input, ctx) =>
260
+ ctx.http({
241
261
  method: 'POST',
242
- path: `/improvements/executions/${executionId}/push`,
243
- body,
244
- });
245
- return response.payload;
246
- }
247
- throw invalidUsage(`Unknown executions action: ${action ?? '(none)'}.`, 'Use: improvements executions list|get|decisions|push.');
248
- };
262
+ path: `${TASK_PATH}/execute`,
263
+ pathParams: { taskId: input.taskId },
264
+ body: input.improveImages === undefined ? {} : { improve_images: input.improveImages },
265
+ }),
266
+ });
249
267
 
250
- export const handleImprovementsCommand = async (argv) => {
251
- const subgroup = argv[0];
252
- const wantsHelp =
253
- !subgroup || subgroup === '--help' || subgroup === '-h' || subgroup === 'help';
268
+ const executionsList = defineCommand({
269
+ id: 'improvements.executions.list',
270
+ summary: 'List improvement executions for a product.',
271
+ kind: CommandKind.READ,
272
+ toolsets: TOOLSETS,
273
+ input: {
274
+ ...productInput(),
275
+ reviewStatus: string(),
276
+ limit: int({ min: LIST_MIN_LIMIT, max: LIST_MAX_LIMIT }),
277
+ offset: int({ min: MIN_OFFSET }),
278
+ },
279
+ cli: PRODUCT_CLI,
280
+ run: (input, ctx) =>
281
+ ctx.http({
282
+ method: 'GET',
283
+ path: '/products/{productId}/improvements/executions',
284
+ pathParams: productParams(input),
285
+ query: { review_status: input.reviewStatus, limit: input.limit, offset: input.offset },
286
+ }),
287
+ });
254
288
 
255
- if (wantsHelp) {
256
- return cliSuccess(
257
- {
258
- usage: IMPROVEMENTS_HELP.trim(),
259
- },
260
- 'Choose a sub-noun: settings, analyze, candidates, tasks, or executions. The typical flow is analyze -> candidates brief -> candidates schedule -> tasks execute -> executions push.',
261
- );
262
- }
289
+ const executionsGet = defineCommand({
290
+ id: 'improvements.executions.get',
291
+ summary: 'Get one improvement execution with its proposed changes.',
292
+ kind: CommandKind.READ,
293
+ toolsets: TOOLSETS,
294
+ input: { executionId: uuid().required() },
295
+ cli: EXECUTION_CLI,
296
+ run: (input, ctx) =>
297
+ ctx.http({ method: 'GET', path: EXECUTION_PATH, pathParams: { executionId: input.executionId } }),
298
+ });
263
299
 
264
- if (subgroup === 'analyze') {
265
- return handleAnalyze(parseArgs(argv.slice(1)));
266
- }
300
+ const executionsDecisions = defineCommand({
301
+ id: 'improvements.executions.decisions',
302
+ summary: 'Record review decisions for an execution from a JSON body.',
303
+ kind: CommandKind.WRITE,
304
+ toolsets: TOOLSETS,
305
+ input: { executionId: uuid().required() },
306
+ cli: EXECUTION_CLI,
307
+ body: BodyMode.JSON,
308
+ run: (input, ctx) =>
309
+ ctx.http({
310
+ method: 'PATCH',
311
+ path: `${EXECUTION_PATH}/decisions`,
312
+ pathParams: { executionId: input.executionId },
313
+ body: input.body,
314
+ }),
315
+ });
267
316
 
268
- const action = argv[1];
269
- const parsed = parseArgs(argv.slice(2));
317
+ const executionsPush = defineCommand({
318
+ id: 'improvements.executions.push',
319
+ summary: 'Push an execution to the CMS, optionally with decisions in a JSON body.',
320
+ kind: CommandKind.WRITE,
321
+ toolsets: TOOLSETS,
322
+ input: { executionId: uuid().required() },
323
+ cli: EXECUTION_CLI,
324
+ body: BodyMode.OPTIONAL_JSON_OR_EMPTY_OBJECT,
325
+ longRunning: { pollTool: POLL_EXECUTIONS_GET },
326
+ run: (input, ctx) =>
327
+ ctx.http({
328
+ method: 'POST',
329
+ path: `${EXECUTION_PATH}/push`,
330
+ pathParams: { executionId: input.executionId },
331
+ body: input.body,
332
+ }),
333
+ });
270
334
 
271
- if (subgroup === 'settings') return handleSettings(action, parsed);
272
- if (subgroup === 'candidates') return handleCandidates(action, parsed);
273
- if (subgroup === 'tasks') return handleTasks(action, parsed);
274
- if (subgroup === 'executions') return handleExecutions(action, parsed);
335
+ const subGroup = ({ id, usage, actions, children }) =>
336
+ defineGroup({
337
+ id,
338
+ help: { usage, guidance: `Use: improvements ${id.slice(id.indexOf('.') + 1)} ${actions}.` },
339
+ unknown: (word) => ({
340
+ message: `Unknown ${id.slice(id.indexOf('.') + 1)} action: ${word}.`,
341
+ guidance: `Use: improvements ${id.slice(id.indexOf('.') + 1)} ${actions}.`,
342
+ }),
343
+ children,
344
+ });
275
345
 
276
- throw invalidUsage(`Unknown improvements sub-noun: ${subgroup}.`);
277
- };
346
+ export const improvementsGroup = defineGroup({
347
+ id: 'improvements',
348
+ help: {
349
+ usage: IMPROVEMENTS_HELP,
350
+ guidance:
351
+ 'Choose a sub-noun: settings, analyze, candidates, tasks, or executions. The typical flow is analyze -> ' +
352
+ 'candidates brief -> candidates schedule -> tasks execute -> executions push.',
353
+ },
354
+ unknown: (word) => ({
355
+ message: `Unknown improvements sub-noun: ${word}.`,
356
+ guidance: 'Run `outrank-cli improvements --help` to see available improvement commands.',
357
+ }),
358
+ children: [
359
+ subGroup({
360
+ id: 'improvements.settings',
361
+ usage: SETTINGS_HELP,
362
+ actions: 'get|update <product-id>',
363
+ children: [settingsGet, settingsUpdate],
364
+ }),
365
+ analyze,
366
+ subGroup({
367
+ id: 'improvements.candidates',
368
+ usage: CANDIDATES_HELP,
369
+ actions: 'list|brief|edit-brief|schedule',
370
+ children: [candidatesList, candidatesBrief, candidatesEditBrief, candidatesSchedule],
371
+ }),
372
+ subGroup({
373
+ id: 'improvements.tasks',
374
+ usage: TASKS_HELP,
375
+ actions: 'list|reschedule|cancel|edit-brief|execute',
376
+ children: [tasksList, tasksReschedule, tasksCancel, tasksEditBrief, tasksExecute],
377
+ }),
378
+ subGroup({
379
+ id: 'improvements.executions',
380
+ usage: EXECUTIONS_HELP,
381
+ actions: 'list|get|decisions|push',
382
+ children: [executionsList, executionsGet, executionsDecisions, executionsPush],
383
+ }),
384
+ ],
385
+ });
@@ -0,0 +1,22 @@
1
+ import { articlesGroup } from './articles.js';
2
+ import { authGroup } from './auth.js';
3
+ import { billingGroup } from './billing.js';
4
+ import { improvementsGroup } from './improvements.js';
5
+ import { keywordsGroup } from './keywords.js';
6
+ import { marketplaceGroup } from './marketplace/index.js';
7
+ import { productsGroup } from './products/index.js';
8
+ import { createRegistry } from './registry.js';
9
+ import { subscriptionGroup } from './subscription.js';
10
+ import { usageGroup } from './usage.js';
11
+
12
+ export const registry = createRegistry([
13
+ authGroup,
14
+ usageGroup,
15
+ subscriptionGroup,
16
+ billingGroup,
17
+ keywordsGroup,
18
+ articlesGroup,
19
+ improvementsGroup,
20
+ productsGroup,
21
+ marketplaceGroup,
22
+ ]);