@jclvsh/dropspace 2.17.0 → 2.20.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/dist/cli.js +2 -0
- package/dist/commands/audience.d.ts +3 -0
- package/dist/commands/audience.js +19 -0
- package/dist/commands/connections.js +15 -0
- package/dist/commands/launches.js +16 -3
- package/dist/index.js +3 -1
- package/dist/tools/audience.d.ts +3 -0
- package/dist/tools/audience.js +30 -0
- package/dist/tools/connections.js +23 -1
- package/dist/tools/launches.js +25 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerBoostCommands } from "./commands/boosts.js";
|
|
|
17
17
|
import { registerRedditCommands } from "./commands/reddit.js";
|
|
18
18
|
import { registerAutopilotCommands } from "./commands/autopilot.js";
|
|
19
19
|
import { registerRecommendationCommands } from "./commands/recommendations.js";
|
|
20
|
+
import { registerAudienceCommands } from "./commands/audience.js";
|
|
20
21
|
const program = new Command()
|
|
21
22
|
.name("dropspace")
|
|
22
23
|
.description("dropspace CLI - manage launches, personas, boosts, and more")
|
|
@@ -32,6 +33,7 @@ try {
|
|
|
32
33
|
registerWebhookCommands(program, client);
|
|
33
34
|
registerDropspaceCommands(program, client);
|
|
34
35
|
registerUsageCommands(program, client);
|
|
36
|
+
registerAudienceCommands(program, client);
|
|
35
37
|
registerCreditCommands(program, client);
|
|
36
38
|
registerMediaCommands(program, client);
|
|
37
39
|
registerBoostCommands(program, client);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { formatOutput, handleError, getFormat } from "../cli-utils.js";
|
|
2
|
+
export function registerAudienceCommands(program, client) {
|
|
3
|
+
program
|
|
4
|
+
.command("audience")
|
|
5
|
+
.description("follower counts + growth per connected account (instagram, youtube, tiktok, twitter, facebook pages)")
|
|
6
|
+
.option("-w, --window-days <n>", "growth window in days (max 365)", "30")
|
|
7
|
+
.action(async (opts) => {
|
|
8
|
+
try {
|
|
9
|
+
const params = {};
|
|
10
|
+
if (opts.windowDays)
|
|
11
|
+
params.window_days = opts.windowDays;
|
|
12
|
+
const result = await client.get("/audience", params);
|
|
13
|
+
formatOutput(result, getFormat(program.opts()));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
handleError(error);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -36,4 +36,19 @@ export function registerConnectionCommands(program, client) {
|
|
|
36
36
|
handleError(error);
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
+
connections
|
|
40
|
+
.command("profile <platform>")
|
|
41
|
+
.description("edit the connected account's profile on the platform (youtube only today: the channel description, max 1,000 chars; pass an empty string to clear)")
|
|
42
|
+
.requiredOption("--description <text>", "the new profile / channel description")
|
|
43
|
+
.action(async (platform, opts) => {
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.patch(`/connections/${encodeURIComponent(platform)}/profile`, {
|
|
46
|
+
description: opts.description,
|
|
47
|
+
});
|
|
48
|
+
formatOutput(result, getFormat(program.opts()));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
handleError(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
39
54
|
}
|
|
@@ -35,6 +35,7 @@ export function registerLaunchCommands(program, client) {
|
|
|
35
35
|
.option("--custom-content <text>", "single content string for all platforms")
|
|
36
36
|
.option("--reddit-title <title>", "reddit title (with custom-content)")
|
|
37
37
|
.option("--user-accounts <json>", 'platform→token_id map as JSON. simple key = primary account ("instagram"); composite key targets a specific account when you have several ("instagram:<account_id>", ids from `connections list`) — works for instagram/tiktok/youtube/twitter/reddit')
|
|
38
|
+
.option("--attribution-mode <mode>", 'how official-account posts credit you: "handle" (default) or "anonymous"')
|
|
38
39
|
.option("--publish", "immediately publish after creation")
|
|
39
40
|
.option("--wait", "wait for publishing to complete (requires --publish)")
|
|
40
41
|
.action(async (opts) => {
|
|
@@ -52,6 +53,8 @@ export function registerLaunchCommands(program, client) {
|
|
|
52
53
|
body.persona_id = opts.persona;
|
|
53
54
|
if (opts.dropspacePlatforms)
|
|
54
55
|
body.dropspace_platforms = opts.dropspacePlatforms;
|
|
56
|
+
if (opts.attributionMode)
|
|
57
|
+
body.attribution_mode = opts.attributionMode;
|
|
55
58
|
if (opts.mediaMode)
|
|
56
59
|
body.media_mode = opts.mediaMode;
|
|
57
60
|
if (opts.platformContents)
|
|
@@ -100,6 +103,7 @@ export function registerLaunchCommands(program, client) {
|
|
|
100
103
|
.option("--persona <id>", "persona ID (empty to clear)")
|
|
101
104
|
.option("--platform-contents <json>", 'per-platform content as JSON string. reddit supports metadata.subreddits (string[], max 10, no r/ prefix — one post per subreddit) and metadata.first_comment (max 1500). e.g. \'{"reddit":{"title":"...","content":"...","metadata":{"subreddits":["SideProject"],"first_comment":"..."}}}\'')
|
|
102
105
|
.option("--dropspace-platforms <list>", "official dropspace account platforms", parseList)
|
|
106
|
+
.option("--attribution-mode <mode>", 'how official-account posts credit you: "handle" (default) or "anonymous"')
|
|
103
107
|
.option("--user-accounts <json>", 'platform→token_id map as JSON. simple key = primary account ("instagram"); composite key targets a specific account when you have several ("instagram:<account_id>", ids from `connections list`) — works for instagram/tiktok/youtube/twitter/reddit')
|
|
104
108
|
.option("--media-mode <mode>", "media mode: images or video")
|
|
105
109
|
.action(async (id, opts) => {
|
|
@@ -123,6 +127,8 @@ export function registerLaunchCommands(program, client) {
|
|
|
123
127
|
body.platform_contents = JSON.parse(opts.platformContents);
|
|
124
128
|
if (opts.dropspacePlatforms)
|
|
125
129
|
body.dropspace_platforms = opts.dropspacePlatforms;
|
|
130
|
+
if (opts.attributionMode)
|
|
131
|
+
body.attribution_mode = opts.attributionMode;
|
|
126
132
|
if (opts.userAccounts)
|
|
127
133
|
body.user_platform_accounts = JSON.parse(opts.userAccounts);
|
|
128
134
|
if (opts.mediaMode)
|
|
@@ -160,10 +166,17 @@ export function registerLaunchCommands(program, client) {
|
|
|
160
166
|
});
|
|
161
167
|
launches
|
|
162
168
|
.command("retry <id>")
|
|
163
|
-
.description("retry failed platforms for a launch")
|
|
164
|
-
.
|
|
169
|
+
.description("retry failed platforms for a launch, or one reddit destination with --subreddit")
|
|
170
|
+
.option("--subreddit <name>", "retry only this reddit destination (no r/ prefix)")
|
|
171
|
+
.option("--account-id <id>", "the connected reddit account owning that destination (entity_id from `connections list`); omit for your primary. only meaningful with --subreddit")
|
|
172
|
+
.action(async (id, opts) => {
|
|
165
173
|
try {
|
|
166
|
-
const
|
|
174
|
+
const body = {};
|
|
175
|
+
if (opts.subreddit)
|
|
176
|
+
body.subreddit = opts.subreddit;
|
|
177
|
+
if (opts.accountId)
|
|
178
|
+
body.account_id = opts.accountId;
|
|
179
|
+
const result = await client.post(`/launches/${id}/retry`, Object.keys(body).length > 0 ? body : undefined);
|
|
167
180
|
formatOutput(result, getFormat(program.opts()));
|
|
168
181
|
}
|
|
169
182
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,10 @@ import { registerBoostTools } from "./tools/boosts.js";
|
|
|
15
15
|
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
16
16
|
import { registerRecommendationTools } from "./tools/recommendations.js";
|
|
17
17
|
import { registerRedditTools } from "./tools/reddit.js";
|
|
18
|
+
import { registerAudienceTools } from "./tools/audience.js";
|
|
18
19
|
const server = new McpServer({
|
|
19
20
|
name: "dropspace",
|
|
20
|
-
version: "2.
|
|
21
|
+
version: "2.20.0",
|
|
21
22
|
});
|
|
22
23
|
const client = new DropspaceClient();
|
|
23
24
|
registerLaunchTools(server, client);
|
|
@@ -33,5 +34,6 @@ registerBoostTools(server, client);
|
|
|
33
34
|
registerAutopilotTools(server, client);
|
|
34
35
|
registerRecommendationTools(server, client);
|
|
35
36
|
registerRedditTools(server, client);
|
|
37
|
+
registerAudienceTools(server, client);
|
|
36
38
|
const transport = new StdioServerTransport();
|
|
37
39
|
await server.connect(transport);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerAudienceTools(server, client) {
|
|
3
|
+
server.tool("get_audience", 'follower counts + growth for every connected instagram / youtube / tiktok / twitter account and facebook page. one entry per ACCOUNT, not per platform: "entity_id" is null for your primary account on a platform and set for each additional one, so a user with two instagram accounts returns two instagram entries. counts are read live from each platform API (cached ~15 min); "series" is the stored history. growth is measured from the OLDEST stored point inside the window, which — because history points are recorded when you read, at most one per account per day — can be much younger than window_days: read "effective_window_days" (the span the delta actually covers) and "baseline_captured_at" before comparing growth across accounts. linkedin is not covered (its API exposes no personal-profile follower count).', {
|
|
4
|
+
window_days: z
|
|
5
|
+
.number()
|
|
6
|
+
.int()
|
|
7
|
+
.min(1)
|
|
8
|
+
.max(365)
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("growth window in days (default: 30, max: 365)"),
|
|
11
|
+
}, async ({ window_days }) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = await client.get("/audience", window_days ? { window_days: String(window_days) } : undefined);
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
isError: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerConnectionTools(server, client) {
|
|
3
|
-
server.tool("list_connections", 'list your connected social media accounts. each row has a "platform" and an "entity_id" (null for your primary account on that platform, set for additional accounts). to target a specific account in create_launch when you have more than one on instagram/tiktok/youtube, pass "{platform}:{entity_id}" as a user_platform_accounts key', {
|
|
3
|
+
server.tool("list_connections", 'list your connected social media accounts. each row has a "platform" and an "entity_id" (null for your primary account on that platform, set for additional accounts). to target a specific account in create_launch when you have more than one on instagram / tiktok / youtube / twitter / reddit, pass "{platform}:{entity_id}" as a user_platform_accounts key (the bare "{platform}" key targets your primary). the same entity_id is what DELETE /connections/{platform}?account_id=... disconnects, and what retry_launch takes as account_id', {
|
|
4
4
|
page: z.number().int().positive().optional().describe("page number (default: 1)"),
|
|
5
5
|
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default: 50, max: 100)"),
|
|
6
6
|
}, async ({ page, page_size }) => {
|
|
@@ -27,4 +27,26 @@ export function registerConnectionTools(server, client) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
+
server.tool("update_connection_profile", "edit the connected account's profile ON the platform. youtube only today: sets the channel description (max 1,000 characters; an empty string clears it) via channels.update. instagram's api can read a profile but not write it, facebook page \"about\" needs a permission dropspace does not hold, and tiktok / x / linkedin expose no profile write - those return an error with the reason. requires the write scope.", {
|
|
31
|
+
platform: z.string().describe('platform whose profile to edit (currently only "youtube")'),
|
|
32
|
+
description: z.string().max(1000).describe("the new profile / channel description (max 1,000 characters)"),
|
|
33
|
+
}, async ({ platform, description }) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await client.patch(`/connections/${encodeURIComponent(platform)}/profile`, { description });
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
30
52
|
}
|
package/dist/tools/launches.js
CHANGED
|
@@ -98,6 +98,10 @@ export function registerLaunchTools(server, client) {
|
|
|
98
98
|
.array(z.enum(PLATFORMS))
|
|
99
99
|
.optional()
|
|
100
100
|
.describe("platforms to post via official dropspace accounts"),
|
|
101
|
+
attribution_mode: z
|
|
102
|
+
.enum(["handle", "anonymous"])
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('how official-account posts credit the author: "handle" (default) signs with the connected @handle on that platform when one exists, "anonymous" always uses the plain "posted from dropspace user" note'),
|
|
101
105
|
media_mode: z.enum(["images", "video"]).optional().describe("media mode for the launch"),
|
|
102
106
|
user_platform_accounts: z
|
|
103
107
|
.record(z.string(), z.string())
|
|
@@ -106,7 +110,7 @@ export function registerLaunchTools(server, client) {
|
|
|
106
110
|
platform_contents: z
|
|
107
111
|
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
108
112
|
.optional()
|
|
109
|
-
.describe('per-platform content — each value needs "content" (string). Reddit also requires "title" (max 300) and supports "metadata": { "subreddits": string[] (max 10, names without the r/ prefix — posts the launch once per subreddit, each tracked and retryable on its own; omit to post to the default community), "first_comment": string (max 1500 — posted as a comment under each reddit post once it goes live) }. for multi-account reddit, each connected account can target its own subreddits via "reddit:<account_id>" content keys (falls back to the base "reddit" block); a subreddit is posted by at most one of your accounts per launch (first wins, others skipped as SUBREDDIT_ALREADY_TARGETED). each subreddit has its own rules: many restrict or ban promotional posts and posting where the account has no history gets filtered, so target a few genuinely relevant subreddits rather than blasting. Twitter supports "thread" (string[], max 6, each ≤280 or ≤25,000 for X Premium) instead of "content". TikTok supports "tiktok_settings": { "publish_mode"?: "direct"|"draft" (default "direct"; "draft" uploads to the TikTok inbox to finish and publish in-app), "privacy_level": "PUBLIC_TO_EVERYONE"|"FOLLOWER_OF_CREATOR"|"MUTUAL_FOLLOW_FRIENDS"|"SELF_ONLY" (required before publish in direct mode; not required for draft), "allow_comments"?, "allow_duet"?, "allow_stitch"?, "is_commercial"?, "is_your_brand"?, "is_branded_content"?, "auto_add_music"? (booleans, direct mode) }. YouTube requires video and supports "title" (max 100 chars). character limits: Twitter 280/tweet (25,000 for X Premium), LinkedIn 3000, Instagram 2200, Reddit 3000, Facebook 3000, TikTok 4000, YouTube 5000 (description). X Premium status is auto-detected from the connected account. mutually exclusive with custom_content'),
|
|
113
|
+
.describe('per-platform content — each value needs "content" (string). Reddit also requires "title" (max 300) and supports "metadata": { "subreddits": string[] (max 10, names without the r/ prefix — posts the launch once per subreddit, each tracked and retryable on its own; omit to post to the default community), "first_comment": string (max 1500 — posted as a comment under each reddit post once it goes live) }. for multi-account reddit, each connected account can target its own subreddits via "reddit:<account_id>" content keys (falls back to the base "reddit" block); a subreddit is posted by at most one of your accounts per launch (first wins, others skipped as SUBREDDIT_ALREADY_TARGETED). each subreddit has its own rules: many restrict or ban promotional posts and posting where the account has no history gets filtered, so target a few genuinely relevant subreddits rather than blasting. Twitter supports "thread" (string[], max 6, each ≤280 or ≤25,000 for X Premium) instead of "content". TikTok supports "tiktok_settings": { "publish_mode"?: "direct"|"draft" (default "direct"; "draft" uploads to the TikTok inbox to finish and publish in-app), "privacy_level": "PUBLIC_TO_EVERYONE"|"FOLLOWER_OF_CREATOR"|"MUTUAL_FOLLOW_FRIENDS"|"SELF_ONLY" (required before publish in direct mode; not required for draft), "allow_comments"?, "allow_duet"?, "allow_stitch"?, "is_commercial"?, "is_your_brand"?, "is_branded_content"?, "auto_add_music"? (booleans, direct mode), "cover_timestamp_ms"? (integer 0-600000 — which frame TikTok uses as the cover, in ms from the start; VIDEO posts only, photo posts take their cover by index; omit for the first frame) }. YouTube requires video and supports "title" (max 100 chars). character limits: Twitter 280/tweet (25,000 for X Premium), LinkedIn 3000, Instagram 2200, Reddit 3000, Facebook 3000, TikTok 4000, YouTube 5000 (description). X Premium status is auto-detected from the connected account. mutually exclusive with custom_content'),
|
|
110
114
|
custom_content: z
|
|
111
115
|
.union([z.string().min(1), z.array(z.string()).min(1).max(6)])
|
|
112
116
|
.optional()
|
|
@@ -222,6 +226,10 @@ export function registerLaunchTools(server, client) {
|
|
|
222
226
|
.optional()
|
|
223
227
|
.describe('per-platform content update (deep-merged with existing). each value can include "content" (string). Twitter supports "thread" (string[]) instead. Reddit "title" is optional on update, and "metadata" accepts "subreddits" (string[], max 10, no r/ prefix — one post per subreddit) and "first_comment" (string, max 1500). TikTok "tiktok_settings" is deep-merged — you can update individual fields (e.g. just "privacy_level") without overwriting the rest. see create_launch for full field details'),
|
|
224
228
|
dropspace_platforms: z.array(z.enum(PLATFORMS)).optional().describe("official dropspace account platforms"),
|
|
229
|
+
attribution_mode: z
|
|
230
|
+
.enum(["handle", "anonymous"])
|
|
231
|
+
.optional()
|
|
232
|
+
.describe('how official-account posts credit the author ("handle" default, "anonymous" for the plain note)'),
|
|
225
233
|
user_platform_accounts: z
|
|
226
234
|
.record(z.string(), z.string())
|
|
227
235
|
.optional()
|
|
@@ -302,11 +310,24 @@ export function registerLaunchTools(server, client) {
|
|
|
302
310
|
};
|
|
303
311
|
}
|
|
304
312
|
});
|
|
305
|
-
server.tool("retry_launch", "retry failed platforms for a launch", {
|
|
313
|
+
server.tool("retry_launch", "retry failed platforms for a launch. by default every failed platform is retried; pass `subreddit` to retry ONE reddit destination (leaving its siblings — succeeded or failed — untouched), and add `account_id` when that destination belongs to a secondary connected reddit account rather than your primary.", {
|
|
306
314
|
id: z.string().uuid().describe("launch ID"),
|
|
307
|
-
|
|
315
|
+
subreddit: z
|
|
316
|
+
.string()
|
|
317
|
+
.optional()
|
|
318
|
+
.describe("retry only this reddit destination (name without the r/ prefix)"),
|
|
319
|
+
account_id: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("the connected reddit account that owns the destination (its entity_id from list_connections); omit for your primary account. only meaningful with `subreddit`"),
|
|
323
|
+
}, async ({ id, subreddit, account_id }) => {
|
|
308
324
|
try {
|
|
309
|
-
const
|
|
325
|
+
const body = {};
|
|
326
|
+
if (subreddit)
|
|
327
|
+
body.subreddit = subreddit;
|
|
328
|
+
if (account_id)
|
|
329
|
+
body.account_id = account_id;
|
|
330
|
+
const result = await client.post(`/launches/${id}/retry`, Object.keys(body).length > 0 ? body : undefined);
|
|
310
331
|
return {
|
|
311
332
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
312
333
|
};
|