@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 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,3 @@
1
+ import type { Command } from "commander";
2
+ import type { DropspaceClient } from "../client.js";
3
+ export declare function registerAudienceCommands(program: Command, client: DropspaceClient): void;
@@ -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
- .action(async (id) => {
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 result = await client.post(`/launches/${id}/retry`);
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.9.0",
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,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { DropspaceClient } from "../client.js";
3
+ export declare function registerAudienceTools(server: McpServer, client: DropspaceClient): void;
@@ -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
  }
@@ -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
- }, async ({ id }) => {
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 result = await client.post(`/launches/${id}/retry`);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jclvsh/dropspace",
3
- "version": "2.17.0",
3
+ "version": "2.20.0",
4
4
  "description": "MCP server and CLI for the Dropspace API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",