socialdatax-skills 0.2.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/LICENSE +21 -0
- package/README.md +257 -0
- package/assets/logo.png +0 -0
- package/cli.mjs +1457 -0
- package/package.json +55 -0
- package/skills/media-comments/SKILL.md +71 -0
- package/skills/media-comments/agents/openai.yaml +7 -0
- package/skills/media-detail/SKILL.md +66 -0
- package/skills/media-detail/agents/openai.yaml +7 -0
- package/skills/media-search/SKILL.md +134 -0
- package/skills/media-search/agents/openai.yaml +7 -0
- package/skills/media-user-info/SKILL.md +66 -0
- package/skills/media-user-info/agents/openai.yaml +7 -0
- package/skills/media-user-posts/SKILL.md +69 -0
- package/skills/media-user-posts/agents/openai.yaml +7 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { cp, mkdir, readFile, rm, stat } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = "socialdatax-skills";
|
|
10
|
+
const PACKAGE_VERSION = "0.2.0";
|
|
11
|
+
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
12
|
+
const LOG_PREFIX = `[${PACKAGE_NAME}]`;
|
|
13
|
+
const MIN_NODE_VERSION = "20.18.1";
|
|
14
|
+
const HOMEPAGE_URL = "https://socialdatax.com";
|
|
15
|
+
const PRIMARY_API_KEY_ENV = "SOCIALDATAX_API_KEY";
|
|
16
|
+
const LEGACY_API_KEY_ENV = "SOCIAL_MEDIA_MCP_API_KEY";
|
|
17
|
+
const API_KEY_ENV_NAMES = [PRIMARY_API_KEY_ENV, LEGACY_API_KEY_ENV];
|
|
18
|
+
const AVAILABLE_SKILLS = [
|
|
19
|
+
{
|
|
20
|
+
name: "media-search",
|
|
21
|
+
summary: "Search XHS notes and Douyin works by keyword with optional filters.",
|
|
22
|
+
emoji: "🔍",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "media-detail",
|
|
26
|
+
summary: "Read structured content details and metrics for XHS and Douyin.",
|
|
27
|
+
emoji: "📄",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "media-comments",
|
|
31
|
+
summary: "Fetch and analyze XHS comments/replies and Douyin first-level comments.",
|
|
32
|
+
emoji: "💬",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "media-user-info",
|
|
36
|
+
summary: "Retrieve creator profile information for XHS and Douyin.",
|
|
37
|
+
emoji: "👤",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "media-user-posts",
|
|
41
|
+
summary: "Retrieve creator content lists for XHS and Douyin.",
|
|
42
|
+
emoji: "🗂️",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const AVAILABLE_SKILL_NAMES = AVAILABLE_SKILLS.map((skill) => skill.name);
|
|
46
|
+
const BOOLEAN_OPTIONS = new Set(["dryRun", "force", "json", "pretty"]);
|
|
47
|
+
const INSTALL_TARGETS = ["openclaw", "hermes", "agents", "codex", "claude-code", "claude"];
|
|
48
|
+
const VALID_SCOPES = ["user", "workspace", "shared"];
|
|
49
|
+
const XHS_DIRECT_ACTION_OPTIONS = {
|
|
50
|
+
search: ["keyword", "page", "sortType", "noteType", "publishTimeRange", "pretty"],
|
|
51
|
+
detail: ["noteId", "url", "pretty"],
|
|
52
|
+
comments: ["noteId", "url", "pageToken", "pretty"],
|
|
53
|
+
"sub-comments": ["noteId", "commentId", "pageToken", "pretty"],
|
|
54
|
+
"user-info": ["userId", "profileUrl", "pretty"],
|
|
55
|
+
"user-posts": ["userId", "profileUrl", "pageToken", "pretty"],
|
|
56
|
+
};
|
|
57
|
+
const XHS_OPTION_DISPLAY_NAMES = {
|
|
58
|
+
keyword: "--keyword",
|
|
59
|
+
page: "--page",
|
|
60
|
+
sortType: "--sort-type",
|
|
61
|
+
noteType: "--note-type",
|
|
62
|
+
publishTimeRange: "--publish-time-range",
|
|
63
|
+
url: "--url",
|
|
64
|
+
noteId: "--note-id",
|
|
65
|
+
commentId: "--comment-id",
|
|
66
|
+
pageToken: "--page-token",
|
|
67
|
+
profileUrl: "--profile-url",
|
|
68
|
+
userId: "--user-id",
|
|
69
|
+
};
|
|
70
|
+
const XHS_SEARCH_SORT_TYPES = [
|
|
71
|
+
"general",
|
|
72
|
+
"time_descending",
|
|
73
|
+
"like_count_descending",
|
|
74
|
+
"comment_count_descending",
|
|
75
|
+
"collect_count_descending",
|
|
76
|
+
];
|
|
77
|
+
const XHS_LEGACY_SEARCH_SORT_TYPE_ALIASES = {
|
|
78
|
+
popularity_descending: "like_count_descending",
|
|
79
|
+
comment_descending: "comment_count_descending",
|
|
80
|
+
collect_descending: "collect_count_descending",
|
|
81
|
+
};
|
|
82
|
+
const DOUYIN_DIRECT_ACTION_OPTIONS = {
|
|
83
|
+
search: [
|
|
84
|
+
"keyword",
|
|
85
|
+
"pageToken",
|
|
86
|
+
"sortType",
|
|
87
|
+
"publishTimeRange",
|
|
88
|
+
"durationRange",
|
|
89
|
+
"contentType",
|
|
90
|
+
"pretty",
|
|
91
|
+
],
|
|
92
|
+
detail: ["awemeId", "url", "pretty"],
|
|
93
|
+
comments: ["awemeId", "url", "pageToken", "pretty"],
|
|
94
|
+
"user-info": ["secUserId", "profileUrl", "pretty"],
|
|
95
|
+
"user-posts": ["secUserId", "profileUrl", "pageToken", "pretty"],
|
|
96
|
+
};
|
|
97
|
+
const DOUYIN_SEARCH_SORT_TYPES = ["general", "time_descending", "like_count_descending"];
|
|
98
|
+
const DOUYIN_SEARCH_PUBLISH_TIME_RANGES = ["all", "day", "week", "half_year"];
|
|
99
|
+
const DOUYIN_SEARCH_DURATION_RANGES = [
|
|
100
|
+
"all",
|
|
101
|
+
"under_1_minute",
|
|
102
|
+
"one_to_five_minutes",
|
|
103
|
+
"over_5_minutes",
|
|
104
|
+
];
|
|
105
|
+
const DOUYIN_SEARCH_CONTENT_TYPES = ["all", "video", "image"];
|
|
106
|
+
const DOUYIN_OPTION_DISPLAY_NAMES = {
|
|
107
|
+
keyword: "--keyword",
|
|
108
|
+
pageToken: "--page-token",
|
|
109
|
+
sortType: "--sort-type",
|
|
110
|
+
publishTimeRange: "--publish-time-range",
|
|
111
|
+
durationRange: "--duration-range",
|
|
112
|
+
contentType: "--content-type",
|
|
113
|
+
url: "--url",
|
|
114
|
+
profileUrl: "--profile-url",
|
|
115
|
+
awemeId: "--aweme-id",
|
|
116
|
+
commentId: "--comment-id",
|
|
117
|
+
secUserId: "--sec-user-id",
|
|
118
|
+
};
|
|
119
|
+
const PLATFORMS = {
|
|
120
|
+
xhs: {
|
|
121
|
+
id: "xhs",
|
|
122
|
+
displayName: "XHS / Xiaohongshu / RedNote",
|
|
123
|
+
status: "public",
|
|
124
|
+
registryName: "com.socialdatax/xhs-insights",
|
|
125
|
+
legacyRegistryName: "com.52choujiang/xhs-insights",
|
|
126
|
+
endpoint: "https://mcp.52choujiang.com/xhs/mcp",
|
|
127
|
+
apiKeyEnv: API_KEY_ENV_NAMES,
|
|
128
|
+
upstreamEnv: [
|
|
129
|
+
"SOCIAL_MEDIA_XHS_MCP_UPSTREAM_URL",
|
|
130
|
+
"SOCIAL_MEDIA_MCP_UPSTREAM_URL",
|
|
131
|
+
"XHS_MCP_UPSTREAM_URL",
|
|
132
|
+
],
|
|
133
|
+
tools: [
|
|
134
|
+
{
|
|
135
|
+
name: "xhs_search_notes",
|
|
136
|
+
description:
|
|
137
|
+
"Search Xiaohongshu / XHS / RedNote notes by keyword with optional sort and filters.",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "xhs_get_note_detail_by_note_url",
|
|
141
|
+
description:
|
|
142
|
+
"Resolve a note link, short link, share text, or note ID into structured details.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "xhs_get_note_detail_by_note_id",
|
|
146
|
+
description: "Fetch structured note details by note ID.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "xhs_get_note_comments_by_note_id",
|
|
150
|
+
description: "Fetch paginated first-level comments by note ID.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "xhs_get_note_comments_by_note_url",
|
|
154
|
+
description:
|
|
155
|
+
"Fetch paginated first-level comments from a note URL, short link, or share text.",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "xhs_get_note_sub_comments_by_comment_id",
|
|
159
|
+
description:
|
|
160
|
+
"Fetch paginated replies under a first-level comment by note ID and comment ID.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "xhs_get_user_info_by_user_id",
|
|
164
|
+
description: "Fetch creator profile data by user ID.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "xhs_get_user_info_by_profile_url",
|
|
168
|
+
description:
|
|
169
|
+
"Resolve a profile link, short link, or share text into creator data.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "xhs_get_user_posted_notes_by_user_id",
|
|
173
|
+
description: "Fetch a paginated list of notes published by a creator.",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "xhs_get_user_posted_notes_by_profile_url",
|
|
177
|
+
description:
|
|
178
|
+
"Fetch creator notes from a profile link, short link, or share text.",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
douyin: {
|
|
183
|
+
id: "douyin",
|
|
184
|
+
displayName: "Douyin / 抖音",
|
|
185
|
+
status: "public",
|
|
186
|
+
registryName: "com.socialdatax/douyin-insights",
|
|
187
|
+
legacyRegistryName: "com.52choujiang/douyin-insights",
|
|
188
|
+
endpoint: "https://mcp.52choujiang.com/douyin/mcp",
|
|
189
|
+
apiKeyEnv: API_KEY_ENV_NAMES,
|
|
190
|
+
upstreamEnv: [
|
|
191
|
+
"SOCIAL_MEDIA_DOUYIN_MCP_UPSTREAM_URL",
|
|
192
|
+
"SOCIAL_MEDIA_MCP_UPSTREAM_URL",
|
|
193
|
+
"DOUYIN_MCP_UPSTREAM_URL",
|
|
194
|
+
],
|
|
195
|
+
tools: [
|
|
196
|
+
{
|
|
197
|
+
name: "douyin_get_video_detail_by_aweme_id",
|
|
198
|
+
description: "Fetch structured Douyin work details by aweme_id.",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "douyin_get_video_detail_by_url",
|
|
202
|
+
description: "Resolve a Douyin content page link, short link, or share text into structured details.",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "douyin_get_video_comments_by_aweme_id",
|
|
206
|
+
description: "Fetch paginated first-level comments by aweme_id.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "douyin_get_video_comments_by_url",
|
|
210
|
+
description: "Fetch paginated first-level comments from a Douyin content page link, short link, or share text.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "douyin_get_user_info_by_sec_user_id",
|
|
214
|
+
description: "Fetch creator profile data by sec_user_id.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "douyin_get_user_info_by_profile_url",
|
|
218
|
+
description: "Resolve a Douyin profile link, short link, or share text into creator profile data.",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "douyin_get_user_posted_videos_by_sec_user_id",
|
|
222
|
+
description: "Fetch a paginated list of works published by a creator.",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "douyin_get_user_posted_videos_by_profile_url",
|
|
226
|
+
description: "Fetch creator works from a profile link, short link, or share text.",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "douyin_search_videos",
|
|
230
|
+
description: "Search Douyin works by keyword with optional paging and filters.",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
236
|
+
let mcpSdkModules;
|
|
237
|
+
|
|
238
|
+
const cliArgs = process.argv.slice(2);
|
|
239
|
+
const command = cliArgs[0];
|
|
240
|
+
|
|
241
|
+
function isMainModule() {
|
|
242
|
+
if (!process.argv[1]) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return (
|
|
247
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
248
|
+
realpathSync(resolve(process.argv[1]))
|
|
249
|
+
);
|
|
250
|
+
} catch {
|
|
251
|
+
return fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isMainModule()) {
|
|
256
|
+
try {
|
|
257
|
+
if (command === "install") {
|
|
258
|
+
await installSkills(cliArgs.slice(1));
|
|
259
|
+
} else if (command === "list") {
|
|
260
|
+
listSkills();
|
|
261
|
+
} else if (command === "doctor" || command === "verify") {
|
|
262
|
+
printDoctor(cliArgs.slice(1));
|
|
263
|
+
} else if (command === "xhs") {
|
|
264
|
+
await runXhsDirectCommand(cliArgs.slice(1));
|
|
265
|
+
} else if (command === "douyin") {
|
|
266
|
+
await runDouyinDirectCommand(cliArgs.slice(1));
|
|
267
|
+
} else if (command === "--platform" || command?.startsWith("--platform=") || command === "print-config") {
|
|
268
|
+
printRemovedMcpConfigHelp(command);
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
} else if (command === "--help" || command === "-h" || command === "help") {
|
|
271
|
+
printHelp();
|
|
272
|
+
} else if (!command) {
|
|
273
|
+
printHelp();
|
|
274
|
+
} else {
|
|
275
|
+
console.error(`${LOG_PREFIX} Unknown command: ${command}`);
|
|
276
|
+
console.error("");
|
|
277
|
+
printHelp();
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(`${LOG_PREFIX} ${error.message}`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseCommandArgs(args) {
|
|
287
|
+
const options = {};
|
|
288
|
+
const positional = [];
|
|
289
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
290
|
+
const arg = args[index];
|
|
291
|
+
if (!arg.startsWith("--")) {
|
|
292
|
+
positional.push(arg);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const optionText = arg.slice(2);
|
|
297
|
+
const equalsIndex = optionText.indexOf("=");
|
|
298
|
+
const rawKey =
|
|
299
|
+
equalsIndex === -1 ? optionText : optionText.slice(0, equalsIndex);
|
|
300
|
+
const inlineValue =
|
|
301
|
+
equalsIndex === -1 ? undefined : optionText.slice(equalsIndex + 1);
|
|
302
|
+
const key = rawKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
303
|
+
if (BOOLEAN_OPTIONS.has(key)) {
|
|
304
|
+
options[key] = inlineValue ?? true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (inlineValue !== undefined) {
|
|
309
|
+
options[key] = inlineValue;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const next = args[index + 1];
|
|
314
|
+
if (next && !next.startsWith("--")) {
|
|
315
|
+
options[key] = next;
|
|
316
|
+
index += 1;
|
|
317
|
+
} else {
|
|
318
|
+
options[key] = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { options, positional };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseOptions(args) {
|
|
325
|
+
return parseCommandArgs(args).options;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function toKebabCase(key) {
|
|
329
|
+
return key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateKnownOptions(options, allowedOptions) {
|
|
333
|
+
for (const key of Object.keys(options)) {
|
|
334
|
+
if (!allowedOptions.includes(key)) {
|
|
335
|
+
throw new Error(`Unsupported option --${toKebabCase(key)}.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function requireOptionValue(options, key, displayName) {
|
|
341
|
+
if (options[key] === true || options[key] === "") {
|
|
342
|
+
throw new Error(`Missing value for ${displayName}.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function validateFlagOption(options, key, displayName) {
|
|
347
|
+
if (options[key] !== undefined && options[key] !== true) {
|
|
348
|
+
throw new Error(`${displayName} does not take a value.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function validateXhsDirectActionOptions(action, options) {
|
|
353
|
+
const allowedOptions = XHS_DIRECT_ACTION_OPTIONS[action];
|
|
354
|
+
if (!allowedOptions) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
validateKnownOptions(options, allowedOptions);
|
|
359
|
+
validateFlagOption(options, "pretty", "--pretty");
|
|
360
|
+
for (const key of allowedOptions) {
|
|
361
|
+
if (key !== "pretty") {
|
|
362
|
+
requireOptionValue(options, key, XHS_OPTION_DISPLAY_NAMES[key]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function validateDouyinDirectActionOptions(action, options) {
|
|
368
|
+
const allowedOptions = DOUYIN_DIRECT_ACTION_OPTIONS[action];
|
|
369
|
+
if (!allowedOptions) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
validateKnownOptions(options, allowedOptions);
|
|
374
|
+
validateFlagOption(options, "pretty", "--pretty");
|
|
375
|
+
for (const key of allowedOptions) {
|
|
376
|
+
if (key !== "pretty") {
|
|
377
|
+
requireOptionValue(options, key, DOUYIN_OPTION_DISPLAY_NAMES[key]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parsePositiveIntegerOption(value, displayName) {
|
|
383
|
+
const text = String(value);
|
|
384
|
+
if (!/^\d+$/.test(text)) {
|
|
385
|
+
throw new Error(`${displayName} must be an integer greater than or equal to 1.`);
|
|
386
|
+
}
|
|
387
|
+
const parsed = Number(text);
|
|
388
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
389
|
+
throw new Error(`${displayName} must be an integer greater than or equal to 1.`);
|
|
390
|
+
}
|
|
391
|
+
return parsed;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseAllowedStringOption(value, displayName, allowedValues, label) {
|
|
395
|
+
const normalized = String(value).trim();
|
|
396
|
+
if (!allowedValues.includes(normalized)) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Unsupported ${displayName} "${normalized}". Use one of: ${label}.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return normalized;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseSemanticOption(value, displayName, allowedValues, legacyAliases, label) {
|
|
405
|
+
const normalized = String(value).trim();
|
|
406
|
+
const canonical = legacyAliases[normalized] || normalized;
|
|
407
|
+
if (!allowedValues.includes(canonical)) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Unsupported ${displayName} "${normalized}". Use one of: ${label}.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return canonical;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function validateInstallTarget(target) {
|
|
416
|
+
if (!INSTALL_TARGETS.includes(target)) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Unsupported --target "${target}". Use one of: openclaw, hermes, agents, codex, claude-code, or claude.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseNodeVersion(version) {
|
|
424
|
+
return version
|
|
425
|
+
.replace(/^v/, "")
|
|
426
|
+
.split(".")
|
|
427
|
+
.map((part) => Number.parseInt(part, 10));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function ensureSupportedNodeVersion() {
|
|
431
|
+
const current = parseNodeVersion(process.versions.node);
|
|
432
|
+
const minimum = parseNodeVersion(MIN_NODE_VERSION);
|
|
433
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
434
|
+
const currentPart = current[index] || 0;
|
|
435
|
+
const minimumPart = minimum[index] || 0;
|
|
436
|
+
if (currentPart > minimumPart) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (currentPart < minimumPart) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Node.js ${MIN_NODE_VERSION} or newer is required. Current version: ${process.version}.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function validateScope(scope) {
|
|
448
|
+
if (!VALID_SCOPES.includes(scope)) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Unsupported --scope "${scope}". Use one of: ${VALID_SCOPES.join(", ")}.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function validateTargetScope(target, scope, hasCustomPath) {
|
|
456
|
+
if (hasCustomPath) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (scope === "shared" && target !== "hermes") {
|
|
460
|
+
throw new Error(
|
|
461
|
+
'--scope shared is only supported with --target hermes. Use --target agents for the shared AgentSkills directory.'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function readFirstEnv(names) {
|
|
467
|
+
for (const name of names) {
|
|
468
|
+
const value = process.env[name]?.trim();
|
|
469
|
+
if (value) {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function resolveUpstreamUrl(platform) {
|
|
477
|
+
return readFirstEnv(platform.upstreamEnv) || platform.endpoint;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function expandHome(path) {
|
|
481
|
+
if (!path || path === "~") {
|
|
482
|
+
return homedir();
|
|
483
|
+
}
|
|
484
|
+
if (path.startsWith("~/")) {
|
|
485
|
+
return join(homedir(), path.slice(2));
|
|
486
|
+
}
|
|
487
|
+
return path;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function resolveInstallDir({ target, scope, customPath, skillName }) {
|
|
491
|
+
if (customPath) {
|
|
492
|
+
return resolve(expandHome(customPath));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
switch (target) {
|
|
496
|
+
case "openclaw":
|
|
497
|
+
if (scope === "workspace") {
|
|
498
|
+
return resolve(process.cwd(), "skills", skillName);
|
|
499
|
+
}
|
|
500
|
+
return join(
|
|
501
|
+
process.env.OPENCLAW_SKILLS_DIR ||
|
|
502
|
+
join(homedir(), ".openclaw", "workspace", "skills"),
|
|
503
|
+
skillName
|
|
504
|
+
);
|
|
505
|
+
case "hermes":
|
|
506
|
+
if (scope === "workspace") {
|
|
507
|
+
return resolve(process.cwd(), "skills", skillName);
|
|
508
|
+
}
|
|
509
|
+
if (scope === "shared") {
|
|
510
|
+
return join(homedir(), ".agents", "skills", skillName);
|
|
511
|
+
}
|
|
512
|
+
return join(homedir(), ".hermes", "skills", skillName);
|
|
513
|
+
case "agents":
|
|
514
|
+
if (scope === "workspace") {
|
|
515
|
+
return resolve(process.cwd(), "skills", skillName);
|
|
516
|
+
}
|
|
517
|
+
return join(homedir(), ".agents", "skills", skillName);
|
|
518
|
+
case "codex":
|
|
519
|
+
if (scope === "workspace") {
|
|
520
|
+
return resolve(process.cwd(), ".codex", "skills", skillName);
|
|
521
|
+
}
|
|
522
|
+
return join(homedir(), ".codex", "skills", skillName);
|
|
523
|
+
case "claude-code":
|
|
524
|
+
case "claude":
|
|
525
|
+
if (scope === "workspace") {
|
|
526
|
+
return resolve(process.cwd(), ".claude", "skills", skillName);
|
|
527
|
+
}
|
|
528
|
+
return join(homedir(), ".claude", "skills", skillName);
|
|
529
|
+
default:
|
|
530
|
+
throw new Error(
|
|
531
|
+
"Missing or unsupported --target. Use openclaw, hermes, agents, codex, claude-code, or claude."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resolveSkillNames(positional) {
|
|
537
|
+
if (positional.length === 0) {
|
|
538
|
+
return AVAILABLE_SKILL_NAMES;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
for (const skillName of positional) {
|
|
542
|
+
if (!AVAILABLE_SKILL_NAMES.includes(skillName)) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Unsupported skill "${skillName}". Available skills: ${AVAILABLE_SKILL_NAMES.join(", ")}.`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return positional;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function installSkills(args) {
|
|
552
|
+
const { options, positional } = parseCommandArgs(args);
|
|
553
|
+
validateKnownOptions(options, ["target", "scope", "path", "force", "dryRun"]);
|
|
554
|
+
const target = options.target;
|
|
555
|
+
const scope = options.scope || "user";
|
|
556
|
+
requireOptionValue(options, "target", "--target");
|
|
557
|
+
requireOptionValue(options, "scope", "--scope");
|
|
558
|
+
requireOptionValue(options, "path", "--path");
|
|
559
|
+
validateFlagOption(options, "force", "--force");
|
|
560
|
+
validateFlagOption(options, "dryRun", "--dry-run");
|
|
561
|
+
if (!target && !options.path) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
"Missing --target. Use openclaw, hermes, agents, codex, claude-code, or claude; or provide --path."
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
if (target) {
|
|
567
|
+
validateInstallTarget(target);
|
|
568
|
+
}
|
|
569
|
+
validateScope(scope);
|
|
570
|
+
validateTargetScope(target, scope, Boolean(options.path));
|
|
571
|
+
const skillNames = resolveSkillNames(positional);
|
|
572
|
+
const dryRun = Boolean(options.dryRun);
|
|
573
|
+
const installed = [];
|
|
574
|
+
|
|
575
|
+
for (const skillName of skillNames) {
|
|
576
|
+
const destination = resolveInstallDestination({
|
|
577
|
+
skillName,
|
|
578
|
+
target,
|
|
579
|
+
scope,
|
|
580
|
+
path: options.path,
|
|
581
|
+
usePathAsParent: skillNames.length > 1,
|
|
582
|
+
});
|
|
583
|
+
if (dryRun) {
|
|
584
|
+
await validateInstallPlan({
|
|
585
|
+
skillName,
|
|
586
|
+
destination,
|
|
587
|
+
force: options.force,
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
await installOneSkill({
|
|
591
|
+
skillName,
|
|
592
|
+
destination,
|
|
593
|
+
force: options.force,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
installed.push({ skillName, destination });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (dryRun) {
|
|
600
|
+
console.log(
|
|
601
|
+
`Dry run: would install ${installed.length} skill${
|
|
602
|
+
installed.length === 1 ? "" : "s"
|
|
603
|
+
} for ${target || "custom"}:`
|
|
604
|
+
);
|
|
605
|
+
for (const item of installed) {
|
|
606
|
+
const suffix = existsSync(item.destination)
|
|
607
|
+
? options.force
|
|
608
|
+
? " (would replace)"
|
|
609
|
+
: " (exists)"
|
|
610
|
+
: "";
|
|
611
|
+
console.log(`- ${item.skillName}: ${item.destination}${suffix}`);
|
|
612
|
+
}
|
|
613
|
+
console.log("");
|
|
614
|
+
console.log("No files were written.");
|
|
615
|
+
console.log("No API key is required for dry-run.");
|
|
616
|
+
console.log("No MCP server configuration would be changed.");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
console.log(
|
|
621
|
+
`Installed ${installed.length} skill${
|
|
622
|
+
installed.length === 1 ? "" : "s"
|
|
623
|
+
} for ${target || "custom"}:`
|
|
624
|
+
);
|
|
625
|
+
for (const item of installed) {
|
|
626
|
+
console.log(`- ${item.skillName}: ${item.destination}`);
|
|
627
|
+
}
|
|
628
|
+
console.log("");
|
|
629
|
+
console.log("No MCP server setup is required for the bundled skills.");
|
|
630
|
+
console.log("No API key was stored by this installer.");
|
|
631
|
+
console.log("No MCP server configuration was changed.");
|
|
632
|
+
console.log("Installed files are AgentSkills files only.");
|
|
633
|
+
console.log(`Data calls are read-only and require ${PRIMARY_API_KEY_ENV} at runtime.`);
|
|
634
|
+
console.log("Configure your API Key before making authenticated calls:");
|
|
635
|
+
console.log(` export ${PRIMARY_API_KEY_ENV}="<${PRIMARY_API_KEY_ENV}>"`);
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log("Direct CLI examples:");
|
|
638
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs search --keyword "露营桌" --pretty`);
|
|
639
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs detail --note-id "<note_id>" --pretty`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function resolveInstallDestination({
|
|
643
|
+
skillName,
|
|
644
|
+
target,
|
|
645
|
+
scope,
|
|
646
|
+
path,
|
|
647
|
+
usePathAsParent,
|
|
648
|
+
}) {
|
|
649
|
+
const customPath = path && usePathAsParent ? join(path, skillName) : path;
|
|
650
|
+
return resolveInstallDir({
|
|
651
|
+
target,
|
|
652
|
+
scope,
|
|
653
|
+
customPath,
|
|
654
|
+
skillName,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function validateInstallPlan({ skillName, destination, force }) {
|
|
659
|
+
const sourceDir = join(currentDir, "skills", skillName);
|
|
660
|
+
|
|
661
|
+
if (!existsSync(sourceDir)) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
`Skill source directory not found: ${sourceDir}. Reinstall ${PACKAGE_NAME}.`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (existsSync(destination) && !force) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Skill already exists at ${destination}. Re-run with --force to replace it.`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (existsSync(destination) && force) {
|
|
674
|
+
await ensureSafeToReplaceSkill(destination, skillName);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function installOneSkill({
|
|
679
|
+
skillName,
|
|
680
|
+
destination,
|
|
681
|
+
force,
|
|
682
|
+
}) {
|
|
683
|
+
const sourceDir = join(currentDir, "skills", skillName);
|
|
684
|
+
|
|
685
|
+
if (!existsSync(sourceDir)) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`Skill source directory not found: ${sourceDir}. Reinstall ${PACKAGE_NAME}.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (existsSync(destination) && !force) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`Skill already exists at ${destination}. Re-run with --force to replace it.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (existsSync(destination) && force) {
|
|
698
|
+
await ensureSafeToReplaceSkill(destination, skillName);
|
|
699
|
+
await rm(destination, { recursive: true, force: true });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
703
|
+
await cp(sourceDir, destination, { recursive: true });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function ensureSafeToReplaceSkill(destination, skillName) {
|
|
707
|
+
const destinationStats = await stat(destination);
|
|
708
|
+
if (!destinationStats.isDirectory()) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Refusing to replace ${destination}: existing path is not a skill directory. Remove it manually or choose a different --path.`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const skillFile = join(destination, "SKILL.md");
|
|
715
|
+
if (!existsSync(skillFile)) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Refusing to replace ${destination}: existing directory does not contain SKILL.md. Remove it manually or choose a different --path.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const existingSkillName = extractSkillName(await readFile(skillFile, "utf8"));
|
|
722
|
+
if (existingSkillName !== skillName) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`Refusing to replace ${destination}: existing SKILL.md is for "${existingSkillName || "unknown"}", not "${skillName}". Remove it manually or choose a different --path.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function extractSkillName(markdown) {
|
|
730
|
+
const frontmatterMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
731
|
+
if (!frontmatterMatch) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const nameMatch = frontmatterMatch[1].match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
736
|
+
return nameMatch?.[1]?.trim();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function listSkills() {
|
|
740
|
+
console.log("\nAvailable skills:\n");
|
|
741
|
+
for (const skill of AVAILABLE_SKILLS) {
|
|
742
|
+
console.log(` ${skill.emoji} ${skill.name}`);
|
|
743
|
+
console.log(` ${skill.summary}`);
|
|
744
|
+
console.log();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function buildDoctorReport() {
|
|
749
|
+
const platforms = Object.values(PLATFORMS).map((platform) => {
|
|
750
|
+
const endpoint = resolveUpstreamUrl(platform);
|
|
751
|
+
return {
|
|
752
|
+
id: platform.id,
|
|
753
|
+
displayName: platform.displayName,
|
|
754
|
+
registryName: platform.registryName,
|
|
755
|
+
legacyRegistryName: platform.legacyRegistryName,
|
|
756
|
+
endpoint,
|
|
757
|
+
defaultEndpoint: platform.endpoint,
|
|
758
|
+
endpointOverrideActive: endpoint !== platform.endpoint,
|
|
759
|
+
transport: "streamable-http",
|
|
760
|
+
tools: platform.tools.map((tool) => tool.name),
|
|
761
|
+
toolDetails: platform.tools.map((tool) => ({
|
|
762
|
+
name: tool.name,
|
|
763
|
+
description: tool.description,
|
|
764
|
+
})),
|
|
765
|
+
};
|
|
766
|
+
});
|
|
767
|
+
return {
|
|
768
|
+
package: {
|
|
769
|
+
name: PACKAGE_NAME,
|
|
770
|
+
version: PACKAGE_VERSION,
|
|
771
|
+
homepage: HOMEPAGE_URL,
|
|
772
|
+
license: "MIT",
|
|
773
|
+
npmLifecycleScripts: [],
|
|
774
|
+
},
|
|
775
|
+
runtime: {
|
|
776
|
+
currentNode: process.version,
|
|
777
|
+
minimumNode: MIN_NODE_VERSION,
|
|
778
|
+
recommendedNode: "22 LTS or newer",
|
|
779
|
+
},
|
|
780
|
+
install: {
|
|
781
|
+
writes: "AgentSkills directories only",
|
|
782
|
+
apiKeyStored: false,
|
|
783
|
+
mcpConfigChanged: false,
|
|
784
|
+
supportsDryRun: true,
|
|
785
|
+
},
|
|
786
|
+
security: {
|
|
787
|
+
readOnly: true,
|
|
788
|
+
accountActions: false,
|
|
789
|
+
readsLocalBrowserData: false,
|
|
790
|
+
requiresApiKeyAtRuntime: true,
|
|
791
|
+
apiKeyEnv: [PRIMARY_API_KEY_ENV],
|
|
792
|
+
},
|
|
793
|
+
platforms,
|
|
794
|
+
platform: platforms.find((platform) => platform.id === "xhs"),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function printDoctor(args) {
|
|
799
|
+
const { options, positional } = parseCommandArgs(args);
|
|
800
|
+
if (positional.length > 0) {
|
|
801
|
+
throw new Error(`Unexpected argument: ${positional[0]}`);
|
|
802
|
+
}
|
|
803
|
+
validateKnownOptions(options, ["json"]);
|
|
804
|
+
validateFlagOption(options, "json", "--json");
|
|
805
|
+
const report = buildDoctorReport();
|
|
806
|
+
|
|
807
|
+
if (options.json) {
|
|
808
|
+
process.stdout.write(JSON.stringify(report, null, 2));
|
|
809
|
+
process.stdout.write("\n");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
console.log(`${PACKAGE_NAME} doctor`);
|
|
814
|
+
console.log("");
|
|
815
|
+
console.log(`Package: ${report.package.name}@${report.package.version}`);
|
|
816
|
+
console.log(`Website: ${report.package.homepage}`);
|
|
817
|
+
console.log(`License: ${report.package.license}`);
|
|
818
|
+
console.log(`Node: current ${report.runtime.currentNode}; minimum ${report.runtime.minimumNode}; recommended ${report.runtime.recommendedNode}`);
|
|
819
|
+
console.log("");
|
|
820
|
+
console.log("Install safety:");
|
|
821
|
+
console.log("- npm lifecycle scripts: none declared by this package.");
|
|
822
|
+
console.log("- install writes AgentSkills files only.");
|
|
823
|
+
console.log("- install does not store API keys.");
|
|
824
|
+
console.log("- install does not change MCP server configuration.");
|
|
825
|
+
console.log("- install --dry-run previews destinations without writing files.");
|
|
826
|
+
console.log("");
|
|
827
|
+
console.log("Runtime data calls:");
|
|
828
|
+
console.log("- read-only social media intelligence workflows.");
|
|
829
|
+
console.log("- no login, posting, editing, liking, commenting, or other account actions.");
|
|
830
|
+
console.log("- no local browser data access.");
|
|
831
|
+
console.log(`- requires ${PRIMARY_API_KEY_ENV} only when making authenticated data calls.`);
|
|
832
|
+
console.log("");
|
|
833
|
+
console.log("Platform MCPs:");
|
|
834
|
+
for (const platform of report.platforms) {
|
|
835
|
+
console.log(`- ${platform.displayName}`);
|
|
836
|
+
console.log(` registry: ${platform.registryName}`);
|
|
837
|
+
if (platform.legacyRegistryName) {
|
|
838
|
+
console.log(` legacy registry: ${platform.legacyRegistryName}`);
|
|
839
|
+
}
|
|
840
|
+
console.log(` endpoint: ${platform.endpoint}`);
|
|
841
|
+
if (platform.endpointOverrideActive) {
|
|
842
|
+
console.log(` default endpoint: ${platform.defaultEndpoint}`);
|
|
843
|
+
}
|
|
844
|
+
console.log(` transport: ${platform.transport}`);
|
|
845
|
+
console.log(` tools: ${platform.tools.length}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function printHelp() {
|
|
850
|
+
console.log(`${PACKAGE_NAME}`);
|
|
851
|
+
console.log("");
|
|
852
|
+
console.log("Commands:");
|
|
853
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs search --keyword "露营桌" --pretty`);
|
|
854
|
+
console.log(" Call the XHS search tool directly and print JSON.");
|
|
855
|
+
console.log("");
|
|
856
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs detail --note-id "<note_id>" --pretty`);
|
|
857
|
+
console.log(" Call the XHS note detail tool directly and print JSON.");
|
|
858
|
+
console.log("");
|
|
859
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs comments --note-id "<note_id>" --pretty`);
|
|
860
|
+
console.log(" Call the XHS comments tool directly and print JSON.");
|
|
861
|
+
console.log("");
|
|
862
|
+
console.log(
|
|
863
|
+
` npx -y ${PACKAGE_SPEC} xhs sub-comments --note-id "<note_id>" --comment-id "<comment_id>" --pretty`
|
|
864
|
+
);
|
|
865
|
+
console.log(" Call the XHS comment replies tool directly and print JSON.");
|
|
866
|
+
console.log("");
|
|
867
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs user-info --user-id "<user_id>" --pretty`);
|
|
868
|
+
console.log(" Call the XHS creator profile tool directly and print JSON.");
|
|
869
|
+
console.log("");
|
|
870
|
+
console.log(` npx -y ${PACKAGE_SPEC} xhs user-posts --user-id "<user_id>" --pretty`);
|
|
871
|
+
console.log(" Call the XHS creator posts tool directly and print JSON.");
|
|
872
|
+
console.log("");
|
|
873
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin search --keyword "露营桌" --pretty`);
|
|
874
|
+
console.log(" Call the Douyin work search tool directly and print JSON.");
|
|
875
|
+
console.log("");
|
|
876
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin detail --aweme-id "<aweme_id>" --pretty`);
|
|
877
|
+
console.log(" Call the Douyin work detail tool directly and print JSON.");
|
|
878
|
+
console.log("");
|
|
879
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin comments --aweme-id "<aweme_id>" --pretty`);
|
|
880
|
+
console.log(" Call the Douyin work comments tool directly and print JSON.");
|
|
881
|
+
console.log("");
|
|
882
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin user-info --sec-user-id "<sec_user_id>" --pretty`);
|
|
883
|
+
console.log(" Call the Douyin creator profile tool directly and print JSON.");
|
|
884
|
+
console.log("");
|
|
885
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin user-info --profile-url "<profile_url_or_share_text>" --pretty`);
|
|
886
|
+
console.log(" Call the Douyin creator profile tool from a profile link or share text.");
|
|
887
|
+
console.log("");
|
|
888
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin user-posts --sec-user-id "<sec_user_id>" --pretty`);
|
|
889
|
+
console.log(" Call the Douyin creator works tool directly and print JSON.");
|
|
890
|
+
console.log("");
|
|
891
|
+
console.log(` npx -y ${PACKAGE_SPEC} douyin user-posts --profile-url "<profile_url_or_share_text>" --pretty`);
|
|
892
|
+
console.log(" Call the Douyin creator works tool from a profile link or share text.");
|
|
893
|
+
console.log("");
|
|
894
|
+
console.log(` npx -y ${PACKAGE_SPEC} list`);
|
|
895
|
+
console.log(" List available skills.");
|
|
896
|
+
console.log("");
|
|
897
|
+
console.log(` npx -y ${PACKAGE_SPEC} doctor`);
|
|
898
|
+
console.log(" Print package safety and privacy summary without making data calls.");
|
|
899
|
+
console.log("");
|
|
900
|
+
console.log(` npx -y ${PACKAGE_SPEC} doctor --json`);
|
|
901
|
+
console.log(" Print the same summary as JSON.");
|
|
902
|
+
console.log("");
|
|
903
|
+
console.log(` npx -y ${PACKAGE_SPEC} install --target openclaw`);
|
|
904
|
+
console.log(" Install all skills to ~/.openclaw/workspace/skills.");
|
|
905
|
+
console.log("");
|
|
906
|
+
console.log(` npx -y ${PACKAGE_SPEC} install --target openclaw --dry-run`);
|
|
907
|
+
console.log(" Preview OpenClaw install destinations without writing files.");
|
|
908
|
+
console.log("");
|
|
909
|
+
console.log(` npx -y ${PACKAGE_SPEC} install media-search --target openclaw`);
|
|
910
|
+
console.log(" Install only the search skill to ~/.openclaw/workspace/skills.");
|
|
911
|
+
console.log("");
|
|
912
|
+
console.log(` npx -y ${PACKAGE_SPEC} install media-user-info --target openclaw`);
|
|
913
|
+
console.log(" Install only the creator profile skill to ~/.openclaw/workspace/skills.");
|
|
914
|
+
console.log("");
|
|
915
|
+
console.log(
|
|
916
|
+
` npx -y ${PACKAGE_SPEC} install media-comments media-detail --target openclaw`
|
|
917
|
+
);
|
|
918
|
+
console.log(" Install multiple selected skills.");
|
|
919
|
+
console.log("");
|
|
920
|
+
console.log(
|
|
921
|
+
` npx -y ${PACKAGE_SPEC} install media-search --target openclaw --scope workspace`
|
|
922
|
+
);
|
|
923
|
+
console.log(" Install one skill to ./skills/<skill-name>.");
|
|
924
|
+
console.log("");
|
|
925
|
+
console.log(` npx -y ${PACKAGE_SPEC} install media-search --target hermes`);
|
|
926
|
+
console.log(" Install one skill to ~/.hermes/skills.");
|
|
927
|
+
console.log("");
|
|
928
|
+
console.log(
|
|
929
|
+
` npx -y ${PACKAGE_SPEC} install media-search --target hermes --scope shared`
|
|
930
|
+
);
|
|
931
|
+
console.log(" Install one skill to ~/.agents/skills.");
|
|
932
|
+
console.log("");
|
|
933
|
+
console.log(` npx -y ${PACKAGE_SPEC} install media-search --target agents`);
|
|
934
|
+
console.log(" Install one skill to ~/.agents/skills.");
|
|
935
|
+
console.log("");
|
|
936
|
+
console.log(` npx -y ${PACKAGE_SPEC} install media-search --target codex`);
|
|
937
|
+
console.log(" Install one skill to ~/.codex/skills.");
|
|
938
|
+
console.log("");
|
|
939
|
+
console.log(
|
|
940
|
+
` npx -y ${PACKAGE_SPEC} install media-search --target codex --scope workspace`
|
|
941
|
+
);
|
|
942
|
+
console.log(" Install one skill to ./.codex/skills.");
|
|
943
|
+
console.log("");
|
|
944
|
+
console.log(
|
|
945
|
+
` npx -y ${PACKAGE_SPEC} install media-search --target claude-code`
|
|
946
|
+
);
|
|
947
|
+
console.log(" Install one skill to ~/.claude/skills.");
|
|
948
|
+
console.log("");
|
|
949
|
+
console.log(
|
|
950
|
+
` npx -y ${PACKAGE_SPEC} install media-search --target claude-code --scope workspace`
|
|
951
|
+
);
|
|
952
|
+
console.log(" Install one skill to ./.claude/skills.");
|
|
953
|
+
console.log("");
|
|
954
|
+
console.log("Available skills:");
|
|
955
|
+
console.log(` ${AVAILABLE_SKILL_NAMES.join(", ")}`);
|
|
956
|
+
console.log("");
|
|
957
|
+
console.log("Options:");
|
|
958
|
+
console.log(" --keyword <text>");
|
|
959
|
+
console.log(" --url <url-or-share-text>");
|
|
960
|
+
console.log(" For Douyin detail/comments, pass a content page link, short link, or share text, not video.play_url.");
|
|
961
|
+
console.log(" --note-id <note_id>");
|
|
962
|
+
console.log(" --aweme-id <aweme_id>");
|
|
963
|
+
console.log(" --comment-id <comment_id>");
|
|
964
|
+
console.log(" --profile-url <profile-url-or-share-text>");
|
|
965
|
+
console.log(" --user-id <user_id>");
|
|
966
|
+
console.log(" --sec-user-id <sec_user_id>");
|
|
967
|
+
console.log(" --page <number>");
|
|
968
|
+
console.log(" --sort-type <general|time_descending|like_count_descending|comment_count_descending|collect_count_descending>");
|
|
969
|
+
console.log(" XHS sort meanings: general=default, time_descending=newest, like_count_descending=most liked, comment_count_descending=most commented, collect_count_descending=most collected.");
|
|
970
|
+
console.log(" --note-type <all|image|video> XHS search note type filter; default is all.");
|
|
971
|
+
console.log(" --publish-time-range <all|day|week|half_year>");
|
|
972
|
+
console.log(" XHS search publish-time filter; default is all.");
|
|
973
|
+
console.log(" --sort-type <general|time_descending|like_count_descending>");
|
|
974
|
+
console.log(" Douyin search sort; omit for default sort.");
|
|
975
|
+
console.log(" --publish-time-range <all|day|week|half_year>");
|
|
976
|
+
console.log(" Douyin publish-time filter; omit for no filter.");
|
|
977
|
+
console.log(" --duration-range <all|under_1_minute|one_to_five_minutes|over_5_minutes>");
|
|
978
|
+
console.log(" Douyin duration filter; omit for no duration filter.");
|
|
979
|
+
console.log(" --content-type <all|video|image>");
|
|
980
|
+
console.log(" Douyin content type filter; omit for all content types.");
|
|
981
|
+
console.log(" --page-token <token>");
|
|
982
|
+
console.log(" --pretty Pretty-print direct CLI JSON output.");
|
|
983
|
+
console.log(" --json Print doctor output as JSON.");
|
|
984
|
+
console.log(" --target <openclaw|hermes|agents|codex|claude-code|claude>");
|
|
985
|
+
console.log(" For install.");
|
|
986
|
+
console.log(" --scope <user|workspace|shared> shared is only for --target hermes.");
|
|
987
|
+
console.log(" --path <directory> Override install destination.");
|
|
988
|
+
console.log(" --dry-run Preview install without writing files.");
|
|
989
|
+
console.log(" --force Replace an existing directory for the same skill.");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function printRemovedMcpConfigHelp(command) {
|
|
993
|
+
console.error(`${LOG_PREFIX} ${command} is no longer supported by this skills package.`);
|
|
994
|
+
console.error("");
|
|
995
|
+
console.error("This package now installs AgentSkills and provides direct CLI data commands only.");
|
|
996
|
+
console.error("For MCP client configuration, use the platform MCP listings:");
|
|
997
|
+
console.error(" com.socialdatax/xhs-insights");
|
|
998
|
+
console.error(" com.socialdatax/douyin-insights");
|
|
999
|
+
console.error("Legacy registry names remain compatible for historical listings:");
|
|
1000
|
+
console.error(" com.52choujiang/xhs-insights");
|
|
1001
|
+
console.error(" com.52choujiang/douyin-insights");
|
|
1002
|
+
console.error("");
|
|
1003
|
+
console.error("Use hosted streamable HTTP when your client supports remote MCP:");
|
|
1004
|
+
console.error(" https://mcp.52choujiang.com/xhs/mcp");
|
|
1005
|
+
console.error(" https://mcp.52choujiang.com/douyin/mcp");
|
|
1006
|
+
console.error("");
|
|
1007
|
+
console.error("For command/stdio-only clients, use mcp-remote:");
|
|
1008
|
+
console.error(` npx -y mcp-remote https://mcp.52choujiang.com/xhs/mcp --header "Authorization: Bearer <${PRIMARY_API_KEY_ENV}>"`);
|
|
1009
|
+
console.error(` npx -y mcp-remote https://mcp.52choujiang.com/douyin/mcp --header "Authorization: Bearer <${PRIMARY_API_KEY_ENV}>"`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function runXhsDirectCommand(args) {
|
|
1013
|
+
const { options, positional } = parseCommandArgs(args);
|
|
1014
|
+
const action = positional[0];
|
|
1015
|
+
if (!action) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Missing XHS command. Use search, detail, comments, sub-comments, user-info, or user-posts.`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
if (positional.length > 1) {
|
|
1021
|
+
throw new Error(`Unexpected argument: ${positional[1]}`);
|
|
1022
|
+
}
|
|
1023
|
+
validateXhsDirectActionOptions(action, options);
|
|
1024
|
+
|
|
1025
|
+
const operation = buildXhsOperation(action, options);
|
|
1026
|
+
const data = await callDirectOperation(operation);
|
|
1027
|
+
const envelope = {
|
|
1028
|
+
platform: operation.platform.id,
|
|
1029
|
+
tool: operation.tool,
|
|
1030
|
+
arguments: operation.arguments,
|
|
1031
|
+
data,
|
|
1032
|
+
};
|
|
1033
|
+
process.stdout.write(JSON.stringify(envelope, null, options.pretty ? 2 : 0));
|
|
1034
|
+
process.stdout.write("\n");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function runDouyinDirectCommand(args) {
|
|
1038
|
+
const { options, positional } = parseCommandArgs(args);
|
|
1039
|
+
const action = positional[0];
|
|
1040
|
+
if (!action) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Missing Douyin command. Use search, detail, comments, user-info, or user-posts.`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
if (positional.length > 1) {
|
|
1046
|
+
throw new Error(`Unexpected argument: ${positional[1]}`);
|
|
1047
|
+
}
|
|
1048
|
+
validateDouyinDirectActionOptions(action, options);
|
|
1049
|
+
|
|
1050
|
+
const operation = buildDouyinOperation(action, options);
|
|
1051
|
+
const data = await callDirectOperation(operation);
|
|
1052
|
+
const envelope = {
|
|
1053
|
+
platform: operation.platform.id,
|
|
1054
|
+
tool: operation.tool,
|
|
1055
|
+
arguments: operation.arguments,
|
|
1056
|
+
data,
|
|
1057
|
+
};
|
|
1058
|
+
process.stdout.write(JSON.stringify(envelope, null, options.pretty ? 2 : 0));
|
|
1059
|
+
process.stdout.write("\n");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function buildXhsOperation(action, options) {
|
|
1063
|
+
switch (action) {
|
|
1064
|
+
case "search":
|
|
1065
|
+
return buildDirectOperation("search", buildXhsSearchCall(options));
|
|
1066
|
+
case "detail":
|
|
1067
|
+
return buildDirectOperation(
|
|
1068
|
+
"detail",
|
|
1069
|
+
buildOneOfCall(options, {
|
|
1070
|
+
idOption: "noteId",
|
|
1071
|
+
urlOption: "url",
|
|
1072
|
+
idTool: "xhs_get_note_detail_by_note_id",
|
|
1073
|
+
urlTool: "xhs_get_note_detail_by_note_url",
|
|
1074
|
+
idArgument: "note_id",
|
|
1075
|
+
urlArgument: "note_url",
|
|
1076
|
+
idDisplay: "--note-id",
|
|
1077
|
+
urlDisplay: "--url",
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
case "comments":
|
|
1081
|
+
return buildDirectOperation(
|
|
1082
|
+
"comments",
|
|
1083
|
+
buildOneOfCall(options, {
|
|
1084
|
+
idOption: "noteId",
|
|
1085
|
+
urlOption: "url",
|
|
1086
|
+
idTool: "xhs_get_note_comments_by_note_id",
|
|
1087
|
+
urlTool: "xhs_get_note_comments_by_note_url",
|
|
1088
|
+
idArgument: "note_id",
|
|
1089
|
+
urlArgument: "note_url",
|
|
1090
|
+
idDisplay: "--note-id",
|
|
1091
|
+
urlDisplay: "--url",
|
|
1092
|
+
pageToken: options.pageToken,
|
|
1093
|
+
})
|
|
1094
|
+
);
|
|
1095
|
+
case "sub-comments":
|
|
1096
|
+
return buildDirectOperation(
|
|
1097
|
+
"sub-comments",
|
|
1098
|
+
buildXhsSubCommentsCall(options)
|
|
1099
|
+
);
|
|
1100
|
+
case "user-info":
|
|
1101
|
+
return buildDirectOperation(
|
|
1102
|
+
"user-info",
|
|
1103
|
+
buildOneOfCall(options, {
|
|
1104
|
+
idOption: "userId",
|
|
1105
|
+
urlOption: "profileUrl",
|
|
1106
|
+
idTool: "xhs_get_user_info_by_user_id",
|
|
1107
|
+
urlTool: "xhs_get_user_info_by_profile_url",
|
|
1108
|
+
idArgument: "user_id",
|
|
1109
|
+
urlArgument: "profile_url",
|
|
1110
|
+
idDisplay: "--user-id",
|
|
1111
|
+
urlDisplay: "--profile-url",
|
|
1112
|
+
})
|
|
1113
|
+
);
|
|
1114
|
+
case "user-posts":
|
|
1115
|
+
return buildDirectOperation(
|
|
1116
|
+
"user-posts",
|
|
1117
|
+
buildOneOfCall(options, {
|
|
1118
|
+
idOption: "userId",
|
|
1119
|
+
urlOption: "profileUrl",
|
|
1120
|
+
idTool: "xhs_get_user_posted_notes_by_user_id",
|
|
1121
|
+
urlTool: "xhs_get_user_posted_notes_by_profile_url",
|
|
1122
|
+
idArgument: "user_id",
|
|
1123
|
+
urlArgument: "profile_url",
|
|
1124
|
+
idDisplay: "--user-id",
|
|
1125
|
+
urlDisplay: "--profile-url",
|
|
1126
|
+
pageToken: options.pageToken,
|
|
1127
|
+
})
|
|
1128
|
+
);
|
|
1129
|
+
default:
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`Unsupported XHS command "${action}". Use search, detail, comments, sub-comments, user-info, or user-posts.`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function buildDouyinOperation(action, options) {
|
|
1137
|
+
switch (action) {
|
|
1138
|
+
case "search":
|
|
1139
|
+
return buildDirectOperation(
|
|
1140
|
+
"search",
|
|
1141
|
+
buildDouyinSearchCall(options),
|
|
1142
|
+
PLATFORMS.douyin
|
|
1143
|
+
);
|
|
1144
|
+
case "detail":
|
|
1145
|
+
return buildDirectOperation(
|
|
1146
|
+
"detail",
|
|
1147
|
+
buildOneOfCall(options, {
|
|
1148
|
+
idOption: "awemeId",
|
|
1149
|
+
urlOption: "url",
|
|
1150
|
+
idTool: "douyin_get_video_detail_by_aweme_id",
|
|
1151
|
+
urlTool: "douyin_get_video_detail_by_url",
|
|
1152
|
+
idArgument: "aweme_id",
|
|
1153
|
+
urlArgument: "url",
|
|
1154
|
+
idDisplay: "--aweme-id",
|
|
1155
|
+
urlDisplay: "--url",
|
|
1156
|
+
}),
|
|
1157
|
+
PLATFORMS.douyin
|
|
1158
|
+
);
|
|
1159
|
+
case "comments":
|
|
1160
|
+
return buildDirectOperation(
|
|
1161
|
+
"comments",
|
|
1162
|
+
buildOneOfCall(options, {
|
|
1163
|
+
idOption: "awemeId",
|
|
1164
|
+
urlOption: "url",
|
|
1165
|
+
idTool: "douyin_get_video_comments_by_aweme_id",
|
|
1166
|
+
urlTool: "douyin_get_video_comments_by_url",
|
|
1167
|
+
idArgument: "aweme_id",
|
|
1168
|
+
urlArgument: "url",
|
|
1169
|
+
idDisplay: "--aweme-id",
|
|
1170
|
+
urlDisplay: "--url",
|
|
1171
|
+
pageToken: options.pageToken,
|
|
1172
|
+
}),
|
|
1173
|
+
PLATFORMS.douyin
|
|
1174
|
+
);
|
|
1175
|
+
case "user-info":
|
|
1176
|
+
return buildDirectOperation(
|
|
1177
|
+
"user-info",
|
|
1178
|
+
buildOneOfCall(options, {
|
|
1179
|
+
idOption: "secUserId",
|
|
1180
|
+
urlOption: "profileUrl",
|
|
1181
|
+
idTool: "douyin_get_user_info_by_sec_user_id",
|
|
1182
|
+
urlTool: "douyin_get_user_info_by_profile_url",
|
|
1183
|
+
idArgument: "sec_user_id",
|
|
1184
|
+
urlArgument: "profile_url",
|
|
1185
|
+
idDisplay: "--sec-user-id",
|
|
1186
|
+
urlDisplay: "--profile-url",
|
|
1187
|
+
}),
|
|
1188
|
+
PLATFORMS.douyin
|
|
1189
|
+
);
|
|
1190
|
+
case "user-posts":
|
|
1191
|
+
return buildDirectOperation(
|
|
1192
|
+
"user-posts",
|
|
1193
|
+
buildOneOfCall(options, {
|
|
1194
|
+
idOption: "secUserId",
|
|
1195
|
+
urlOption: "profileUrl",
|
|
1196
|
+
idTool: "douyin_get_user_posted_videos_by_sec_user_id",
|
|
1197
|
+
urlTool: "douyin_get_user_posted_videos_by_profile_url",
|
|
1198
|
+
idArgument: "sec_user_id",
|
|
1199
|
+
urlArgument: "profile_url",
|
|
1200
|
+
idDisplay: "--sec-user-id",
|
|
1201
|
+
urlDisplay: "--profile-url",
|
|
1202
|
+
pageToken: options.pageToken,
|
|
1203
|
+
}),
|
|
1204
|
+
PLATFORMS.douyin
|
|
1205
|
+
);
|
|
1206
|
+
default:
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`Unsupported Douyin command "${action}". Use search, detail, comments, user-info, or user-posts.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function buildDirectOperation(operation, { tool, toolArguments }, platform = PLATFORMS.xhs) {
|
|
1214
|
+
return {
|
|
1215
|
+
platform,
|
|
1216
|
+
operation,
|
|
1217
|
+
backend: "mcp",
|
|
1218
|
+
tool,
|
|
1219
|
+
arguments: toolArguments,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function buildXhsSearchCall(options) {
|
|
1224
|
+
if (!options.keyword) {
|
|
1225
|
+
throw new Error("Missing --keyword for xhs search.");
|
|
1226
|
+
}
|
|
1227
|
+
const page =
|
|
1228
|
+
options.page === undefined
|
|
1229
|
+
? 1
|
|
1230
|
+
: parsePositiveIntegerOption(options.page, "--page");
|
|
1231
|
+
const sortType = parseSemanticOption(
|
|
1232
|
+
options.sortType || "general",
|
|
1233
|
+
"--sort-type",
|
|
1234
|
+
XHS_SEARCH_SORT_TYPES,
|
|
1235
|
+
XHS_LEGACY_SEARCH_SORT_TYPE_ALIASES,
|
|
1236
|
+
XHS_SEARCH_SORT_TYPES.join(", ")
|
|
1237
|
+
);
|
|
1238
|
+
const noteType = options.noteType || "all";
|
|
1239
|
+
const allowedNoteTypes = ["all", "image", "video"];
|
|
1240
|
+
if (!allowedNoteTypes.includes(noteType)) {
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
`Unsupported --note-type "${noteType}". Use one of: ${allowedNoteTypes.join(", ")}.`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const publishTimeRange = options.publishTimeRange || "all";
|
|
1246
|
+
const allowedPublishTimeRanges = ["all", "day", "week", "half_year"];
|
|
1247
|
+
if (!allowedPublishTimeRanges.includes(publishTimeRange)) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
`Unsupported --publish-time-range "${publishTimeRange}". Use one of: ${allowedPublishTimeRanges.join(", ")}.`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const toolArguments = {
|
|
1253
|
+
keyword: options.keyword,
|
|
1254
|
+
page,
|
|
1255
|
+
};
|
|
1256
|
+
if (options.sortType !== undefined) {
|
|
1257
|
+
toolArguments.sort_type = sortType;
|
|
1258
|
+
}
|
|
1259
|
+
if (options.noteType !== undefined) {
|
|
1260
|
+
toolArguments.note_type = noteType;
|
|
1261
|
+
}
|
|
1262
|
+
if (options.publishTimeRange !== undefined) {
|
|
1263
|
+
toolArguments.publish_time_range = publishTimeRange;
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
tool: "xhs_search_notes",
|
|
1267
|
+
toolArguments,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function buildOneOfCall(
|
|
1272
|
+
options,
|
|
1273
|
+
{
|
|
1274
|
+
idOption,
|
|
1275
|
+
urlOption,
|
|
1276
|
+
idTool,
|
|
1277
|
+
urlTool,
|
|
1278
|
+
idArgument,
|
|
1279
|
+
urlArgument,
|
|
1280
|
+
idDisplay,
|
|
1281
|
+
urlDisplay,
|
|
1282
|
+
pageToken,
|
|
1283
|
+
}
|
|
1284
|
+
) {
|
|
1285
|
+
const idValue = options[idOption];
|
|
1286
|
+
const urlValue = options[urlOption];
|
|
1287
|
+
if (idValue && urlValue) {
|
|
1288
|
+
throw new Error(`Use only one of ${idDisplay} or ${urlDisplay}.`);
|
|
1289
|
+
}
|
|
1290
|
+
if (!idValue && !urlValue) {
|
|
1291
|
+
throw new Error(`Missing input. Use ${idDisplay} or ${urlDisplay}.`);
|
|
1292
|
+
}
|
|
1293
|
+
const toolArguments = {};
|
|
1294
|
+
const tool = idValue ? idTool : urlTool;
|
|
1295
|
+
toolArguments[idValue ? idArgument : urlArgument] = idValue || urlValue;
|
|
1296
|
+
if (pageToken) {
|
|
1297
|
+
toolArguments.page_token = pageToken;
|
|
1298
|
+
}
|
|
1299
|
+
return { tool, toolArguments };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function buildDouyinSearchCall(options) {
|
|
1303
|
+
if (!options.keyword) {
|
|
1304
|
+
throw new Error("Missing --keyword for douyin search.");
|
|
1305
|
+
}
|
|
1306
|
+
const toolArguments = {
|
|
1307
|
+
keyword: options.keyword,
|
|
1308
|
+
};
|
|
1309
|
+
if (options.sortType !== undefined) {
|
|
1310
|
+
toolArguments.sort_type = parseAllowedStringOption(
|
|
1311
|
+
options.sortType,
|
|
1312
|
+
"--sort-type",
|
|
1313
|
+
DOUYIN_SEARCH_SORT_TYPES,
|
|
1314
|
+
DOUYIN_SEARCH_SORT_TYPES.join(", ")
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (options.publishTimeRange !== undefined) {
|
|
1318
|
+
toolArguments.publish_time_range = parseAllowedStringOption(
|
|
1319
|
+
options.publishTimeRange,
|
|
1320
|
+
"--publish-time-range",
|
|
1321
|
+
DOUYIN_SEARCH_PUBLISH_TIME_RANGES,
|
|
1322
|
+
DOUYIN_SEARCH_PUBLISH_TIME_RANGES.join(", ")
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
if (options.durationRange !== undefined) {
|
|
1326
|
+
toolArguments.duration_range = parseAllowedStringOption(
|
|
1327
|
+
options.durationRange,
|
|
1328
|
+
"--duration-range",
|
|
1329
|
+
DOUYIN_SEARCH_DURATION_RANGES,
|
|
1330
|
+
DOUYIN_SEARCH_DURATION_RANGES.join(", ")
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (options.contentType !== undefined) {
|
|
1334
|
+
toolArguments.content_type = parseAllowedStringOption(
|
|
1335
|
+
options.contentType,
|
|
1336
|
+
"--content-type",
|
|
1337
|
+
DOUYIN_SEARCH_CONTENT_TYPES,
|
|
1338
|
+
DOUYIN_SEARCH_CONTENT_TYPES.join(", ")
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (options.pageToken) {
|
|
1342
|
+
toolArguments.page_token = options.pageToken;
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
tool: "douyin_search_videos",
|
|
1346
|
+
toolArguments,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
function buildXhsSubCommentsCall(options) {
|
|
1352
|
+
if (!options.noteId) {
|
|
1353
|
+
throw new Error("Missing --note-id for xhs sub-comments.");
|
|
1354
|
+
}
|
|
1355
|
+
if (!options.commentId) {
|
|
1356
|
+
throw new Error("Missing --comment-id for xhs sub-comments.");
|
|
1357
|
+
}
|
|
1358
|
+
const toolArguments = {
|
|
1359
|
+
note_id: options.noteId,
|
|
1360
|
+
comment_id: options.commentId,
|
|
1361
|
+
};
|
|
1362
|
+
if (options.pageToken) {
|
|
1363
|
+
toolArguments.page_token = options.pageToken;
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
tool: "xhs_get_note_sub_comments_by_comment_id",
|
|
1367
|
+
toolArguments,
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
async function callDirectOperation(operation) {
|
|
1372
|
+
switch (operation.backend) {
|
|
1373
|
+
case "mcp":
|
|
1374
|
+
return callMcpBackend(operation);
|
|
1375
|
+
default:
|
|
1376
|
+
throw new Error(`Unsupported direct CLI backend: ${operation.backend}.`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
async function callMcpBackend(operation) {
|
|
1381
|
+
ensureSupportedNodeVersion();
|
|
1382
|
+
const { platform, tool } = operation;
|
|
1383
|
+
const apiKey = readFirstEnv(platform.apiKeyEnv);
|
|
1384
|
+
if (!apiKey) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`Missing API Key. Set ${PRIMARY_API_KEY_ENV} before running direct CLI calls.`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const { Client, StreamableHTTPClientTransport } = await loadMcpSdkModules();
|
|
1391
|
+
const upstreamUrl = resolveUpstreamUrl(platform);
|
|
1392
|
+
const client = new Client(
|
|
1393
|
+
{ name: PACKAGE_NAME, version: PACKAGE_VERSION },
|
|
1394
|
+
{ capabilities: {} }
|
|
1395
|
+
);
|
|
1396
|
+
const transport = new StreamableHTTPClientTransport(new URL(upstreamUrl), {
|
|
1397
|
+
requestInit: {
|
|
1398
|
+
headers: {
|
|
1399
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
try {
|
|
1405
|
+
await client.connect(transport);
|
|
1406
|
+
const result = await client.callTool({
|
|
1407
|
+
name: tool,
|
|
1408
|
+
arguments: operation.arguments,
|
|
1409
|
+
});
|
|
1410
|
+
if (result.isError) {
|
|
1411
|
+
const errorMessage =
|
|
1412
|
+
result.structuredContent?.message ||
|
|
1413
|
+
extractTextContent(result.content) ||
|
|
1414
|
+
`MCP tool ${tool} returned an error.`;
|
|
1415
|
+
const error = new Error(errorMessage);
|
|
1416
|
+
error.structuredContent = result.structuredContent;
|
|
1417
|
+
throw error;
|
|
1418
|
+
}
|
|
1419
|
+
return result.structuredContent ?? result;
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
throw formatDirectCallError({ error, operation, upstreamUrl });
|
|
1422
|
+
} finally {
|
|
1423
|
+
await client.close().catch(() => {});
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async function loadMcpSdkModules() {
|
|
1428
|
+
if (!mcpSdkModules) {
|
|
1429
|
+
const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
|
|
1430
|
+
import("@modelcontextprotocol/sdk/client/index.js"),
|
|
1431
|
+
import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
|
|
1432
|
+
]);
|
|
1433
|
+
mcpSdkModules = { Client, StreamableHTTPClientTransport };
|
|
1434
|
+
}
|
|
1435
|
+
return mcpSdkModules;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function formatDirectCallError({ error, operation, upstreamUrl }) {
|
|
1439
|
+
const message = error?.message || String(error);
|
|
1440
|
+
if (error?.structuredContent) {
|
|
1441
|
+
return error;
|
|
1442
|
+
}
|
|
1443
|
+
return new Error(
|
|
1444
|
+
`Direct CLI call failed for ${operation.platform.id}/${operation.operation} at ${upstreamUrl}: ${message}`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function extractTextContent(content) {
|
|
1449
|
+
if (!Array.isArray(content)) {
|
|
1450
|
+
return undefined;
|
|
1451
|
+
}
|
|
1452
|
+
return content
|
|
1453
|
+
.filter((item) => item?.type === "text" && item.text)
|
|
1454
|
+
.map((item) => item.text)
|
|
1455
|
+
.join("\n")
|
|
1456
|
+
.trim();
|
|
1457
|
+
}
|