@scrapecreators/cli 1.0.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 +241 -0
- package/api-config/apis.js +14559 -0
- package/api-config/instagram-apis.js +5753 -0
- package/api-config/tiktok-apis.js +17388 -0
- package/api-config/tiktok-shop-apis.js +1053 -0
- package/bin/check-lockfile-sync.js +42 -0
- package/bin/scrapecreators.js +4 -0
- package/package.json +48 -0
- package/src/api-client.js +71 -0
- package/src/auth.js +27 -0
- package/src/cli.js +78 -0
- package/src/command-registry.js +156 -0
- package/src/commands/agent.js +311 -0
- package/src/commands/api.js +49 -0
- package/src/commands/auth.js +69 -0
- package/src/commands/balance.js +40 -0
- package/src/commands/config.js +65 -0
- package/src/commands/list.js +52 -0
- package/src/config.js +23 -0
- package/src/interactive.js +148 -0
- package/src/output.js +400 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
|
|
4
|
+
function readJson(path) {
|
|
5
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeDeps(deps) {
|
|
9
|
+
return Object.fromEntries(Object.entries(deps || {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pkg = readJson(new URL("../package.json", import.meta.url));
|
|
13
|
+
const lock = readJson(new URL("../package-lock.json", import.meta.url));
|
|
14
|
+
|
|
15
|
+
const pkgDeps = normalizeDeps(pkg.dependencies);
|
|
16
|
+
const lockDeps = normalizeDeps(lock?.packages?.[""]?.dependencies);
|
|
17
|
+
|
|
18
|
+
const errors = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, version] of Object.entries(pkgDeps)) {
|
|
21
|
+
if (!(name in lockDeps)) {
|
|
22
|
+
errors.push(`missing in lockfile root deps: ${name}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (lockDeps[name] !== version) {
|
|
26
|
+
errors.push(`version mismatch for ${name}: package.json=${version}, package-lock.json=${lockDeps[name]}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const name of Object.keys(lockDeps)) {
|
|
31
|
+
if (!(name in pkgDeps)) {
|
|
32
|
+
errors.push(`extra lockfile root dependency not in package.json: ${name}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
console.error("lockfile check failed:");
|
|
38
|
+
for (const err of errors) console.error(`- ${err}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log("lockfile check passed.");
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scrapecreators/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for the ScrapeCreators API — scrape 27+ social media platforms from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scrapecreators": "./bin/scrapecreators.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/scrapecreators.js",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"check:lockfile": "node bin/check-lockfile-sync.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clack/prompts": "0.10.1",
|
|
16
|
+
"chalk": "5.6.2",
|
|
17
|
+
"cli-table3": "0.6.5",
|
|
18
|
+
"commander": "13.1.0",
|
|
19
|
+
"conf": "13.1.0",
|
|
20
|
+
"ora": "8.2.0"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin/",
|
|
27
|
+
"src/",
|
|
28
|
+
"api-config/",
|
|
29
|
+
"!api-config/withIcons.js"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"scraping",
|
|
33
|
+
"social-media",
|
|
34
|
+
"tiktok",
|
|
35
|
+
"instagram",
|
|
36
|
+
"youtube",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/ScrapeCreators/scrapecreators-cli"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://scrapecreators.com/",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"vitest": "^4.1.4"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const API_BASE = "https://api.scrapecreators.com";
|
|
2
|
+
|
|
3
|
+
// 50 MB — generous for JSON API responses, prevents memory exhaustion
|
|
4
|
+
const MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
export { MAX_RESPONSE_BYTES };
|
|
7
|
+
|
|
8
|
+
export async function readBodyWithLimit(res) {
|
|
9
|
+
const contentLength = parseInt(res.headers.get("content-length"), 10);
|
|
10
|
+
if (contentLength > MAX_RESPONSE_BYTES) {
|
|
11
|
+
throw new Error(`Response Content-Length (${contentLength}) exceeds ${MAX_RESPONSE_BYTES} byte limit`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let total = 0;
|
|
16
|
+
for await (const chunk of res.body) {
|
|
17
|
+
total += chunk.length;
|
|
18
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
19
|
+
throw new Error(`Response body exceeded ${MAX_RESPONSE_BYTES} byte limit`);
|
|
20
|
+
}
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function callApi(apiKey, method, path, params = {}) {
|
|
27
|
+
const url = new URL(`${API_BASE}${path}`);
|
|
28
|
+
|
|
29
|
+
if (method === "GET" && params) {
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
32
|
+
url.searchParams.set(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const options = {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
"x-api-key": apiKey,
|
|
41
|
+
accept: "application/json",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (method !== "GET" && params && Object.keys(params).length > 0) {
|
|
46
|
+
options.headers["content-type"] = "application/json";
|
|
47
|
+
options.body = JSON.stringify(params);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
options.signal = AbortSignal.timeout(60_000);
|
|
51
|
+
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
const res = await fetch(url.toString(), options);
|
|
54
|
+
const elapsed = Date.now() - start;
|
|
55
|
+
const text = await readBodyWithLimit(res);
|
|
56
|
+
|
|
57
|
+
let data;
|
|
58
|
+
try {
|
|
59
|
+
data = JSON.parse(text);
|
|
60
|
+
} catch {
|
|
61
|
+
data = text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: res.ok,
|
|
66
|
+
status: res.status,
|
|
67
|
+
data,
|
|
68
|
+
elapsed,
|
|
69
|
+
url: url.toString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import config from "./config.js";
|
|
2
|
+
|
|
3
|
+
export function resolveApiKey(opts = {}) {
|
|
4
|
+
if (opts.apiKey) return opts.apiKey;
|
|
5
|
+
const stored = config.get("apiKey");
|
|
6
|
+
if (stored) return stored;
|
|
7
|
+
if (process.env.SCRAPECREATORS_API_KEY) return process.env.SCRAPECREATORS_API_KEY;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function storeApiKey(key) {
|
|
12
|
+
config.set("apiKey", key);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clearApiKey() {
|
|
16
|
+
config.delete("apiKey");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getStoredApiKey() {
|
|
20
|
+
return config.get("apiKey") || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function maskKey(key) {
|
|
24
|
+
if (!key) return "(not set)";
|
|
25
|
+
if (key.length <= 12) return key.slice(0, 2) + "…" + key.slice(-2);
|
|
26
|
+
return key.slice(0, 6) + "…" + key.slice(-4);
|
|
27
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { registerApiCommands } from "./command-registry.js";
|
|
3
|
+
import { authLogin, authStatus, authLogout } from "./commands/auth.js";
|
|
4
|
+
import { configSet, configGet, configList } from "./commands/config.js";
|
|
5
|
+
import { balanceCommand } from "./commands/balance.js";
|
|
6
|
+
import { listCommand } from "./commands/list.js";
|
|
7
|
+
import { agentAddCommand } from "./commands/agent.js";
|
|
8
|
+
import { runInteractive } from "./interactive.js";
|
|
9
|
+
|
|
10
|
+
export function run(argv) {
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name("scrapecreators")
|
|
15
|
+
.description("CLI for the ScrapeCreators API — scrape 27+ social media platforms")
|
|
16
|
+
.version("1.0.0")
|
|
17
|
+
.option("--api-key <key>", "API key (overrides env and config)")
|
|
18
|
+
.option("--format <format>", "output format: json, table, csv, markdown", "auto")
|
|
19
|
+
.option("--json", "shorthand for compact JSON (default)")
|
|
20
|
+
.option("--pretty", "pretty-print JSON with indentation")
|
|
21
|
+
.option("--output <path>", "save response to file, print path only")
|
|
22
|
+
.option("--clean", "strip noisy fields: booleans, empty values, settings (any format)")
|
|
23
|
+
.option("--no-color", "disable ANSI colors")
|
|
24
|
+
.option("--verbose", "show request URL, timing, credits used");
|
|
25
|
+
|
|
26
|
+
const globalOptsGetter = () => program.opts();
|
|
27
|
+
|
|
28
|
+
// --- API commands (dynamic from api-config) ---
|
|
29
|
+
registerApiCommands(program, globalOptsGetter);
|
|
30
|
+
|
|
31
|
+
// --- auth ---
|
|
32
|
+
const authCmd = program.command("auth").description("manage authentication");
|
|
33
|
+
authCmd.command("login").description("set your API key").action(() => authLogin());
|
|
34
|
+
authCmd.command("status").description("show current auth status").action(() => authStatus());
|
|
35
|
+
authCmd.command("logout").description("remove stored API key").action(() => authLogout());
|
|
36
|
+
|
|
37
|
+
// --- balance ---
|
|
38
|
+
program
|
|
39
|
+
.command("balance")
|
|
40
|
+
.description("check credit balance")
|
|
41
|
+
.action(() => balanceCommand(globalOptsGetter()));
|
|
42
|
+
|
|
43
|
+
// --- config ---
|
|
44
|
+
const configCmd = program.command("config").description("manage CLI configuration");
|
|
45
|
+
configCmd
|
|
46
|
+
.command("set <key> <value>")
|
|
47
|
+
.description("set a config value")
|
|
48
|
+
.action((key, value) => configSet(key, value));
|
|
49
|
+
configCmd
|
|
50
|
+
.command("get <key>")
|
|
51
|
+
.description("get a config value")
|
|
52
|
+
.action((key) => configGet(key));
|
|
53
|
+
configCmd
|
|
54
|
+
.command("list")
|
|
55
|
+
.description("show all config values")
|
|
56
|
+
.action(() => configList());
|
|
57
|
+
|
|
58
|
+
// --- list ---
|
|
59
|
+
program
|
|
60
|
+
.command("list [platform]")
|
|
61
|
+
.description("list available platforms and endpoints")
|
|
62
|
+
.action((platform) => listCommand(platform));
|
|
63
|
+
|
|
64
|
+
// --- agent-first: agent add ---
|
|
65
|
+
const agentCmd = program.command("agent").description("configure AI agent integrations");
|
|
66
|
+
agentCmd
|
|
67
|
+
.command("add <target>")
|
|
68
|
+
.description("write MCP config into an agent (cursor, claude, codex)")
|
|
69
|
+
.action((target) => agentAddCommand(target, globalOptsGetter()));
|
|
70
|
+
|
|
71
|
+
// if no args and stdin is a TTY, launch interactive mode
|
|
72
|
+
if (argv.length <= 2 && process.stdin.isTTY) {
|
|
73
|
+
runInteractive(globalOptsGetter());
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
program.parse(argv);
|
|
78
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { apis } from "../api-config/apis.js";
|
|
2
|
+
import { handleApiCommand } from "./commands/api.js";
|
|
3
|
+
|
|
4
|
+
function buildParamDescription(p) {
|
|
5
|
+
const base = p.description || "";
|
|
6
|
+
if (p.placeholder === undefined || p.placeholder === "") return base;
|
|
7
|
+
const val = String(p.placeholder);
|
|
8
|
+
if (p.type === "boolean" || val === "null" || val === "false" || val === "true") return base;
|
|
9
|
+
return `${base} (e.g. ${val})`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const toolDefs = apis.flatMap((api) =>
|
|
13
|
+
api.endpoints.map((ep) => ({
|
|
14
|
+
name: ep.path.replace(/^\//, "").replace(/\//g, "_").replace(/-/g, "_"),
|
|
15
|
+
title: `${api.name} - ${ep.name}`,
|
|
16
|
+
description: ep.fullDescription || ep.description,
|
|
17
|
+
method: ep.method,
|
|
18
|
+
path: ep.path,
|
|
19
|
+
params: (ep.params || []).map((p) => ({
|
|
20
|
+
name: p.name,
|
|
21
|
+
type: p.type,
|
|
22
|
+
required: !!p.required,
|
|
23
|
+
description: buildParamDescription(p),
|
|
24
|
+
...(p.type === "select" && p.options ? { options: p.options } : {}),
|
|
25
|
+
...(p.default !== undefined ? { default: p.default } : {}),
|
|
26
|
+
})),
|
|
27
|
+
...(ep.paginationField ? { paginationField: ep.paginationField } : {}),
|
|
28
|
+
...(ep.credits ? { credits: ep.credits } : {}),
|
|
29
|
+
}))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// /v1/tiktok/profile -> { platform: "tiktok", action: "profile" }
|
|
33
|
+
// /v3/tiktok/profile/videos -> { platform: "tiktok", action: "profile-videos" }
|
|
34
|
+
// /v1/facebook/adLibrary/search/ads -> { platform: "facebook", action: "adlibrary-search-ads" }
|
|
35
|
+
// /v1/linktree -> { platform: "linktree", action: "get" }
|
|
36
|
+
// /v1/detect-age-gender -> { platform: "detect", action: "age-gender" }
|
|
37
|
+
// /v1/credit-balance -> { platform: "credit", action: "balance" }
|
|
38
|
+
function parseToolPath(path) {
|
|
39
|
+
// handle hyphenated single-segment paths like /v1/detect-age-gender, /v1/credit-balance
|
|
40
|
+
const stripped = path.replace(/^\//, "");
|
|
41
|
+
const parts = stripped.split("/");
|
|
42
|
+
|
|
43
|
+
if (parts.length === 2 && parts[1].includes("-")) {
|
|
44
|
+
const idx = parts[1].indexOf("-");
|
|
45
|
+
return {
|
|
46
|
+
platform: parts[1].slice(0, idx),
|
|
47
|
+
action: parts[1].slice(idx + 1),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const platform = parts[1];
|
|
52
|
+
const actionParts = parts.slice(2);
|
|
53
|
+
// if no action segments (e.g. /v1/linktree), use "get" as default
|
|
54
|
+
const action = actionParts.length > 0
|
|
55
|
+
? actionParts.join("-").toLowerCase()
|
|
56
|
+
: "get";
|
|
57
|
+
return { platform, action };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// pick latest version when multiple exist for same platform+action
|
|
61
|
+
function deduplicateTools(tools) {
|
|
62
|
+
const map = new Map();
|
|
63
|
+
for (const tool of tools) {
|
|
64
|
+
const { platform, action } = parseToolPath(tool.path);
|
|
65
|
+
const key = `${platform}/${action}`;
|
|
66
|
+
const existing = map.get(key);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
map.set(key, tool);
|
|
69
|
+
} else {
|
|
70
|
+
const existingVersion = parseInt(existing.path.match(/^\/v(\d+)/)?.[1] || "1");
|
|
71
|
+
const newVersion = parseInt(tool.path.match(/^\/v(\d+)/)?.[1] || "1");
|
|
72
|
+
if (newVersion > existingVersion) {
|
|
73
|
+
map.set(key, tool);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...map.values()];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function snakeToCli(name) {
|
|
81
|
+
return name.replace(/_/g, "-");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cliToCamel(str) {
|
|
85
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function registerApiCommands(program, globalOptsGetter) {
|
|
89
|
+
const tools = deduplicateTools(toolDefs);
|
|
90
|
+
|
|
91
|
+
// group by platform
|
|
92
|
+
const platforms = new Map();
|
|
93
|
+
for (const tool of tools) {
|
|
94
|
+
const { platform, action } = parseToolPath(tool.path);
|
|
95
|
+
if (!platforms.has(platform)) platforms.set(platform, []);
|
|
96
|
+
platforms.get(platform).push({ ...tool, _action: action });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const [platform, tools] of platforms) {
|
|
100
|
+
// skip credit/balance - handled as `scrapecreators balance`
|
|
101
|
+
if (platform === "credit") continue;
|
|
102
|
+
|
|
103
|
+
const platCmd = program
|
|
104
|
+
.command(platform)
|
|
105
|
+
.description(`${platform} endpoints`);
|
|
106
|
+
|
|
107
|
+
for (const tool of tools) {
|
|
108
|
+
const actionCmd = platCmd
|
|
109
|
+
.command(tool._action)
|
|
110
|
+
.description(tool.title);
|
|
111
|
+
|
|
112
|
+
for (const param of tool.params) {
|
|
113
|
+
const flag = snakeToCli(param.name);
|
|
114
|
+
const camel = cliToCamel(flag);
|
|
115
|
+
|
|
116
|
+
if (param.type === "boolean") {
|
|
117
|
+
actionCmd.option(`--${flag}`, param.description);
|
|
118
|
+
} else if (param.type === "select" && param.options?.length) {
|
|
119
|
+
actionCmd.option(
|
|
120
|
+
`--${flag} <value>`,
|
|
121
|
+
`${param.description} (${param.options.join("|")})`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
const bracket = param.required ? `<${flag}>` : `[${flag}]`;
|
|
125
|
+
actionCmd.option(`--${flag} ${bracket}`, param.description);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
actionCmd.action(async (cmdOpts) => {
|
|
130
|
+
const globalOpts = globalOptsGetter();
|
|
131
|
+
await handleApiCommand(tool, cmdOpts, globalOpts);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return platforms;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getToolDefs() {
|
|
140
|
+
return toolDefs;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getDeduplicatedTools() {
|
|
144
|
+
return deduplicateTools(toolDefs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getPlatformMap() {
|
|
148
|
+
const tools = deduplicateTools(toolDefs);
|
|
149
|
+
const platforms = new Map();
|
|
150
|
+
for (const tool of tools) {
|
|
151
|
+
const { platform, action } = parseToolPath(tool.path);
|
|
152
|
+
if (!platforms.has(platform)) platforms.set(platform, []);
|
|
153
|
+
platforms.get(platform).push({ ...tool, _action: action });
|
|
154
|
+
}
|
|
155
|
+
return platforms;
|
|
156
|
+
}
|