@jclvsh/dropspace 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @jclvsh/dropspace
2
2
 
3
- MCP server and CLI for the [Dropspace](https://www.dropspace.dev) API. Lets AI agents (Claude Code, Cursor) and humans manage launches, personas, media generation, boosts, autopilot, recommendations, platform connections, API keys, and webhooks. 59 tools across 13 categories.
3
+ MCP server and CLI for the [Dropspace](https://www.dropspace.dev) API. Lets AI agents (Claude Code, Cursor) and humans manage launches, personas, media generation, boosts, autopilot, recommendations, reddit engagement, platform connections, API keys, and webhooks. 69 tools across 14 categories.
4
4
 
5
5
  ## Installation
6
6
 
@@ -49,6 +49,7 @@ dropspace launches --help
49
49
  | media | generate, job | 2 |
50
50
  | connections | list | 1 |
51
51
  | boosts | create, list, get, activate, cancel, performance | 6 |
52
+ | reddit | suggest, config, configure, discover, comments, approve, reject, standing, activate, deactivate | 10 |
52
53
  | credits | balance, purchase | 2 |
53
54
  | keys | me, list, create, rename, revoke, rotate | 6 |
54
55
  | webhooks | list, create, get, update, delete, test, rotate-secret, deliveries | 8 |
@@ -136,7 +137,7 @@ Add to your `.cursor/mcp.json`:
136
137
  | `dropspace_platforms` | string[] | No | Platforms to post via official dropspace accounts |
137
138
  | `media_mode` | string | No | `"images"` or `"video"` |
138
139
  | `user_platform_accounts` | object | No | Map of platform key to token_id (UUID). Simple keys: `twitter`, `reddit`, `instagram`, `tiktok`. LinkedIn: `linkedin:personal` or `linkedin:organization:<org_id>`. Facebook: `facebook:page:<page_id>` |
139
- | `platform_contents` | object | No | Per-platform content (mutually exclusive with `custom_content`) |
140
+ | `platform_contents` | object | No | Per-platform content (mutually exclusive with `custom_content`). Reddit accepts `metadata.subreddits` (string[], max 10, no `r/` prefix — one post per subreddit, each retryable) and `metadata.first_comment` (max 1500) |
140
141
  | `custom_content` | string \| string[] | No | Single content for all platforms, or array of tweets for twitter thread mode (mutually exclusive with `platform_contents`) |
141
142
  | `custom_content_reddit_title` | string | No | Reddit title (required with `custom_content` + reddit) |
142
143
  | `media` | object[] | No | Inline media, URL or base64 (1-10 items, mutually exclusive with `media_assets`) |
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { registerUsageCommands } from "./commands/usage.js";
14
14
  import { registerCreditCommands } from "./commands/credits.js";
15
15
  import { registerMediaCommands } from "./commands/media.js";
16
16
  import { registerBoostCommands } from "./commands/boosts.js";
17
+ import { registerRedditCommands } from "./commands/reddit.js";
17
18
  import { registerAutopilotCommands } from "./commands/autopilot.js";
18
19
  import { registerRecommendationCommands } from "./commands/recommendations.js";
19
20
  const program = new Command()
@@ -36,6 +37,7 @@ try {
36
37
  registerBoostCommands(program, client);
37
38
  registerAutopilotCommands(program, client);
38
39
  registerRecommendationCommands(program, client);
40
+ registerRedditCommands(program, client);
39
41
  await program.parseAsync(process.argv);
40
42
  }
41
43
  catch (error) {
package/dist/client.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare class DropspaceClient {
4
4
  constructor();
5
5
  get<T>(path: string, params?: Record<string, string>): Promise<T>;
6
6
  post<T>(path: string, body?: unknown): Promise<T>;
7
+ put<T>(path: string, body: unknown): Promise<T>;
7
8
  patch<T>(path: string, body: unknown): Promise<T>;
8
9
  delete(path: string): Promise<void>;
9
10
  private request;
package/dist/client.js CHANGED
@@ -31,6 +31,13 @@ export class DropspaceClient {
31
31
  body: body !== undefined ? JSON.stringify(body) : undefined,
32
32
  });
33
33
  }
34
+ async put(path, body) {
35
+ return this.request(`${this.baseUrl}${path}`, {
36
+ method: "PUT",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify(body),
39
+ });
40
+ }
34
41
  async patch(path, body) {
35
42
  return this.request(`${this.baseUrl}${path}`, {
36
43
  method: "PATCH",
@@ -31,7 +31,7 @@ export function registerLaunchCommands(program, client) {
31
31
  .option("--persona <id>", "persona ID for tone/style")
32
32
  .option("--dropspace-platforms <list>", "platforms for official dropspace accounts", parseList)
33
33
  .option("--media-mode <mode>", "media mode: images or video")
34
- .option("--platform-contents <json>", "per-platform content as JSON string")
34
+ .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":"..."}}}\'')
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-to-token_id map as JSON string")
@@ -98,7 +98,7 @@ export function registerLaunchCommands(program, client) {
98
98
  .option("--schedule <datetime>", "ISO 8601 schedule datetime (empty to clear)")
99
99
  .option("--status <status>", "status: draft, manual, trigger, scheduled, cancelled")
100
100
  .option("--persona <id>", "persona ID (empty to clear)")
101
- .option("--platform-contents <json>", "per-platform content as JSON string")
101
+ .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
102
  .option("--dropspace-platforms <list>", "official dropspace account platforms", parseList)
103
103
  .option("--user-accounts <json>", "platform-to-token_id map as JSON string")
104
104
  .option("--media-mode <mode>", "media mode: images or video")
@@ -0,0 +1,8 @@
1
+ import type { Command } from "commander";
2
+ import type { DropspaceClient } from "../client.js";
3
+ /**
4
+ * Reddit discovery + engagement CLI — the earn-standing-before-promoting loop:
5
+ * suggest → activate → discover → approve/reject → standing. Requires an api key whose
6
+ * account has reddit connected; caps and gates are enforced server-side (hard rails).
7
+ */
8
+ export declare function registerRedditCommands(program: Command, client: DropspaceClient): void;
@@ -0,0 +1,146 @@
1
+ import { formatOutput, handleError, getFormat } from "../cli-utils.js";
2
+ /**
3
+ * Reddit discovery + engagement CLI — the earn-standing-before-promoting loop:
4
+ * suggest → activate → discover → approve/reject → standing. Requires an api key whose
5
+ * account has reddit connected; caps and gates are enforced server-side (hard rails).
6
+ */
7
+ export function registerRedditCommands(program, client) {
8
+ const reddit = program.command("reddit").description("subreddit discovery + engagement (earn standing before promoting)");
9
+ reddit
10
+ .command("suggest")
11
+ .description("ICP-matched subreddit discovery with per-community standing badges")
12
+ .requiredOption("-b, --brief <text>", "what the product is and who it's for (10-2000 chars)")
13
+ .action(async (opts) => {
14
+ try {
15
+ const result = await client.post("/reddit/subreddits/suggest", { brief: opts.brief });
16
+ formatOutput(result, getFormat(program.opts()));
17
+ }
18
+ catch (error) {
19
+ handleError(error);
20
+ }
21
+ });
22
+ reddit
23
+ .command("config")
24
+ .description("read the engagement settings (enabled, caps, gate state, comment voice)")
25
+ .action(async () => {
26
+ try {
27
+ const result = await client.get("/reddit/engagement/config");
28
+ formatOutput(result, getFormat(program.opts()));
29
+ }
30
+ catch (error) {
31
+ handleError(error);
32
+ }
33
+ });
34
+ reddit
35
+ .command("configure")
36
+ .description("enable/disable engagement and tune caps (values above the rails are clamped)")
37
+ .requiredOption("--enabled <bool>", "true | false")
38
+ .option("--max-per-day <n>", "daily comment cap (clamped to 5)")
39
+ .option("--max-per-sub <n>", "per-subreddit daily cap (clamped to 2)")
40
+ .option("--min-gap <minutes>", "minimum gap between comments (floored at 20)")
41
+ .option("--persona-id <uuid|null>", "persona whose voice drafts the comments; 'null' = auto (latest)")
42
+ .action(async (opts) => {
43
+ try {
44
+ const body = { enabled: opts.enabled === "true" };
45
+ if (opts.maxPerDay)
46
+ body.max_comments_per_day = parseInt(opts.maxPerDay, 10);
47
+ if (opts.maxPerSub)
48
+ body.max_comments_per_sub_per_day = parseInt(opts.maxPerSub, 10);
49
+ if (opts.minGap)
50
+ body.min_gap_minutes = parseInt(opts.minGap, 10);
51
+ if (opts.personaId)
52
+ body.persona_id = opts.personaId === "null" ? null : opts.personaId;
53
+ const result = await client.put("/reddit/engagement/config", body);
54
+ formatOutput(result, getFormat(program.opts()));
55
+ }
56
+ catch (error) {
57
+ handleError(error);
58
+ }
59
+ });
60
+ reddit
61
+ .command("discover")
62
+ .description("draft helpful comments for one active subreddit (posts nothing — fills the review queue)")
63
+ .action(async () => {
64
+ try {
65
+ const result = await client.post("/reddit/engagement/discover");
66
+ formatOutput(result, getFormat(program.opts()));
67
+ }
68
+ catch (error) {
69
+ handleError(error);
70
+ }
71
+ });
72
+ reddit
73
+ .command("comments")
74
+ .description("list drafted/posted engagement comments")
75
+ .option("--status <status>", "pending (review queue, default) | history")
76
+ .action(async (opts) => {
77
+ try {
78
+ const result = await client.get("/reddit/engagement/comments", opts.status ? { status: opts.status } : undefined);
79
+ formatOutput(result, getFormat(program.opts()));
80
+ }
81
+ catch (error) {
82
+ handleError(error);
83
+ }
84
+ });
85
+ reddit
86
+ .command("approve <comment-id>")
87
+ .description("approve a drafted comment — POSTS it into a pacing slot (caps/gaps still enforced)")
88
+ .option("--body <text>", "edited comment text (replaces the draft)")
89
+ .action(async (commentId, opts) => {
90
+ try {
91
+ const result = await client.post(`/reddit/engagement/comments/${commentId}/approve`, opts.body ? { body: opts.body } : undefined);
92
+ formatOutput(result, getFormat(program.opts()));
93
+ }
94
+ catch (error) {
95
+ handleError(error);
96
+ }
97
+ });
98
+ reddit
99
+ .command("reject <comment-id>")
100
+ .description("discard a drafted comment without posting")
101
+ .action(async (commentId) => {
102
+ try {
103
+ const result = await client.post(`/reddit/engagement/comments/${commentId}/reject`);
104
+ formatOutput(result, getFormat(program.opts()));
105
+ }
106
+ catch (error) {
107
+ handleError(error);
108
+ }
109
+ });
110
+ reddit
111
+ .command("standing")
112
+ .description("earned standing per subreddit (tier, comments, karma, any removal pause)")
113
+ .action(async () => {
114
+ try {
115
+ const result = await client.get("/reddit/engagement/standing");
116
+ formatOutput(result, getFormat(program.opts()));
117
+ }
118
+ catch (error) {
119
+ handleError(error);
120
+ }
121
+ });
122
+ reddit
123
+ .command("activate <subreddit>")
124
+ .description("activate a subreddit as an engagement candidate (you pick the communities)")
125
+ .action(async (subreddit) => {
126
+ try {
127
+ const result = await client.post("/reddit/engagement/subreddits", { subreddit });
128
+ formatOutput(result, getFormat(program.opts()));
129
+ }
130
+ catch (error) {
131
+ handleError(error);
132
+ }
133
+ });
134
+ reddit
135
+ .command("deactivate <subreddit>")
136
+ .description("stop drafting comments for a subreddit (earned standing is kept)")
137
+ .action(async (subreddit) => {
138
+ try {
139
+ await client.delete(`/reddit/engagement/subreddits/${encodeURIComponent(subreddit)}`);
140
+ formatOutput({ subreddit, engagement_enabled: false }, getFormat(program.opts()));
141
+ }
142
+ catch (error) {
143
+ handleError(error);
144
+ }
145
+ });
146
+ }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { registerMediaTools } from "./tools/media.js";
14
14
  import { registerBoostTools } from "./tools/boosts.js";
15
15
  import { registerAutopilotTools } from "./tools/autopilot.js";
16
16
  import { registerRecommendationTools } from "./tools/recommendations.js";
17
+ import { registerRedditTools } from "./tools/reddit.js";
17
18
  const server = new McpServer({
18
19
  name: "dropspace",
19
20
  version: "2.9.0",
@@ -31,5 +32,6 @@ registerMediaTools(server, client);
31
32
  registerBoostTools(server, client);
32
33
  registerAutopilotTools(server, client);
33
34
  registerRecommendationTools(server, client);
35
+ registerRedditTools(server, client);
34
36
  const transport = new StdioServerTransport();
35
37
  await server.connect(transport);
@@ -102,7 +102,7 @@ export function registerLaunchTools(server, client) {
102
102
  platform_contents: z
103
103
  .record(z.string(), z.record(z.string(), z.unknown()))
104
104
  .optional()
105
- .describe('per-platform content — each value needs "content" (string). Reddit also requires "title" (max 300). 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'),
105
+ .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) }. 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'),
106
106
  custom_content: z
107
107
  .union([z.string().min(1), z.array(z.string()).min(1).max(6)])
108
108
  .optional()
@@ -216,7 +216,7 @@ export function registerLaunchTools(server, client) {
216
216
  platform_contents: z
217
217
  .record(z.string(), z.record(z.string(), z.unknown()))
218
218
  .optional()
219
- .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. 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'),
219
+ .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'),
220
220
  dropspace_platforms: z.array(z.enum(PLATFORMS)).optional().describe("official dropspace account platforms"),
221
221
  user_platform_accounts: z
222
222
  .record(z.string(), z.string())
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Reddit engagement + discovery MCP tools.
3
+ *
4
+ * The full loop an agent runs to earn standing before promoting (reddit's 90:10 norm):
5
+ * suggest ICP subreddits → activate the relevant ones → discover (drafts helpful comments) →
6
+ * approve/reject within the hard rails → standing grows → launch fan-out posts clear the
7
+ * publish-time 90:10 gate. Requires an api key whose account has reddit connected.
8
+ */
9
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { DropspaceClient } from "../client.js";
11
+ export declare function registerRedditTools(server: McpServer, client: DropspaceClient): void;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Reddit engagement + discovery MCP tools.
3
+ *
4
+ * The full loop an agent runs to earn standing before promoting (reddit's 90:10 norm):
5
+ * suggest ICP subreddits → activate the relevant ones → discover (drafts helpful comments) →
6
+ * approve/reject within the hard rails → standing grows → launch fan-out posts clear the
7
+ * publish-time 90:10 gate. Requires an api key whose account has reddit connected.
8
+ */
9
+ import { z } from "zod";
10
+ const subredditParam = z
11
+ .string()
12
+ .min(3)
13
+ .describe("community name, no r/ prefix (canonicalized to lowercase server-side)");
14
+ const commentIdParam = z.string().uuid().describe("engagement comment ID (from list_engagement_comments)");
15
+ function ok(result) {
16
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
17
+ }
18
+ function fail(error) {
19
+ return {
20
+ content: [{ type: "text", text: `error: ${error instanceof Error ? error.message : String(error)}` }],
21
+ isError: true,
22
+ };
23
+ }
24
+ export function registerRedditTools(server, client) {
25
+ server.tool("suggest_subreddits", "ICP-matched subreddit discovery: pass a product brief, get verified real subreddits with a promo-policy classification and a green/yellow/red standing badge for THIS account. prefer green subs; earn standing in yellow ones by commenting before posting.", {
26
+ brief: z.string().min(10).max(2000).describe("what the product is and who it's for"),
27
+ }, async (args) => {
28
+ try {
29
+ return ok(await client.post("/reddit/subreddits/suggest", args));
30
+ }
31
+ catch (error) {
32
+ return fail(error);
33
+ }
34
+ });
35
+ server.tool("get_engagement_config", "read the account's reddit engagement settings — enabled, caps, the 14d/50-karma gate state, and which persona voices the comments.", {}, async () => {
36
+ try {
37
+ return ok(await client.get("/reddit/engagement/config"));
38
+ }
39
+ catch (error) {
40
+ return fail(error);
41
+ }
42
+ });
43
+ server.tool("update_engagement_config", "enable/disable reddit engagement and tune caps. caps are STRUCTURAL RAILS: values above the ceilings (5/day, 2/sub/day, 20-min gap) are clamped, not obeyed. persona_id pins which persona voices the comments (null = auto latest).", {
44
+ enabled: z.boolean().describe("turn engagement on/off for this account"),
45
+ max_comments_per_day: z.number().int().min(1).max(5).optional().describe("clamped to 5"),
46
+ max_comments_per_sub_per_day: z.number().int().min(1).max(2).optional().describe("clamped to 2"),
47
+ min_gap_minutes: z.number().int().min(20).optional().describe("floored at 20"),
48
+ persona_id: z
49
+ .string()
50
+ .uuid()
51
+ .nullable()
52
+ .optional()
53
+ .describe("persona whose voice drafts the comments; null = auto (latest complete persona)"),
54
+ }, async (args) => {
55
+ try {
56
+ return ok(await client.put("/reddit/engagement/config", args));
57
+ }
58
+ catch (error) {
59
+ return fail(error);
60
+ }
61
+ });
62
+ server.tool("discover_engagement", "draft helpful comments for ONE of the account's active subreddits (call again to cover more). fills the review queue; posts NOTHING — approval is the separate, rail-gated step. `skipped` names why nothing ran (kill_switch | not_enabled | gate_not_passed).", {}, async () => {
63
+ try {
64
+ return ok(await client.post("/reddit/engagement/discover"));
65
+ }
66
+ catch (error) {
67
+ return fail(error);
68
+ }
69
+ });
70
+ server.tool("list_engagement_comments", "list drafted/posted engagement comments. status=pending (default) is the review queue awaiting approve/reject; status=history is everything already acted on.", {
71
+ status: z.enum(["pending", "history"]).optional().describe("default: pending"),
72
+ }, async (args) => {
73
+ try {
74
+ return ok(await client.get("/reddit/engagement/comments", args.status ? { status: args.status } : undefined));
75
+ }
76
+ catch (error) {
77
+ return fail(error);
78
+ }
79
+ });
80
+ server.tool("approve_engagement_comment", "approve a drafted comment — this POSTS it (scheduled into a pacing slot). the rails still apply: per-day/per-sub caps and the min-gap return 429 when hit. optional body edits the draft before posting.", {
81
+ comment_id: commentIdParam,
82
+ body: z.string().min(20).max(1500).optional().describe("edited comment text (replaces the draft)"),
83
+ }, async (args) => {
84
+ try {
85
+ return ok(await client.post(`/reddit/engagement/comments/${args.comment_id}/approve`, args.body ? { body: args.body } : undefined));
86
+ }
87
+ catch (error) {
88
+ return fail(error);
89
+ }
90
+ });
91
+ server.tool("reject_engagement_comment", "discard a drafted comment without posting.", { comment_id: commentIdParam }, async (args) => {
92
+ try {
93
+ return ok(await client.post(`/reddit/engagement/comments/${args.comment_id}/reject`));
94
+ }
95
+ catch (error) {
96
+ return fail(error);
97
+ }
98
+ });
99
+ server.tool("get_engagement_standing", "the standing the account has EARNED per subreddit — tier (none → seedling → active → established), comments posted, karma earned, and any active removal pause. this is what lets fan-out posts clear the 90:10 gate.", {}, async () => {
100
+ try {
101
+ return ok(await client.get("/reddit/engagement/standing"));
102
+ }
103
+ catch (error) {
104
+ return fail(error);
105
+ }
106
+ });
107
+ server.tool("activate_engagement_subreddit", "activate a subreddit as an engagement candidate — the seed that lets a fresh account draft its first comments. pick subs from suggest_subreddits; YOU choose the communities, the machine only sustains engagement within them.", { subreddit: subredditParam }, async (args) => {
108
+ try {
109
+ return ok(await client.post("/reddit/engagement/subreddits", { subreddit: args.subreddit }));
110
+ }
111
+ catch (error) {
112
+ return fail(error);
113
+ }
114
+ });
115
+ server.tool("deactivate_engagement_subreddit", "stop drafting comments for a subreddit. earned standing is kept, so re-activating later resumes where it left off.", { subreddit: subredditParam }, async (args) => {
116
+ try {
117
+ await client.delete(`/reddit/engagement/subreddits/${encodeURIComponent(args.subreddit)}`);
118
+ return ok({ subreddit: args.subreddit, engagement_enabled: false });
119
+ }
120
+ catch (error) {
121
+ return fail(error);
122
+ }
123
+ });
124
+ }
package/dist/types.d.ts CHANGED
@@ -423,3 +423,69 @@ export interface RecommendationApplyResponse {
423
423
  recommendation: Recommendation;
424
424
  outcome: RecommendationApplyOutcome;
425
425
  }
426
+ export interface SubredditStandingBadge {
427
+ level: string;
428
+ tier: string;
429
+ promo_posts_here: number;
430
+ contributions_here: number;
431
+ promo_share: number | null;
432
+ reasons: string[];
433
+ }
434
+ export interface SubredditSuggestion {
435
+ name: string;
436
+ title: string;
437
+ subscribers: number;
438
+ over18: boolean;
439
+ subreddit_type: string;
440
+ promo_policy: string;
441
+ promo_evidence: string | null;
442
+ standing: SubredditStandingBadge;
443
+ reason: string;
444
+ }
445
+ export interface SubredditSuggestions {
446
+ icp_summary: string;
447
+ suggestions: SubredditSuggestion[];
448
+ }
449
+ export interface EngagementConfig {
450
+ enabled: boolean;
451
+ max_comments_per_day: number;
452
+ max_comments_per_sub_per_day: number;
453
+ min_gap_minutes: number;
454
+ gate_passed: boolean;
455
+ account_username: string | null;
456
+ account_total_karma: number | null;
457
+ persona_id: string | null;
458
+ }
459
+ export interface EngagementComment {
460
+ id: string;
461
+ comment_kind: string;
462
+ subreddit: string;
463
+ body: string;
464
+ status: string;
465
+ scheduled_for: string | null;
466
+ comment_permalink: string | null;
467
+ comment_score: number | null;
468
+ error_message: string | null;
469
+ created_at: string;
470
+ target: {
471
+ subreddit: string;
472
+ thread_title: string;
473
+ thread_url: string;
474
+ thread_author: string | null;
475
+ relevance_score: number | null;
476
+ } | null;
477
+ }
478
+ export interface SubredditStanding {
479
+ subreddit: string;
480
+ tier: string;
481
+ comments_posted: number;
482
+ comments_removed: number;
483
+ karma_earned: number;
484
+ paused_until: string | null;
485
+ pause_reason: string | null;
486
+ last_activity_at: string | null;
487
+ }
488
+ export interface EngagementSweepResult {
489
+ drafted: number;
490
+ skipped: string | null;
491
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jclvsh/dropspace",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "description": "MCP server and CLI for the Dropspace API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",