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.
- package/README.md +898 -817
- package/SKILL.md +41 -3
- package/package.json +1 -1
- package/src/adapters/cli/run.js +12 -5
- package/src/adapters/mcp/help.js +1 -1
- package/src/adapters/mcp/input.js +1 -0
- package/src/adapters/mcp/toolNotes.js +162 -1
- package/src/commands/articles.js +91 -4
- package/src/commands/products/blog.js +971 -0
- package/src/commands/products/index.js +9 -1
- package/src/commands/registry.js +1 -0
- package/src/core/constants.js +21 -0
- package/src/core/files.js +37 -1
- package/src/core/schema.js +7 -0
|
@@ -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
|
});
|
package/src/commands/registry.js
CHANGED
package/src/core/constants.js
CHANGED
|
@@ -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
|
+
};
|
package/src/core/schema.js
CHANGED
|
@@ -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
|
|