@northmoon-labs/social-cli 0.1.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 ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Northmoon Labs
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,478 @@
1
+ # Northmoon Social CLI
2
+
3
+ `nmsocial` 是给 Agent 使用的 Northmoon Social 命令行工具,让 Agent 可以通过稳定的命令完成社媒账号授权、素材管理、内容发布、评论处理、数据统计和自动回复配置。
4
+
5
+ 你可以使用 `nmsocial` 完成:
6
+
7
+ - 获取社媒账号授权链接
8
+ - 查询已授权账号
9
+ - 上传视频和图片素材
10
+ - 创建视频 / 图文发布任务
11
+ - 查询发布任务和发布记录
12
+ - 查询评论并回复评论
13
+ - 配置 webhook 转发地址
14
+ - 查询账号、作品、平台统计数据
15
+ - 管理自动回复策略
16
+
17
+ ## 安装
18
+
19
+ ```bash
20
+ npm install -g @northmoon-labs/social-cli
21
+ ```
22
+
23
+ 检查是否安装成功:
24
+
25
+ ```bash
26
+ nmsocial --help
27
+ ```
28
+
29
+ 也可以直接用 `npx` 运行:
30
+
31
+ ```bash
32
+ npx @northmoon-labs/social-cli --help
33
+ ```
34
+
35
+ ## 登录
36
+
37
+ 推荐使用 `--api-key-stdin` 登录,避免 API Key 出现在 shell history 中:
38
+
39
+ ```bash
40
+ nmsocial login --api-key-stdin
41
+ ```
42
+
43
+ 如果是 Agent 或自动化客户端调用,也可以直接传入 API Key:
44
+
45
+ ```bash
46
+ nmsocial login --api-key YOUR_KEY
47
+ ```
48
+
49
+ 使用 `--api-key` 更方便自动化,但要注意不要把包含 API Key 的命令输出到公开日志中。
50
+
51
+ 查看当前登录状态:
52
+
53
+ ```bash
54
+ nmsocial status
55
+ ```
56
+
57
+ 退出登录:
58
+
59
+ ```bash
60
+ nmsocial logout
61
+ ```
62
+
63
+ ## 更新 CLI
64
+
65
+ 检查是否有新版本:
66
+
67
+ ```bash
68
+ nmsocial update check
69
+ ```
70
+
71
+ 更新到 npm 上的最新版本:
72
+
73
+ ```bash
74
+ nmsocial update
75
+ ```
76
+
77
+ 只查看将要执行的安装命令,不实际更新:
78
+
79
+ ```bash
80
+ nmsocial update --dry-run
81
+ ```
82
+
83
+ 默认会根据当前运行环境选择包管理器,无法判断时使用 `npm`。你也可以手动指定:
84
+
85
+ ```bash
86
+ nmsocial update --manager npm
87
+ nmsocial update --manager pnpm
88
+ nmsocial update --manager bun
89
+ ```
90
+
91
+ ## 给 Agent 使用
92
+
93
+ npm 包内包含一份给 Agent 阅读的 CLI 使用说明:
94
+
95
+ ```text
96
+ skills/nmsocial-cli/SKILL.md
97
+ ```
98
+
99
+ 如果你的 Agent 支持读取 skill、工具说明或外部上下文,可以把这个文件提供给它。该文件只说明如何调用 `nmsocial`,不包含内部开发文档。
100
+
101
+ ## 全局参数
102
+
103
+ | 参数 | 说明 |
104
+ | --- | --- |
105
+ | `--json` | 输出稳定 JSON,适合脚本解析 |
106
+ | `--timeout MS` | 设置请求超时时间,单位毫秒 |
107
+ | `--dry-run` | 只预览请求内容,不发送请求 |
108
+ | `--help` | 查看帮助 |
109
+ | `--version` | 查看版本 |
110
+
111
+ ## 输出格式
112
+
113
+ 默认输出为简洁文本或表格,适合在终端里快速查看,也能减少 Agent 阅读结果时的无关内容。
114
+
115
+ 如果要让脚本或 Agent 稳定解析返回结果,请加上 `--json`:
116
+
117
+ ```bash
118
+ nmsocial accounts list --platform tiktok --json
119
+ ```
120
+
121
+ ## 平台参数
122
+
123
+ 涉及平台的命令支持平台名称或平台数字:
124
+
125
+ | 平台名称 | 平台数字 |
126
+ | --- | --- |
127
+ | `tiktok` | `1` |
128
+ | `youtube` | `2` |
129
+ | `instagram` | `3` |
130
+ | `facebook` | `4` |
131
+
132
+ 示例:
133
+
134
+ ```bash
135
+ nmsocial auth-url --platform tiktok
136
+ nmsocial auth-url --platform 1
137
+ ```
138
+
139
+ ## 账号授权
140
+
141
+ 获取授权链接:
142
+
143
+ ```bash
144
+ nmsocial auth-url --platform tiktok
145
+ ```
146
+
147
+ 查询已授权账号:
148
+
149
+ ```bash
150
+ nmsocial accounts list --platform tiktok --status 1 --page 1 --page-size 20
151
+ ```
152
+
153
+ 按条件筛选账号:
154
+
155
+ ```bash
156
+ nmsocial accounts list \
157
+ --platform youtube \
158
+ --username demo \
159
+ --is-verified 1 \
160
+ --page-size 50
161
+ ```
162
+
163
+ ## YouTube 剧集
164
+
165
+ 查询剧集列表:
166
+
167
+ ```bash
168
+ nmsocial youtube series list --open-id CHANNEL_ID
169
+ ```
170
+
171
+ 创建剧集:
172
+
173
+ ```bash
174
+ nmsocial youtube series create \
175
+ --open-id CHANNEL_ID \
176
+ --title "Demo Series" \
177
+ --description "Demo description"
178
+ ```
179
+
180
+ 创建前预览请求内容:
181
+
182
+ ```bash
183
+ nmsocial youtube series create \
184
+ --open-id CHANNEL_ID \
185
+ --title "Demo Series" \
186
+ --dry-run \
187
+ --json
188
+ ```
189
+
190
+ ## 素材
191
+
192
+ 查询素材列表:
193
+
194
+ ```bash
195
+ nmsocial materials list --type 1 --title demo --page 1 --page-size 20
196
+ ```
197
+
198
+ 上传素材:
199
+
200
+ ```bash
201
+ nmsocial materials upload ./demo.mp4
202
+ ```
203
+
204
+ 支持的文件类型:
205
+
206
+ - 视频:`mp4`、`mov`、`avi`、`mkv`、`webm`、`flv`、`wmv`、`m4v`、`3gp`
207
+ - 图片:`jpg`、`jpeg`、`png`、`gif`、`bmp`、`webp`、`heic`、`heif`、`tif`、`tiff`
208
+
209
+ 上传后的素材默认保留七天。
210
+
211
+ ## 发布任务
212
+
213
+ 创建视频发布任务:
214
+
215
+ ```bash
216
+ nmsocial publish video \
217
+ --account OPEN_ID_1 \
218
+ --account OPEN_ID_2 \
219
+ --content https://example.com/video1.mp4 \
220
+ --caption "hello"
221
+ ```
222
+
223
+ 创建图文发布任务:
224
+
225
+ ```bash
226
+ nmsocial publish image \
227
+ --account OPEN_ID \
228
+ --content https://example.com/image1.png \
229
+ --content https://example.com/image2.png \
230
+ --title "title" \
231
+ --caption "caption"
232
+ ```
233
+
234
+ 定时发布:
235
+
236
+ ```bash
237
+ nmsocial publish video \
238
+ --account OPEN_ID \
239
+ --content https://example.com/video.mp4 \
240
+ --at 2026-07-18T10:00:00+08:00
241
+ ```
242
+
243
+ 创建前预览请求内容:
244
+
245
+ ```bash
246
+ nmsocial publish video \
247
+ --account OPEN_ID \
248
+ --content https://example.com/video.mp4 \
249
+ --video-assign-type 1 \
250
+ --privacy public \
251
+ --dry-run \
252
+ --json
253
+ ```
254
+
255
+ `--account`、`--content`、`--playlist-id` 等参数可以重复传入。
256
+
257
+ ## 任务查询
258
+
259
+ 查询发布任务:
260
+
261
+ ```bash
262
+ nmsocial tasks list --platform tiktok --page 1 --page-size 20
263
+ ```
264
+
265
+ 按发布方式或时间筛选:
266
+
267
+ ```bash
268
+ nmsocial tasks list \
269
+ --platform tiktok \
270
+ --video-assign-type 1 \
271
+ --create-time-begin 1783990000000 \
272
+ --create-time-end 1784000000000
273
+ ```
274
+
275
+ ## 发布记录
276
+
277
+ 查询发布记录:
278
+
279
+ ```bash
280
+ nmsocial posts list --platform tiktok --open-id OPEN_ID
281
+ ```
282
+
283
+ 查询已发布作品统计:
284
+
285
+ ```bash
286
+ nmsocial posts stats --platform tiktok --main-task-id TASK_ID
287
+ ```
288
+
289
+ ## 评论
290
+
291
+ 查询评论:
292
+
293
+ ```bash
294
+ nmsocial comments list --platform tiktok --open-id OPEN_ID
295
+ ```
296
+
297
+ 回复评论:
298
+
299
+ ```bash
300
+ nmsocial comments reply \
301
+ --platform tiktok \
302
+ --open-id OPEN_ID \
303
+ --video-id VIDEO_ID \
304
+ --comment-id COMMENT_ID \
305
+ --text "Thanks"
306
+ ```
307
+
308
+ 回复前预览请求内容:
309
+
310
+ ```bash
311
+ nmsocial comments reply \
312
+ --platform tiktok \
313
+ --open-id OPEN_ID \
314
+ --video-id VIDEO_ID \
315
+ --comment-id COMMENT_ID \
316
+ --text "Thanks" \
317
+ --dry-run \
318
+ --json
319
+ ```
320
+
321
+ ## Webhook
322
+
323
+ 设置 webhook 转发地址:
324
+
325
+ ```bash
326
+ nmsocial webhook set --url https://partner.example.com/spost/webhook
327
+ ```
328
+
329
+ 关闭 webhook 转发:
330
+
331
+ ```bash
332
+ nmsocial webhook disable
333
+ ```
334
+
335
+ ## 数据统计
336
+
337
+ 查询统计概览:
338
+
339
+ ```bash
340
+ nmsocial stats overview --platform tiktok --start-date 2026-07-11 --end-date 2026-07-17
341
+ ```
342
+
343
+ 查询账号趋势:
344
+
345
+ ```bash
346
+ nmsocial stats account trend --platform tiktok --metric followers
347
+ ```
348
+
349
+ 查询账号排行:
350
+
351
+ ```bash
352
+ nmsocial stats account rank --metric followerIncrease --order desc --page 1 --page-size 20
353
+ ```
354
+
355
+ 查询作品趋势:
356
+
357
+ ```bash
358
+ nmsocial stats post trend --platform tiktok --metric viewCount
359
+ ```
360
+
361
+ 查询作品排行:
362
+
363
+ ```bash
364
+ nmsocial stats post rank --metric viewIncrease --order desc --page 1 --page-size 20
365
+ ```
366
+
367
+ 查询单个作品趋势:
368
+
369
+ ```bash
370
+ nmsocial stats post detail-trend \
371
+ --post-id POST_ID \
372
+ --metric viewCount \
373
+ --metric likeCount \
374
+ --metric commentCount
375
+ ```
376
+
377
+ 查询平台汇总:
378
+
379
+ ```bash
380
+ nmsocial stats platform summary --start-date 2026-07-11 --end-date 2026-07-17
381
+ ```
382
+
383
+ 日期参数格式为 `yyyy-MM-dd`。
384
+
385
+ ## 自动回复
386
+
387
+ 查询自动回复策略:
388
+
389
+ ```bash
390
+ nmsocial auto-replies list --platform tiktok --page 1 --page-size 20
391
+ ```
392
+
393
+ 查看策略详情:
394
+
395
+ ```bash
396
+ nmsocial auto-replies detail --id STRATEGY_ID
397
+ ```
398
+
399
+ 创建策略:
400
+
401
+ ```bash
402
+ nmsocial auto-replies create \
403
+ --name "Demo auto reply" \
404
+ --platform tiktok \
405
+ --account-id ACCOUNT_RECORD_ID \
406
+ --trigger-rule keyword \
407
+ --keyword price \
408
+ --keyword coupon \
409
+ --send-rule sequence \
410
+ --content "Thanks for your comment" \
411
+ --content-delay 0 \
412
+ --once-per-user false \
413
+ --repeat-interval-hours 24 \
414
+ --stop-on-user-reply true \
415
+ --enabled true
416
+ ```
417
+
418
+ 通过 JSON 文件创建:
419
+
420
+ ```bash
421
+ nmsocial auto-replies create --from-file ./auto-reply.json --dry-run --json
422
+ ```
423
+
424
+ 通过 JSON 文件更新:
425
+
426
+ ```bash
427
+ nmsocial auto-replies update --id STRATEGY_ID --from-file ./auto-reply.json
428
+ ```
429
+
430
+ 启用、停用或删除策略:
431
+
432
+ ```bash
433
+ nmsocial auto-replies enable --id STRATEGY_ID
434
+ nmsocial auto-replies disable --id STRATEGY_ID
435
+ nmsocial auto-replies delete --id STRATEGY_ID
436
+ ```
437
+
438
+ ## JSON 输出
439
+
440
+ 需要在脚本中解析输出时,建议使用 `--json`。
441
+
442
+ 成功响应:
443
+
444
+ ```json
445
+ {
446
+ "ok": true,
447
+ "data": {},
448
+ "error": null,
449
+ "meta": {
450
+ "providerCode": 200,
451
+ "providerMessage": "ok",
452
+ "providerTimestamp": "2026-07-14T15:34:08.576+08:00",
453
+ "requestId": "11"
454
+ }
455
+ }
456
+ ```
457
+
458
+ 错误响应:
459
+
460
+ ```json
461
+ {
462
+ "ok": false,
463
+ "data": null,
464
+ "error": {
465
+ "code": "SOCIAL_API_ERROR",
466
+ "message": "invalid api key",
467
+ "status": 403,
468
+ "payload": {}
469
+ },
470
+ "meta": null
471
+ }
472
+ ```
473
+
474
+ ## 使用建议
475
+
476
+ - 创建或更新发布任务、评论回复、webhook、自动回复策略前,可以先使用 `--dry-run --json` 预览请求内容。
477
+ - `nmsocial status` 会隐藏完整 API Key。
478
+ - 不要把 API Key 直接写在 shell 命令中。
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgv } from '../cli/args.js';
3
+ import { printError, printSuccess } from '../cli/output.js';
4
+ import { runCommand } from '../cli/router.js';
5
+ const parsed = parseArgv(process.argv.slice(2));
6
+ const json = Boolean(parsed.options.json);
7
+ try {
8
+ const result = await runCommand(parsed);
9
+ printSuccess(result.data, result.provider, json);
10
+ }
11
+ catch (error) {
12
+ printError(error, json);
13
+ process.exitCode = 1;
14
+ }
@@ -0,0 +1,103 @@
1
+ import { CliError } from './output.js';
2
+ const BOOLEAN_FLAGS = new Set([
3
+ 'json',
4
+ 'verbose',
5
+ 'help',
6
+ 'version',
7
+ 'dry-run',
8
+ 'api-key-stdin'
9
+ ]);
10
+ export function parseArgv(argv) {
11
+ const positionals = [];
12
+ const options = {};
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ const token = argv[i];
15
+ if (token === '--') {
16
+ positionals.push(...argv.slice(i + 1));
17
+ break;
18
+ }
19
+ if (!token.startsWith('--')) {
20
+ positionals.push(token);
21
+ continue;
22
+ }
23
+ const raw = token.slice(2);
24
+ const eq = raw.indexOf('=');
25
+ const key = eq >= 0 ? raw.slice(0, eq) : raw;
26
+ let parsedValue;
27
+ if (eq >= 0) {
28
+ parsedValue = raw.slice(eq + 1);
29
+ }
30
+ else if (BOOLEAN_FLAGS.has(key)) {
31
+ parsedValue = true;
32
+ }
33
+ else {
34
+ const next = argv[i + 1];
35
+ if (next === undefined || next.startsWith('--')) {
36
+ throw new CliError('VALIDATION_REQUIRED_OPTION', `--${key} requires a value`);
37
+ }
38
+ parsedValue = next;
39
+ i += 1;
40
+ }
41
+ if (options[key] === undefined) {
42
+ options[key] = parsedValue;
43
+ }
44
+ else if (Array.isArray(options[key])) {
45
+ options[key].push(parsedValue);
46
+ }
47
+ else {
48
+ options[key] = [options[key], parsedValue];
49
+ }
50
+ }
51
+ return { positionals, options };
52
+ }
53
+ export function values(options, key) {
54
+ const value = options[key];
55
+ if (value === undefined)
56
+ return [];
57
+ return (Array.isArray(value) ? value : [value]).map((item) => String(item));
58
+ }
59
+ export function value(options, key) {
60
+ const raw = options[key];
61
+ if (raw === undefined)
62
+ return undefined;
63
+ const list = Array.isArray(raw) ? raw : [raw];
64
+ const latest = list[list.length - 1];
65
+ if (latest === true)
66
+ return 'true';
67
+ if (latest === false)
68
+ return 'false';
69
+ return latest;
70
+ }
71
+ export function has(options, key) {
72
+ return options[key] !== undefined;
73
+ }
74
+ export function requireOption(options, key) {
75
+ const result = value(options, key);
76
+ if (result === undefined || result === '') {
77
+ throw new CliError('VALIDATION_REQUIRED_OPTION', `missing required option --${key}`);
78
+ }
79
+ return result;
80
+ }
81
+ export function intOption(options, key, allowed) {
82
+ const raw = value(options, key);
83
+ if (raw === undefined)
84
+ return undefined;
85
+ const parsed = Number(raw);
86
+ if (!Number.isInteger(parsed)) {
87
+ throw new CliError('VALIDATION_INVALID_OPTION', `--${key} must be an integer`);
88
+ }
89
+ if (allowed && !allowed.includes(parsed)) {
90
+ throw new CliError('VALIDATION_INVALID_OPTION', `--${key} must be one of ${allowed.join(', ')}`);
91
+ }
92
+ return parsed;
93
+ }
94
+ export function positiveIntOption(options, key, defaultValue) {
95
+ const raw = value(options, key);
96
+ if (raw === undefined)
97
+ return defaultValue;
98
+ const parsed = Number(raw);
99
+ if (!Number.isInteger(parsed) || parsed <= 0) {
100
+ throw new CliError('VALIDATION_INVALID_OPTION', `--${key} must be a positive integer`);
101
+ }
102
+ return parsed;
103
+ }
@@ -0,0 +1,69 @@
1
+ import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { CliError } from './output.js';
5
+ const CONFIG_PATH = join(homedir(), '.nmsocial', 'config.json');
6
+ const DEFAULT_API_URL = 'https://cloud.spost.ai';
7
+ export function getConfigPath() {
8
+ return CONFIG_PATH;
9
+ }
10
+ export function defaultApiUrl() {
11
+ return DEFAULT_API_URL;
12
+ }
13
+ export async function readLocalConfig() {
14
+ try {
15
+ const text = await readFile(CONFIG_PATH, 'utf8');
16
+ return JSON.parse(text);
17
+ }
18
+ catch (error) {
19
+ if (isNodeError(error) && error.code === 'ENOENT')
20
+ return {};
21
+ if (error instanceof SyntaxError) {
22
+ throw new CliError('CONFIG_INVALID_FILE', `invalid config JSON at ${CONFIG_PATH}`);
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ export async function writeLocalConfig(config) {
28
+ await mkdir(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
29
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
30
+ await chmod(dirname(CONFIG_PATH), 0o700).catch(() => { });
31
+ await chmod(CONFIG_PATH, 0o600).catch(() => { });
32
+ }
33
+ export async function deleteLocalConfig() {
34
+ await rm(CONFIG_PATH, { force: true });
35
+ }
36
+ export async function configFileMode() {
37
+ try {
38
+ const info = await stat(CONFIG_PATH);
39
+ return (info.mode & 0o777).toString(8).padStart(3, '0');
40
+ }
41
+ catch (error) {
42
+ if (isNodeError(error) && error.code === 'ENOENT')
43
+ return null;
44
+ throw error;
45
+ }
46
+ }
47
+ export async function resolveConfig(options, { requireApiKey = true } = {}) {
48
+ const local = await readLocalConfig();
49
+ const apiUrl = options['api-url'] || process.env.NMSOCIAL_API_URL || local.apiUrl || DEFAULT_API_URL;
50
+ const apiKey = process.env.NMSOCIAL_API_KEY || local.apiKey;
51
+ validateApiUrl(String(apiUrl));
52
+ if (requireApiKey && !apiKey) {
53
+ throw new CliError('CONFIG_MISSING_API_KEY', 'missing api key; run `nmsocial login --api-key-stdin`');
54
+ }
55
+ return { apiUrl: String(apiUrl), apiKey };
56
+ }
57
+ export function validateApiUrl(apiUrl) {
58
+ try {
59
+ const url = new URL(apiUrl);
60
+ if (!['http:', 'https:'].includes(url.protocol))
61
+ throw new Error('invalid protocol');
62
+ }
63
+ catch {
64
+ throw new CliError('CONFIG_INVALID_API_URL', `invalid API URL: ${apiUrl}`);
65
+ }
66
+ }
67
+ function isNodeError(error) {
68
+ return error instanceof Error && 'code' in error;
69
+ }
@@ -0,0 +1,21 @@
1
+ export const HELP = `Northmoon Social CLI
2
+
3
+ Usage:
4
+ nmsocial login --api-key-stdin [--api-url URL]
5
+ nmsocial login --api-key YOUR_KEY
6
+ nmsocial status
7
+ nmsocial update check
8
+ nmsocial update [--manager npm|pnpm|bun]
9
+ nmsocial auth-url --platform tiktok
10
+ nmsocial accounts list [--platform tiktok] [--page 1] [--page-size 20]
11
+ nmsocial materials upload ./demo.mp4
12
+ nmsocial publish video --account OPEN_ID --content URL
13
+ nmsocial webhook set --url https://example.com/webhook
14
+ nmsocial stats overview [--platform tiktok]
15
+
16
+ Global:
17
+ --json
18
+ --api-url URL
19
+ --timeout MS
20
+ --dry-run
21
+ `;