outrank-cli 0.7.1 → 0.9.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,5 +1,6 @@
1
1
  import { defineGroup } from '../registry.js';
2
2
  import { backlinksGroup } from './backlinks.js';
3
+ import { blogGroup } from './blog.js';
3
4
  import {
4
5
  competitorsGroup,
5
6
  outputSettingsGroup,
@@ -78,6 +79,10 @@ export const PRODUCTS_HELP = `Usage:
78
79
  outrank-cli products backlinks reverify <product-id> <backlink-id>
79
80
  outrank-cli products backlinks subscription <product-id>
80
81
  outrank-cli products backlinks subscribe-quote <product-id> --plan premium|enterprise
82
+ outrank-cli products blog get <product-id>
83
+ outrank-cli products blog create <product-id> [--json <object>]
84
+ outrank-cli products blog posts list <product-id>
85
+ outrank-cli products blog --help (design, domain, posts, pages, categories, authors, redirects, snippets, subscribers, webhooks)
81
86
 
82
87
  JSON Input:
83
88
  Use one of: --json '<object>', --json-file payload.json, or --json-stdin
@@ -142,6 +147,8 @@ Examples:
142
147
  outrank-cli products backlinks reverify 00000000-0000-0000-0000-000000000000 11111111-1111-1111-1111-111111111111
143
148
  outrank-cli products backlinks subscription 00000000-0000-0000-0000-000000000000
144
149
  outrank-cli products backlinks subscribe-quote 00000000-0000-0000-0000-000000000000 --plan premium
150
+ outrank-cli products blog create 00000000-0000-0000-0000-000000000000 --json '{"subdomain":"acme","template":"editorial","accentColor":"#2563EB"}'
151
+ outrank-cli products blog posts update-content 00000000-0000-0000-0000-000000000000 11111111-1111-1111-1111-111111111111 --json-file post.json
145
152
  `;
146
153
 
147
154
  export const productsGroup = defineGroup({
@@ -150,7 +157,7 @@ export const productsGroup = defineGroup({
150
157
  usage: PRODUCTS_HELP,
151
158
  guidance:
152
159
  'Choose one of: list, get, create, update, pause, resume, integrations, output-settings, target-audiences, ' +
153
- 'competitors, gsc, linking, backlinks.',
160
+ 'competitors, gsc, linking, backlinks, blog.',
154
161
  },
155
162
  unknown: (word) => ({
156
163
  message: `Unknown products subcommand: ${word}.`,
@@ -170,5 +177,6 @@ export const productsGroup = defineGroup({
170
177
  gscGroup,
171
178
  linkingGroup,
172
179
  backlinksGroup,
180
+ blogGroup,
173
181
  ],
174
182
  });
@@ -22,6 +22,7 @@ export const Toolset = Object.freeze({
22
22
  MARKETPLACE_SELLER: 'marketplace-seller',
23
23
  MARKETPLACE_BUYER: 'marketplace-buyer',
24
24
  BILLING: 'billing',
25
+ BLOG: 'blog',
25
26
  });
26
27
 
27
28
  export const BodyMode = Object.freeze({
@@ -27,6 +27,8 @@ export const MCP_STARTUP_RESOLVE_TIMEOUT_MS = 3_000;
27
27
 
28
28
  export const MAX_IMAGE_UPLOAD_MEGABYTES = 5;
29
29
  export const MAX_IMAGE_UPLOAD_BYTES = MAX_IMAGE_UPLOAD_MEGABYTES * BYTES_PER_MB;
30
+ export const MAX_TEXT_FILE_MEGABYTES = 1;
31
+ export const MAX_TEXT_FILE_BYTES = MAX_TEXT_FILE_MEGABYTES * BYTES_PER_MB;
30
32
  export const MAX_UPSTREAM_ERROR_BODY_CHARS = 500;
31
33
  export const MAX_RATE_LIMIT_HEADER_LENGTH = 64;
32
34
  export const MAX_TOOL_NAME_LENGTH = 46;
@@ -45,6 +47,25 @@ export const GSC_SEARCH_ANALYTICS_MAX_LIMIT = 1000;
45
47
  export const KEYWORDS_GENERATE_MIN_COUNT = 5;
46
48
  export const KEYWORDS_GENERATE_MAX_COUNT = 30;
47
49
 
50
+ export const BLOG_LIST_MIN_PAGE = 1;
51
+ export const BLOG_TEMPLATES = Object.freeze(['classic', 'editorial', 'journal', 'studio', 'minimal']);
52
+ export const BLOG_ANALYTICS_RANGES = Object.freeze(['7d', '30d', '90d']);
53
+ export const BLOG_DOMAIN_MODES = Object.freeze(['subdomain', 'apex', 'subfolder']);
54
+ export const BLOG_POST_LIST_STATUSES = Object.freeze(['all', 'published', 'scheduled', 'draft']);
55
+ export const BLOG_POST_WRITE_STATUSES = Object.freeze(['draft', 'published', 'scheduled']);
56
+ export const BLOG_SUBSCRIBER_STATUSES = Object.freeze(['subscribed', 'unsubscribed']);
57
+ export const BLOG_PREVIEW_DESIGN_SCOPE = 'design';
58
+ export const BLOG_PREVIEW_SCOPES = Object.freeze([BLOG_PREVIEW_DESIGN_SCOPE, 'post', 'page']);
59
+ export const BLOG_PAGE_STATUSES = Object.freeze(['draft', 'published']);
60
+ export const BLOG_PAGE_LAYOUTS = Object.freeze(['default', 'wide']);
61
+ export const BLOG_SNIPPET_KINDS = Object.freeze(['cta', 'html', 'newsletter']);
62
+ export const BLOG_SNIPPET_KINDS_WITH_REQUIRED_CONTENT = Object.freeze(['cta', 'html']);
63
+ export const BLOG_POSITION_MAX = 10_000;
64
+ export const BLOG_MAX_POSITIONS_PER_REQUEST = 100;
65
+ export const BLOG_REDIRECT_GONE_STATUS_CODE = 410;
66
+ export const BLOG_REDIRECT_STATUS_CODES = Object.freeze([301, 302, BLOG_REDIRECT_GONE_STATUS_CODE]);
67
+ export const BLOG_WEBHOOK_EVENTS = Object.freeze(['subscriber.created']);
68
+
48
69
  export const BILLING_MIN_ADDITIONAL_PRODUCTS = 1;
49
70
  export const BILLING_MAX_ADDITIONAL_PRODUCTS = 30;
50
71
  export const BILLING_MIN_CONFIRM_CHARGE_CENTS = 0;
package/src/core/files.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- import { MAX_IMAGE_UPLOAD_BYTES } from './constants.js';
4
+ import { MAX_IMAGE_UPLOAD_BYTES, MAX_TEXT_FILE_BYTES } from './constants.js';
5
5
  import { usageError } from './errors.js';
6
6
 
7
7
  const ImageMime = Object.freeze({
@@ -184,3 +184,39 @@ export const readImageUpload = async ({ filePath, allowedDir }) => {
184
184
 
185
185
  return toUpload(buffer, fileName);
186
186
  };
187
+
188
+ const UTF8_ENCODING = 'utf-8';
189
+ const BYTE_ORDER_MARK = '\uFEFF';
190
+ const LINE_BREAK_PATTERN = /\r\n?/g;
191
+ const LINE_FEED = '\n';
192
+ const TEXT_OVERSIZE_GUIDANCE = 'Split the content or shorten it below the limit, then retry.';
193
+ const TEXT_ENCODING_GUIDANCE = 'Save the file as UTF-8 text, then retry.';
194
+
195
+ export const readTextFile = async ({ filePath, option }) => {
196
+ if (typeof filePath !== 'string' || filePath === '') {
197
+ throw usageError(`Missing required option: ${option} <path>.`, `Pass a local file path with \`${option} <path>\`.`);
198
+ }
199
+ let buffer;
200
+ try {
201
+ const stats = await fs.stat(filePath);
202
+ if (!stats.isFile()) throw usageError(`Not a file: ${filePath}`, readGuidance(filePath));
203
+ if (stats.size > MAX_TEXT_FILE_BYTES) {
204
+ throw usageError(
205
+ `${filePath} is ${stats.size} bytes; the maximum is ${MAX_TEXT_FILE_BYTES} bytes.`,
206
+ TEXT_OVERSIZE_GUIDANCE,
207
+ );
208
+ }
209
+ buffer = await fs.readFile(filePath);
210
+ } catch (error) {
211
+ if (error?.cliCode) throw error;
212
+ throw usageError(`Failed to read file: ${error.message}`, readGuidance(filePath));
213
+ }
214
+ let text;
215
+ try {
216
+ text = new TextDecoder(UTF8_ENCODING, { fatal: true }).decode(buffer);
217
+ } catch {
218
+ throw usageError(`${filePath} is not valid UTF-8 text.`, TEXT_ENCODING_GUIDANCE);
219
+ }
220
+ const withoutBom = text.startsWith(BYTE_ORDER_MARK) ? text.slice(BYTE_ORDER_MARK.length) : text;
221
+ return withoutBom.replace(LINE_BREAK_PATTERN, LINE_FEED);
222
+ };
@@ -11,6 +11,7 @@ export const FieldKind = Object.freeze({
11
11
  OBJECT: 'object',
12
12
  JSON: 'json',
13
13
  IMAGE: 'image',
14
+ TEXT_FILE: 'text-file',
14
15
  });
15
16
 
16
17
  export const BoolSerialize = Object.freeze({
@@ -63,6 +64,7 @@ export const list = ({ split = DEFAULT_LIST_SEPARATOR, serialize = ListSerialize
63
64
  export const object = () => createField(FieldKind.OBJECT, { properties: undefined });
64
65
  export const json = () => createField(FieldKind.JSON);
65
66
  export const image = () => createField(FieldKind.IMAGE);
67
+ export const textFile = () => createField(FieldKind.TEXT_FILE);
66
68
 
67
69
  export const isField = (value) => Boolean(value) && Object.values(FieldKind).includes(value.kind);
68
70
 
@@ -124,6 +126,10 @@ const parseScalar = (field, value, label) => {
124
126
  if (!valid) throw usageError(`${label} must be an image upload (file_base64, file_name, file_type).`);
125
127
  return value;
126
128
  }
129
+ if (field.kind === FieldKind.TEXT_FILE) {
130
+ if (typeof value !== 'string') throw usageError(`${label} must be the path of a text file.`);
131
+ return value;
132
+ }
127
133
  throw new Error(`Unknown field kind: ${field.kind}`);
128
134
  };
129
135
 
@@ -254,6 +260,7 @@ const zodScalar = (z, field, options = {}) => {
254
260
  if (field.kind === FieldKind.IMAGE) {
255
261
  return z.object({ file_base64: z.string().min(1), file_name: z.string().min(1), file_type: z.string().min(1) });
256
262
  }
263
+ if (field.kind === FieldKind.TEXT_FILE) return z.string();
257
264
  throw new Error(`Unknown field kind: ${field.kind}`);
258
265
  };
259
266