outrank-cli 0.10.0 → 0.11.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.
@@ -1,6 +1,7 @@
1
1
  import { IMPROVEMENTS_CANDIDATES_MAX_LIMIT, LIST_MAX_LIMIT, LIST_MIN_LIMIT, MIN_OFFSET } from '../core/constants.js';
2
2
  import { BoolSerialize, bool, int, list, object, oneOf, string, uuid } from '../core/schema.js';
3
3
  import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
4
+ import { subGroup } from './subGroup.js';
4
5
 
5
6
  const WEEKDAY_CODES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
6
7
  const ENABLED_HINT = 'true starts the unattended improvement loop.';
@@ -343,17 +344,6 @@ const executionsPush = defineCommand({
343
344
  }),
344
345
  });
345
346
 
346
- const subGroup = ({ id, usage, actions, children }) =>
347
- defineGroup({
348
- id,
349
- help: { usage, guidance: `Use: improvements ${id.slice(id.indexOf('.') + 1)} ${actions}.` },
350
- unknown: (word) => ({
351
- message: `Unknown ${id.slice(id.indexOf('.') + 1)} action: ${word}.`,
352
- guidance: `Use: improvements ${id.slice(id.indexOf('.') + 1)} ${actions}.`,
353
- }),
354
- children,
355
- });
356
-
357
347
  export const improvementsGroup = defineGroup({
358
348
  id: 'improvements',
359
349
  help: {
@@ -1,3 +1,4 @@
1
+ import { aiCitedListsGroup } from './aiCitedLists.js';
1
2
  import { articlesGroup } from './articles.js';
2
3
  import { authGroup } from './auth.js';
3
4
  import { billingGroup } from './billing.js';
@@ -19,4 +20,5 @@ export const registry = createRegistry([
19
20
  improvementsGroup,
20
21
  productsGroup,
21
22
  marketplaceGroup,
23
+ aiCitedListsGroup,
22
24
  ]);
@@ -1,7 +1,15 @@
1
1
  import { usageError } from '../../core/errors.js';
2
2
  import { oneOf, string, uuid } from '../../core/schema.js';
3
3
  import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from '../registry.js';
4
- import { PRODUCT_CLI, productInput, productParams, withProduct } from './shared.js';
4
+ import {
5
+ ATTEMPT_ID_PLACEHOLDERS,
6
+ PRODUCT_CLI,
7
+ attemptIdInput,
8
+ attemptIdQuery,
9
+ productInput,
10
+ productParams,
11
+ withProduct,
12
+ } from './shared.js';
5
13
 
6
14
  const INTEGRATIONS_USAGE = `Usage:
7
15
  outrank-cli products integrations providers
@@ -92,12 +100,9 @@ const INTEGRATION_PATH = '/products/{productId}/integrations/{integrationId}';
92
100
  const CONNECT_PATH = '/products/{productId}/integrations/connect/{provider}';
93
101
  const PROVIDER_PRODUCT_CLI = { positionals: ['provider', 'productId'] };
94
102
  const INTEGRATION_CLI = withProduct(['integrationId']);
95
- const ATTEMPT_ID_PLACEHOLDERS = { attemptId: '<attempt-id>' };
96
- const ATTEMPT_ID_REQUIRED = {
97
- guidance:
98
- 'Run `outrank-cli products integrations poll <provider> <product-id> --attempt-id <attempt-id>` after ' +
99
- 'connect starts.',
100
- };
103
+ const ATTEMPT_ID_GUIDANCE =
104
+ 'Run `outrank-cli products integrations poll <provider> <product-id> --attempt-id <attempt-id>` after ' +
105
+ 'connect starts.';
101
106
  const SENSITIVE_BODY = { sensitiveBody: true };
102
107
 
103
108
  const providerInput = (providers) => ({ provider: oneOf(providers).required(), ...productInput() });
@@ -147,14 +152,14 @@ const poll = defineCommand({
147
152
  summary: 'Poll an OAuth connect attempt until the handoff completes.',
148
153
  kind: CommandKind.READ,
149
154
  toolsets: TOOLSETS,
150
- input: { ...providerInput(CONNECT_PROVIDERS), attemptId: string().required(ATTEMPT_ID_REQUIRED) },
155
+ input: { ...providerInput(CONNECT_PROVIDERS), ...attemptIdInput(ATTEMPT_ID_GUIDANCE) },
151
156
  cli: { ...PROVIDER_PRODUCT_CLI, placeholders: ATTEMPT_ID_PLACEHOLDERS },
152
157
  run: (input, ctx) =>
153
158
  ctx.http({
154
159
  method: 'GET',
155
160
  path: CONNECT_PATH,
156
161
  pathParams: providerParams(input),
157
- query: { attempt_id: input.attemptId },
162
+ query: attemptIdQuery(input),
158
163
  }),
159
164
  });
160
165
 
@@ -1,4 +1,4 @@
1
- import { uuid } from '../../core/schema.js';
1
+ import { string, uuid } from '../../core/schema.js';
2
2
 
3
3
  export const PRODUCT_CLI = { positionals: ['productId'] };
4
4
 
@@ -6,3 +6,8 @@ export const productInput = () => ({ productId: uuid().required() });
6
6
  export const productParams = (input) => ({ productId: input.productId });
7
7
 
8
8
  export const withProduct = (extraPositionals) => ({ positionals: ['productId', ...extraPositionals] });
9
+ export const productParamsWith = (input, name) => ({ productId: input.productId, [name]: input[name] });
10
+
11
+ export const ATTEMPT_ID_PLACEHOLDERS = { attemptId: '<attempt-id>' };
12
+ export const attemptIdInput = (guidance) => ({ attemptId: string().required({ guidance }) });
13
+ export const attemptIdQuery = (input) => ({ attempt_id: input.attemptId });
@@ -23,6 +23,7 @@ export const Toolset = Object.freeze({
23
23
  MARKETPLACE_BUYER: 'marketplace-buyer',
24
24
  BILLING: 'billing',
25
25
  BLOG: 'blog',
26
+ AI_CITED_LISTS: 'ai-cited-lists',
26
27
  });
27
28
 
28
29
  export const BodyMode = Object.freeze({
@@ -0,0 +1,15 @@
1
+ import { cliWords, defineGroup, lastSegment } from './registry.js';
2
+
3
+ const WORD_SEPARATOR = ' ';
4
+
5
+ export const subGroup = ({ id, usage, actions, children }) => {
6
+ const noun = lastSegment(id);
7
+ const group = cliWords(id).slice(0, -1).join(WORD_SEPARATOR);
8
+ const guidance = `Use: ${group} ${noun} ${actions}.`;
9
+ return defineGroup({
10
+ id,
11
+ help: { usage, guidance },
12
+ unknown: (word) => ({ message: `Unknown ${noun} action: ${word}.`, guidance }),
13
+ children,
14
+ });
15
+ };
@@ -66,6 +66,10 @@ export const BLOG_REDIRECT_GONE_STATUS_CODE = 410;
66
66
  export const BLOG_REDIRECT_STATUS_CODES = Object.freeze([301, 302, BLOG_REDIRECT_GONE_STATUS_CODE]);
67
67
  export const BLOG_WEBHOOK_EVENTS = Object.freeze(['subscriber.created']);
68
68
 
69
+ export const AI_CITATIONS_PERIOD_WEEKS = Object.freeze([4, 12]);
70
+ export const AI_CITED_LISTS_LIST_VIEWS = Object.freeze(['all', 'to_send', 'skipped']);
71
+ export const AI_CITED_LISTS_ENTRY_MODES = Object.freeze(['generated', 'custom']);
72
+
69
73
  export const BILLING_MIN_ADDITIONAL_PRODUCTS = 1;
70
74
  export const BILLING_MAX_ADDITIONAL_PRODUCTS = 30;
71
75
  export const BILLING_MIN_CONFIRM_CHARGE_CENTS = 0;
@@ -50,7 +50,8 @@ const mergeBody = (spec, input, body) => {
50
50
  };
51
51
 
52
52
  export const executeCommand = async ({ spec, raw, loadBody = async () => undefined, ctx, labels = {} }) => {
53
- const validation = { ...labels, splitLists: ctx.surface !== Surface.MCP };
53
+ const fromText = ctx.surface !== Surface.MCP;
54
+ const validation = { ...labels, splitLists: fromText, coerceEnumText: fromText };
54
55
  try {
55
56
  const { bodyField, rest } = splitBodyField(spec.input);
56
57
  const input = validateInput(rest, raw, validation);
@@ -83,7 +83,7 @@ const parseInteger = (value, label) => {
83
83
  return Number(value);
84
84
  };
85
85
 
86
- const parseScalar = (field, value, label) => {
86
+ const parseScalar = (field, value, label, { coerceEnumText = false } = {}) => {
87
87
  if (field.kind === FieldKind.STRING) {
88
88
  if (typeof value !== 'string') throw usageError(`${label} must be a string.`);
89
89
  return value;
@@ -111,8 +111,11 @@ const parseScalar = (field, value, label) => {
111
111
  throw usageError(`${label} must be true or false.`);
112
112
  }
113
113
  if (field.kind === FieldKind.ENUM) {
114
- if (!field.values.includes(value)) throw usageError(`${label} must be one of: ${field.values.join(', ')}.`);
115
- return value;
114
+ const match = field.values.find(
115
+ (candidate) => candidate === value || (coerceEnumText && String(candidate) === value),
116
+ );
117
+ if (match === undefined) throw usageError(`${label} must be one of: ${field.values.join(', ')}.`);
118
+ return match;
116
119
  }
117
120
  if (field.kind === FieldKind.OBJECT) {
118
121
  if (!isPlainObject(value)) throw usageError(`${label} must be a JSON object.`);
@@ -139,11 +142,11 @@ const splitListValues = (field, value, splitLists) => {
139
142
  return rawItems.flatMap((item) => (typeof item === 'string' ? item.split(field.split) : [item]));
140
143
  };
141
144
 
142
- const parseList = (field, value, label, splitLists) => {
145
+ const parseList = (field, value, label, splitLists, coerceEnumText) => {
143
146
  const items = splitListValues(field, value, splitLists)
144
147
  .map((item) => (typeof item === 'string' ? item.trim() : item))
145
148
  .filter((item) => item !== '');
146
- return items.map((item) => parseScalar(field.item, item, label));
149
+ return items.map((item) => parseScalar(field.item, item, label, { coerceEnumText }));
147
150
  };
148
151
 
149
152
  const withFieldGuidance = (field, run) => {
@@ -157,11 +160,11 @@ const withFieldGuidance = (field, run) => {
157
160
  }
158
161
  };
159
162
 
160
- export const parseFieldValue = (field, value, label, { splitLists = true } = {}) =>
163
+ export const parseFieldValue = (field, value, label, { splitLists = true, coerceEnumText = false } = {}) =>
161
164
  withFieldGuidance(field, () => {
162
165
  if (value === '') throw usageError(`${label} requires a value.`);
163
- if (field.kind === FieldKind.LIST) return parseList(field, value, label, splitLists);
164
- return parseScalar(field, value, label);
166
+ if (field.kind === FieldKind.LIST) return parseList(field, value, label, splitLists, coerceEnumText);
167
+ return parseScalar(field, value, label, { coerceEnumText });
165
168
  });
166
169
 
167
170
  const missingPropertyError = (property, label) => {
@@ -202,7 +205,11 @@ const missingFieldError = (field, name, missingMessage) => {
202
205
 
203
206
  const cloneDefault = (value) => (value !== null && typeof value === 'object' ? structuredClone(value) : value);
204
207
 
205
- export const validateInput = (fields, raw, { labelOf = (name) => name, missingOf, splitLists = true } = {}) => {
208
+ export const validateInput = (
209
+ fields,
210
+ raw,
211
+ { labelOf = (name) => name, missingOf, splitLists = true, coerceEnumText = false } = {},
212
+ ) => {
206
213
  const missingMessage = missingOf ?? ((name) => `Missing required input: ${labelOf(name)}.`);
207
214
  const result = {};
208
215
  for (const [name, field] of Object.entries(fields)) {
@@ -212,7 +219,7 @@ export const validateInput = (fields, raw, { labelOf = (name) => name, missingOf
212
219
  if (field.defaultValue !== undefined) result[name] = cloneDefault(field.defaultValue);
213
220
  continue;
214
221
  }
215
- const parsed = parseFieldValue(field, value, labelOf(name), { splitLists });
222
+ const parsed = parseFieldValue(field, value, labelOf(name), { splitLists, coerceEnumText });
216
223
  if (field.kind === FieldKind.LIST && field.isRequired && parsed.length === 0) {
217
224
  throw missingFieldError(field, name, missingMessage);
218
225
  }
@@ -239,6 +246,9 @@ const boundedNumber = (schema, field) => {
239
246
  return bounded;
240
247
  };
241
248
 
249
+ const zodEnum = (z, values) =>
250
+ values.every((value) => typeof value === 'string') ? z.enum(values) : z.literal(values);
251
+
242
252
  const zodScalar = (z, field, options = {}) => {
243
253
  const { jsonAsObject = false, inBody = false } = options;
244
254
  if (field.kind === FieldKind.STRING) return inBody ? z.string() : z.string().min(1);
@@ -246,7 +256,7 @@ const zodScalar = (z, field, options = {}) => {
246
256
  if (field.kind === FieldKind.INT) return boundedNumber(z.number().int(), field);
247
257
  if (field.kind === FieldKind.NUMBER) return boundedNumber(z.number(), field);
248
258
  if (field.kind === FieldKind.BOOL) return z.boolean();
249
- if (field.kind === FieldKind.ENUM) return z.enum(field.values);
259
+ if (field.kind === FieldKind.ENUM) return zodEnum(z, field.values);
250
260
  if (field.kind === FieldKind.OBJECT) {
251
261
  if (!field.properties) return z.object({}).passthrough();
252
262
  const propertyOptions = { ...options, inBody: true, jsonAsObject: false };
package/src/index.js CHANGED
@@ -26,6 +26,7 @@ Groups:
26
26
  articles Inspect article metadata and bodies, edit content, retry publish, precreate, and generate.
27
27
  improvements Discover, brief, schedule, run, review, and publish article improvements for existing posts.
28
28
  marketplace Seller onboarding, eligibility, placements, earnings, Stripe payouts, and buyer setup for the Outrank Marketplace.
29
+ ai-cited-lists AI-Cited Lists (Backlinks Enterprise): ChatGPT-cited lists, offers to their owners, won spots, outreach setup.
29
30
  subscription Inspect plan, trial, quota, and add-on status.
30
31
  usage Inspect current usage and quota consumption.
31
32
  billing Product-slot purchase flows.
@@ -68,6 +69,8 @@ Examples:
68
69
  outrank-cli marketplace seller status <product-id>
69
70
  outrank-cli marketplace seller onboard <product-id> --country US
70
71
  outrank-cli marketplace seller placements list <product-id> --status needs_approval
72
+ outrank-cli ai-cited-lists status <product-id>
73
+ outrank-cli ai-cited-lists lists list <product-id> --view to_send
71
74
  `;
72
75
 
73
76
  const main = async () => {