@jclvsh/dropspace 2.8.0 → 2.10.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 +15 -2
- package/dist/cli-utils.d.ts +54 -0
- package/dist/cli-utils.js +94 -1
- package/dist/cli.js +2 -0
- package/dist/commands/launches.js +2 -2
- package/dist/commands/recommendations.d.ts +3 -0
- package/dist/commands/recommendations.js +60 -0
- package/dist/index.js +3 -1
- package/dist/tools/launches.js +2 -2
- package/dist/tools/recommendations.d.ts +3 -0
- package/dist/tools/recommendations.js +61 -0
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
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, platform connections, API keys, and webhooks.
|
|
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.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -55,6 +55,7 @@ dropspace launches --help
|
|
|
55
55
|
| status | dropspace official account connections | 1 |
|
|
56
56
|
| usage | plan, limits, and billing period | 1 |
|
|
57
57
|
| autopilot | status, configure, review, approve, reject, run, runs | 7 |
|
|
58
|
+
| recommendations (recs) | list, apply, dismiss | 3 |
|
|
58
59
|
|
|
59
60
|
## MCP Server Configuration
|
|
60
61
|
|
|
@@ -135,7 +136,7 @@ Add to your `.cursor/mcp.json`:
|
|
|
135
136
|
| `dropspace_platforms` | string[] | No | Platforms to post via official dropspace accounts |
|
|
136
137
|
| `media_mode` | string | No | `"images"` or `"video"` |
|
|
137
138
|
| `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>` |
|
|
138
|
-
| `platform_contents` | object | No | Per-platform content (mutually exclusive with `custom_content`)
|
|
139
|
+
| `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) |
|
|
139
140
|
| `custom_content` | string \| string[] | No | Single content for all platforms, or array of tweets for twitter thread mode (mutually exclusive with `platform_contents`) |
|
|
140
141
|
| `custom_content_reddit_title` | string | No | Reddit title (required with `custom_content` + reddit) |
|
|
141
142
|
| `media` | object[] | No | Inline media, URL or base64 (1-10 items, mutually exclusive with `media_assets`) |
|
|
@@ -176,6 +177,8 @@ Add to your `.cursor/mcp.json`:
|
|
|
176
177
|
| `update_persona` | Update a persona's name or writing samples |
|
|
177
178
|
| `delete_persona` | Delete a persona |
|
|
178
179
|
| `analyze_persona` | Trigger AI analysis of a persona's writing style |
|
|
180
|
+
| `finalize_persona` | Accept a completed analysis so the persona is usable |
|
|
181
|
+
| `discard_persona_analysis` | Discard a pending analysis result |
|
|
179
182
|
|
|
180
183
|
#### `analyze_persona` Parameters
|
|
181
184
|
|
|
@@ -310,6 +313,16 @@ Add to your `.cursor/mcp.json`:
|
|
|
310
313
|
|
|
311
314
|
Autopilot is available on pro and premium plans.
|
|
312
315
|
|
|
316
|
+
### Recommendations
|
|
317
|
+
|
|
318
|
+
| Tool | Description |
|
|
319
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
320
|
+
| `list_recommendations` | List actionable insights from your organic post performance (winners, boost/promote/pause signals) |
|
|
321
|
+
| `apply_recommendation` | Apply the one-click action: draft launch, PAUSED boost (needs `ad_account_id`), autopilot queue, directive |
|
|
322
|
+
| `dismiss_recommendation` | Dismiss a pending recommendation |
|
|
323
|
+
|
|
324
|
+
Applying never publishes or spends by itself — drafts need scheduling and boosts need explicit activation.
|
|
325
|
+
|
|
313
326
|
## API Key & Scopes
|
|
314
327
|
|
|
315
328
|
Generate an API key at [www.dropspace.dev/settings](https://www.dropspace.dev/settings) under the API section.
|
package/dist/cli-utils.d.ts
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI + MCP output helpers.
|
|
3
|
+
*
|
|
4
|
+
* The CLI consumes these for terminal output (table / JSON / structured exit
|
|
5
|
+
* codes). MCP tools consume `formatMcpError` so error responses surface the
|
|
6
|
+
* structured `{success, error: {code, message}}` envelope to the LLM instead
|
|
7
|
+
* of a flattened message string — lets the model branch on `error.code`
|
|
8
|
+
* without regex-extracting the bracketed prefix.
|
|
9
|
+
*/
|
|
1
10
|
export declare function formatOutput(data: unknown, format?: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Map a fleet error-code prefix to a CLI exit code.
|
|
13
|
+
*
|
|
14
|
+
* Convention so shell scripts can branch on failure type without parsing stderr:
|
|
15
|
+
* - 0: success (not handled here)
|
|
16
|
+
* - 1: generic / unknown
|
|
17
|
+
* - 2: input / validation (SERVER_VALIDATION_FAILED, SERVER_BAD_REQUEST)
|
|
18
|
+
* - 3: rate limit
|
|
19
|
+
* - 4: auth (AUTH_*, APIKEY_*)
|
|
20
|
+
* - 5: payment / billing (PAYMENT_*, BILLING_*)
|
|
21
|
+
* - 6: not found (any *_NOT_FOUND)
|
|
22
|
+
*
|
|
23
|
+
* Domain-specific codes (e.g. CAMP_001, LAUNCH_001) are categorized by their
|
|
24
|
+
* suffix shape — *_NOT_FOUND maps to 6, others fall through to 1.
|
|
25
|
+
*/
|
|
26
|
+
export declare function exitCodeForMessage(message: string): number;
|
|
2
27
|
export declare function handleError(error: unknown): never;
|
|
28
|
+
/**
|
|
29
|
+
* Parse the `[CODE] message (field: x)` string format that the API client's
|
|
30
|
+
* handleError builds, recovering the structured fields.
|
|
31
|
+
*
|
|
32
|
+
* Returns `{code, message, field?}`. Falls back to `{code: "UNKNOWN_ERROR", message}`
|
|
33
|
+
* when no bracketed code is present (shouldn't happen for API errors, but safe
|
|
34
|
+
* for client-side throws like network timeouts).
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseErrorMessage(raw: string): {
|
|
37
|
+
code: string;
|
|
38
|
+
message: string;
|
|
39
|
+
field?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Format an error caught in an MCP tool handler as the structured response
|
|
43
|
+
* the MCP SDK returns to the model.
|
|
44
|
+
*
|
|
45
|
+
* Returns the canonical v1 error envelope `{success: false, error: {code, message, field?}}`
|
|
46
|
+
* as JSON text inside the MCP `content` block, plus `isError: true` so the SDK
|
|
47
|
+
* surfaces it as an error to the model. Lets the LLM read `error.code`
|
|
48
|
+
* structurally instead of regexing it out of a flattened message.
|
|
49
|
+
*/
|
|
50
|
+
export declare function formatMcpError(error: unknown): {
|
|
51
|
+
content: Array<{
|
|
52
|
+
type: "text";
|
|
53
|
+
text: string;
|
|
54
|
+
}>;
|
|
55
|
+
isError: true;
|
|
56
|
+
};
|
|
3
57
|
export declare function getFormat(opts: Record<string, unknown>): string;
|
|
4
58
|
export declare function parseList(value: string): string[];
|
package/dist/cli-utils.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI + MCP output helpers.
|
|
3
|
+
*
|
|
4
|
+
* The CLI consumes these for terminal output (table / JSON / structured exit
|
|
5
|
+
* codes). MCP tools consume `formatMcpError` so error responses surface the
|
|
6
|
+
* structured `{success, error: {code, message}}` envelope to the LLM instead
|
|
7
|
+
* of a flattened message string — lets the model branch on `error.code`
|
|
8
|
+
* without regex-extracting the bracketed prefix.
|
|
9
|
+
*/
|
|
1
10
|
export function formatOutput(data, format = "json") {
|
|
2
11
|
if (format === "table" && typeof data === "object" && data !== null) {
|
|
3
12
|
const obj = data;
|
|
@@ -16,10 +25,94 @@ export function formatOutput(data, format = "json") {
|
|
|
16
25
|
}
|
|
17
26
|
console.log(JSON.stringify(data, null, 2));
|
|
18
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Map a fleet error-code prefix to a CLI exit code.
|
|
30
|
+
*
|
|
31
|
+
* Convention so shell scripts can branch on failure type without parsing stderr:
|
|
32
|
+
* - 0: success (not handled here)
|
|
33
|
+
* - 1: generic / unknown
|
|
34
|
+
* - 2: input / validation (SERVER_VALIDATION_FAILED, SERVER_BAD_REQUEST)
|
|
35
|
+
* - 3: rate limit
|
|
36
|
+
* - 4: auth (AUTH_*, APIKEY_*)
|
|
37
|
+
* - 5: payment / billing (PAYMENT_*, BILLING_*)
|
|
38
|
+
* - 6: not found (any *_NOT_FOUND)
|
|
39
|
+
*
|
|
40
|
+
* Domain-specific codes (e.g. CAMP_001, LAUNCH_001) are categorized by their
|
|
41
|
+
* suffix shape — *_NOT_FOUND maps to 6, others fall through to 1.
|
|
42
|
+
*/
|
|
43
|
+
export function exitCodeForMessage(message) {
|
|
44
|
+
const codeMatch = message.match(/\[([A-Z]+)_(\d+|[A-Z_]+)\]/);
|
|
45
|
+
if (!codeMatch)
|
|
46
|
+
return 1;
|
|
47
|
+
const prefix = codeMatch[1];
|
|
48
|
+
const suffix = codeMatch[2];
|
|
49
|
+
// Suffix-based: *_NOT_FOUND → 6 regardless of prefix
|
|
50
|
+
if (suffix.includes("NOT_FOUND"))
|
|
51
|
+
return 6;
|
|
52
|
+
switch (prefix) {
|
|
53
|
+
case "AUTH":
|
|
54
|
+
case "APIKEY":
|
|
55
|
+
return 4;
|
|
56
|
+
case "RATE":
|
|
57
|
+
return 3;
|
|
58
|
+
case "PAYMENT":
|
|
59
|
+
case "BILLING":
|
|
60
|
+
return 5;
|
|
61
|
+
case "SERVER":
|
|
62
|
+
// SERVER_002/005 = validation; SERVER_003 = not found; rest = generic
|
|
63
|
+
if (suffix === "002" || suffix === "005")
|
|
64
|
+
return 2;
|
|
65
|
+
if (suffix === "003")
|
|
66
|
+
return 6;
|
|
67
|
+
return 1;
|
|
68
|
+
default:
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
19
72
|
export function handleError(error) {
|
|
20
73
|
const message = error instanceof Error ? error.message : String(error);
|
|
21
74
|
console.error(`error: ${message}`);
|
|
22
|
-
process.exit(
|
|
75
|
+
process.exit(exitCodeForMessage(message));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse the `[CODE] message (field: x)` string format that the API client's
|
|
79
|
+
* handleError builds, recovering the structured fields.
|
|
80
|
+
*
|
|
81
|
+
* Returns `{code, message, field?}`. Falls back to `{code: "UNKNOWN_ERROR", message}`
|
|
82
|
+
* when no bracketed code is present (shouldn't happen for API errors, but safe
|
|
83
|
+
* for client-side throws like network timeouts).
|
|
84
|
+
*/
|
|
85
|
+
export function parseErrorMessage(raw) {
|
|
86
|
+
// Format: "[CODE] message (field: x)" — code optional, field optional
|
|
87
|
+
const match = raw.match(/^\[([A-Z_0-9]+)\]\s+(.*?)(?:\s+\(field:\s+([^)]+)\))?$/);
|
|
88
|
+
if (!match)
|
|
89
|
+
return { code: "UNKNOWN_ERROR", message: raw };
|
|
90
|
+
return {
|
|
91
|
+
code: match[1],
|
|
92
|
+
message: match[2],
|
|
93
|
+
...(match[3] && { field: match[3] }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Format an error caught in an MCP tool handler as the structured response
|
|
98
|
+
* the MCP SDK returns to the model.
|
|
99
|
+
*
|
|
100
|
+
* Returns the canonical v1 error envelope `{success: false, error: {code, message, field?}}`
|
|
101
|
+
* as JSON text inside the MCP `content` block, plus `isError: true` so the SDK
|
|
102
|
+
* surfaces it as an error to the model. Lets the LLM read `error.code`
|
|
103
|
+
* structurally instead of regexing it out of a flattened message.
|
|
104
|
+
*/
|
|
105
|
+
export function formatMcpError(error) {
|
|
106
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
107
|
+
const parsed = parseErrorMessage(raw);
|
|
108
|
+
const envelope = {
|
|
109
|
+
success: false,
|
|
110
|
+
error: parsed,
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
23
116
|
}
|
|
24
117
|
export function getFormat(opts) {
|
|
25
118
|
if (opts.json)
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerCreditCommands } from "./commands/credits.js";
|
|
|
15
15
|
import { registerMediaCommands } from "./commands/media.js";
|
|
16
16
|
import { registerBoostCommands } from "./commands/boosts.js";
|
|
17
17
|
import { registerAutopilotCommands } from "./commands/autopilot.js";
|
|
18
|
+
import { registerRecommendationCommands } from "./commands/recommendations.js";
|
|
18
19
|
const program = new Command()
|
|
19
20
|
.name("dropspace")
|
|
20
21
|
.description("dropspace CLI - manage launches, personas, boosts, and more")
|
|
@@ -34,6 +35,7 @@ try {
|
|
|
34
35
|
registerMediaCommands(program, client);
|
|
35
36
|
registerBoostCommands(program, client);
|
|
36
37
|
registerAutopilotCommands(program, client);
|
|
38
|
+
registerRecommendationCommands(program, client);
|
|
37
39
|
await program.parseAsync(process.argv);
|
|
38
40
|
}
|
|
39
41
|
catch (error) {
|
|
@@ -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>",
|
|
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>",
|
|
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,60 @@
|
|
|
1
|
+
import { formatOutput, handleError, getFormat } from "../cli-utils.js";
|
|
2
|
+
export function registerRecommendationCommands(program, client) {
|
|
3
|
+
const recommendations = program
|
|
4
|
+
.command("recommendations")
|
|
5
|
+
.alias("recs")
|
|
6
|
+
.description("actionable insights from your organic post performance");
|
|
7
|
+
recommendations
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("list recommendations")
|
|
10
|
+
.option("--status <status>", "filter: pending, applied, dismissed, expired")
|
|
11
|
+
.option("--launch <id>", "only recommendations for this launch")
|
|
12
|
+
.option("-p, --page <n>", "page number", "1")
|
|
13
|
+
.option("-s, --page-size <n>", "items per page (max 100)", "20")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
try {
|
|
16
|
+
const params = {};
|
|
17
|
+
if (opts.status)
|
|
18
|
+
params.status = opts.status;
|
|
19
|
+
if (opts.launch)
|
|
20
|
+
params.launch_id = opts.launch;
|
|
21
|
+
if (opts.page)
|
|
22
|
+
params.page = opts.page;
|
|
23
|
+
if (opts.pageSize)
|
|
24
|
+
params.page_size = opts.pageSize;
|
|
25
|
+
const result = await client.get("/recommendations", params);
|
|
26
|
+
formatOutput(result, getFormat(program.opts()));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
handleError(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
recommendations
|
|
33
|
+
.command("apply <id>")
|
|
34
|
+
.description("apply a recommendation (repost/cross-post create a DRAFT launch; boost creates a PAUSED boost; promote queues into autopilot; pause adds a directive)")
|
|
35
|
+
.option("--ad-account <id>", "meta ad account id (required for boost_winner)")
|
|
36
|
+
.action(async (id, opts) => {
|
|
37
|
+
try {
|
|
38
|
+
const body = {};
|
|
39
|
+
if (opts.adAccount)
|
|
40
|
+
body.ad_account_id = opts.adAccount;
|
|
41
|
+
const result = await client.post(`/recommendations/${id}/apply`, body);
|
|
42
|
+
formatOutput(result, getFormat(program.opts()));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
handleError(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
recommendations
|
|
49
|
+
.command("dismiss <id>")
|
|
50
|
+
.description("dismiss a recommendation")
|
|
51
|
+
.action(async (id) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await client.post(`/recommendations/${id}/dismiss`);
|
|
54
|
+
formatOutput(result, getFormat(program.opts()));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
handleError(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,9 +13,10 @@ import { registerCreditTools } from "./tools/credits.js";
|
|
|
13
13
|
import { registerMediaTools } from "./tools/media.js";
|
|
14
14
|
import { registerBoostTools } from "./tools/boosts.js";
|
|
15
15
|
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
16
|
+
import { registerRecommendationTools } from "./tools/recommendations.js";
|
|
16
17
|
const server = new McpServer({
|
|
17
18
|
name: "dropspace",
|
|
18
|
-
version: "2.
|
|
19
|
+
version: "2.9.0",
|
|
19
20
|
});
|
|
20
21
|
const client = new DropspaceClient();
|
|
21
22
|
registerLaunchTools(server, client);
|
|
@@ -29,5 +30,6 @@ registerCreditTools(server, client);
|
|
|
29
30
|
registerMediaTools(server, client);
|
|
30
31
|
registerBoostTools(server, client);
|
|
31
32
|
registerAutopilotTools(server, client);
|
|
33
|
+
registerRecommendationTools(server, client);
|
|
32
34
|
const transport = new StdioServerTransport();
|
|
33
35
|
await server.connect(transport);
|
package/dist/tools/launches.js
CHANGED
|
@@ -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,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function ok(result) {
|
|
3
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
4
|
+
}
|
|
5
|
+
function err(error) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: `error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
8
|
+
isError: true,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function registerRecommendationTools(server, client) {
|
|
12
|
+
server.tool("list_recommendations", "list actionable recommendations computed from your organic post performance: winners to repost, cross-post, or boost, autopilot promote signals, and underperformer pause signals. dropspace sweeps post metrics every 30 minutes and compares each post to your own per-platform baseline.", {
|
|
13
|
+
status: z
|
|
14
|
+
.enum(["pending", "applied", "dismissed", "expired"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("filter by status (default: all)"),
|
|
17
|
+
launch_id: z.string().optional().describe("only recommendations for this launch"),
|
|
18
|
+
page: z.number().int().positive().optional().describe("page number (default: 1)"),
|
|
19
|
+
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default: 20, max: 100)"),
|
|
20
|
+
}, async ({ status, launch_id, page, page_size }) => {
|
|
21
|
+
try {
|
|
22
|
+
const params = {};
|
|
23
|
+
if (status !== undefined)
|
|
24
|
+
params.status = status;
|
|
25
|
+
if (launch_id !== undefined)
|
|
26
|
+
params.launch_id = launch_id;
|
|
27
|
+
if (page !== undefined)
|
|
28
|
+
params.page = String(page);
|
|
29
|
+
if (page_size !== undefined)
|
|
30
|
+
params.page_size = String(page_size);
|
|
31
|
+
return ok(await client.get("/recommendations", params));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return err(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
server.tool("apply_recommendation", "apply a recommendation's one-click action. repost_winner and cross_post create a DRAFT launch seeded with the winning content (nothing publishes until scheduled); boost_winner creates a PAUSED meta boost and requires ad_account_id (activation, and therefore spend, is a separate step); promote_winner queues the winner into autopilot; pause_underperformer adds a standing directive to the autopilot config.", {
|
|
38
|
+
recommendation_id: z.string().describe("the recommendation to apply"),
|
|
39
|
+
ad_account_id: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("meta ad account id — required for boost_winner recommendations, ignored otherwise"),
|
|
43
|
+
}, async ({ recommendation_id, ad_account_id }) => {
|
|
44
|
+
try {
|
|
45
|
+
return ok(await client.post(`/recommendations/${recommendation_id}/apply`, ad_account_id ? { ad_account_id } : {}));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return err(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.tool("dismiss_recommendation", "dismiss a pending recommendation so it stops showing everywhere.", {
|
|
52
|
+
recommendation_id: z.string().describe("the recommendation to dismiss"),
|
|
53
|
+
}, async ({ recommendation_id }) => {
|
|
54
|
+
try {
|
|
55
|
+
return ok(await client.post(`/recommendations/${recommendation_id}/dismiss`));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return err(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -392,3 +392,34 @@ export interface AutopilotRun {
|
|
|
392
392
|
started_at: string;
|
|
393
393
|
completed_at: string | null;
|
|
394
394
|
}
|
|
395
|
+
/** Actionable insight computed from organic post performance */
|
|
396
|
+
export interface Recommendation {
|
|
397
|
+
id: string;
|
|
398
|
+
rec_type: "repost_winner" | "cross_post" | "boost_winner" | "promote_winner" | "pause_underperformer";
|
|
399
|
+
status: "pending" | "applied" | "dismissed" | "expired";
|
|
400
|
+
platform: string;
|
|
401
|
+
launch_id: string | null;
|
|
402
|
+
posting_log_id: string | null;
|
|
403
|
+
payload: unknown;
|
|
404
|
+
score: number | null;
|
|
405
|
+
baseline: number | null;
|
|
406
|
+
expires_at: string | null;
|
|
407
|
+
applied_launch_id: string | null;
|
|
408
|
+
applied_at: string | null;
|
|
409
|
+
dismissed_at: string | null;
|
|
410
|
+
created_at: string;
|
|
411
|
+
updated_at: string;
|
|
412
|
+
}
|
|
413
|
+
/** What applying a recommendation actually did */
|
|
414
|
+
export interface RecommendationApplyOutcome {
|
|
415
|
+
kind: "launch_draft" | "boost_created" | "autopilot_queued" | "directive_added";
|
|
416
|
+
launch_id?: string;
|
|
417
|
+
boost_id?: string;
|
|
418
|
+
boost_status?: string;
|
|
419
|
+
dedupe_key?: string;
|
|
420
|
+
directive?: string;
|
|
421
|
+
}
|
|
422
|
+
export interface RecommendationApplyResponse {
|
|
423
|
+
recommendation: Recommendation;
|
|
424
|
+
outcome: RecommendationApplyOutcome;
|
|
425
|
+
}
|