@jclvsh/dropspace 2.35.0 → 2.42.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 +14 -4
- package/dist/cli.js +0 -0
- package/dist/commands/audience.js +92 -0
- package/dist/commands/launches.js +2 -2
- package/dist/index.js +0 -0
- package/dist/tool-policy.d.ts +1 -1
- package/dist/tool-policy.js +10 -0
- package/dist/tools/audience.js +113 -3
- package/dist/tools/comments.js +27 -0
- package/dist/tools/launches.js +2 -2
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/skills/dropspace/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ Other agents that read skill files can use the same file from `node_modules/@jcl
|
|
|
173
173
|
| `dropspace_platforms` | string[] | No | Platforms to post via official dropspace accounts |
|
|
174
174
|
| `media_mode` | string | No | `"images"` or `"video"` |
|
|
175
175
|
| `user_platform_accounts` | object | No | Map of platform key to token_id (UUID). Simple keys: `twitter`, `reddit`, `instagram`, `tiktok`, `youtube`. LinkedIn: `linkedin:personal` or `linkedin:organization:<org_id>`. Facebook: a bare `facebook` key works when you have exactly one Page (send any connection id from `connections list` as the value); to target a specific Page among several, use `facebook:page:<page_id>` or `facebook:<page_id>`, where `page_id` is the entity_id of that Page from `connections list`; several Pages with no `page_id` returns a 400 listing them |
|
|
176
|
-
| `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). Instagram accepts `instagram_settings.post_type`: `"feed"` (default), `"story"`,
|
|
176
|
+
| `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). Instagram accepts `instagram_settings.post_type`: `"feed"` (default), `"story"`, `"carousel"` (every attached image and the video as one carousel, 2 to 10 items), or `"trial_reel"` (the video as a reel shown to non-followers first; `instagram_settings.graduation_strategy` `"MANUAL"` (default) or `"SS_PERFORMANCE"` says how it reaches followers) |
|
|
177
177
|
| `custom_content` | string \| string[] | No | Single content for all platforms, or array of tweets for twitter thread mode (mutually exclusive with `platform_contents`) |
|
|
178
178
|
| `custom_content_reddit_title` | string | No | Reddit title (required with `custom_content` + reddit) |
|
|
179
179
|
| `media` | object[] | No | Inline media, URL or base64 (1-10 items, mutually exclusive with `media_assets`) |
|
|
@@ -188,7 +188,7 @@ Other agents that read skill files can use the same file from `node_modules/@jcl
|
|
|
188
188
|
| `id` | string | Yes | Launch ID |
|
|
189
189
|
| `scheduled_date` | string \| null | No | ISO 8601 datetime or null to clear |
|
|
190
190
|
| `status` | string | No | `"draft"`, `"manual"`, `"trigger"`, `"scheduled"`, `"cancelled"` |
|
|
191
|
-
| `platform_contents` | object | No | Per-platform content overrides (Instagram: `instagram_settings.post_type` `"feed"`, `"story"`, or `"
|
|
191
|
+
| `platform_contents` | object | No | Per-platform content overrides (Instagram: `instagram_settings.post_type` `"feed"`, `"story"`, `"carousel"`, or `"trial_reel"`, plus `graduation_strategy` for a trial; a trial reel needs a professional account with 200 or more followers, or the update is refused with the reason) |
|
|
192
192
|
| `dropspace_platforms` | string[] | No | Official dropspace account platforms |
|
|
193
193
|
| `user_platform_accounts` | object | No | Map of platform key to token_id (UUID). Simple keys: `twitter`, `reddit`, `instagram`, `tiktok`, `youtube`. LinkedIn: `linkedin:personal` or `linkedin:organization:<org_id>`. Facebook: a bare `facebook` key works when you have exactly one Page (send any connection id from `connections list` as the value); to target a specific Page among several, use `facebook:page:<page_id>` or `facebook:<page_id>`, where `page_id` is the entity_id of that Page from `connections list`; several Pages with no `page_id` returns a 400 listing them |
|
|
194
194
|
| `media` | object[] | No | Inline media (URL or base64, 1-10 items) |
|
|
@@ -269,8 +269,10 @@ Other agents that read skill files can use the same file from `node_modules/@jcl
|
|
|
269
269
|
| `reply_to_comment` | Reply as the publishing account: youtube and instagram (send scope, stdio only) |
|
|
270
270
|
| `hide_comment` | Hide or unhide a comment: instagram (send scope, stdio only) |
|
|
271
271
|
| `delete_comment` | Permanently delete a comment: instagram, media owner only (send scope, stdio only) |
|
|
272
|
+
| `list_comment_queue` | Every comment still waiting for you across your recent instagram and youtube posts, with claude's suggested action (stdio only) |
|
|
273
|
+
| `act_on_comments` | Hide (instagram, send scope) or dismiss up to 50 queue comments (stdio only) |
|
|
272
274
|
|
|
273
|
-
CLI: `dropspace launches comments list|reply|hide|delete
|
|
275
|
+
CLI: `dropspace launches comments list|reply|hide|delete` and `dropspace comment-queue list|hide|dismiss <platform:post_id:comment_id...>`.
|
|
274
276
|
|
|
275
277
|
### Reddit
|
|
276
278
|
|
|
@@ -370,14 +372,22 @@ Reddit engagement (drafted comments in other communities) is paused: its tools a
|
|
|
370
372
|
| Tool | Description |
|
|
371
373
|
| -------------- | ------------------------------------------------------------------------------------------------------ |
|
|
372
374
|
| `get_audience` | Follower counts and growth per connected account (Instagram, YouTube, TikTok, Twitter, Facebook Pages) |
|
|
375
|
+
| `get_trial_reel_analytics` | Instagram trial reel results: totals, one entry per reel, and net followers since the first trial reel (local package only, not the hosted endpoint) |
|
|
376
|
+
| `get_best_posting_hours` | When your Instagram followers are online (last 30 days): per hour, per weekday and hour, and the busiest hours (local package only, not the hosted endpoint) |
|
|
377
|
+
| `get_audience_demographics` | Age, gender, country, and city of your Instagram followers and engaged audience (local package only, not the hosted endpoint) |
|
|
378
|
+
| `get_reels_read` | Each Instagram account's latest reels read (local package only, not the hosted endpoint) |
|
|
379
|
+
| `generate_reels_read` | Claude compares your top, middle, and bottom reels of the last 90 days; pro and premium, one a day per account (local package only, not the hosted endpoint) |
|
|
373
380
|
|
|
374
381
|
#### `get_audience` Parameters
|
|
375
382
|
|
|
376
383
|
| Parameter | Type | Required | Description |
|
|
377
384
|
| ------------- | ------ | -------- | ----------------------------------------- |
|
|
378
385
|
| `window_days` | number | No | Growth window in days (1-365, default 30) |
|
|
386
|
+
| `include_instagram_insights` | boolean | No | Add Instagram growth analytics to each Instagram entry (default false) |
|
|
379
387
|
|
|
380
|
-
CLI: `dropspace audience [-w, --window-days <n>]`.
|
|
388
|
+
CLI: `dropspace audience [-w, --window-days <n>] [--instagram-insights]`, `dropspace trial-reels`, `dropspace best-hours [--timezone <zone>]`, `dropspace demographics [--timeframe <this_week|this_month|last_90_days>]`, and `dropspace reels-read [--generate] [--connection-id <id>]`.
|
|
389
|
+
|
|
390
|
+
With `include_instagram_insights`, each Instagram entry carries `instagram_insights`: follows, unfollows, net, and reach split into followers and non-followers per Pacific-time day, an estimated followers-over-time series rebuilt back from the current count, weekly growth velocity, the share of reach from non-followers, and contact-button taps. Instagram revises the last 7 days (`settled: false`). Each call reads up to 31 days, newest first, so a 90-day window fills over a few calls (`days_pending`). Instagram reports none of this for accounts under 100 followers (`available: false`).
|
|
381
391
|
|
|
382
392
|
Counts come from each platform's official API and are cached for 15 minutes. YouTube works differently: each read spends a shared daily quota, so a connected account's YouTube count is refreshed in the background about once an hour, and an on-demand read re-fetches at most every 6 hours if that background refresh is delayed; if the daily quota is exhausted, a YouTube read returns the most recent successful count instead of null. `current.captured_at` is always when that count was actually read from the platform, not when it was served from cache - use it to judge freshness, not the response time. YouTube history points are kept for 30 days (YouTube's API storage limit), so YouTube growth reaches back about 30 days even for a longer `window_days`; other platforms keep their full history. LinkedIn is not covered (its API exposes no personal-profile follower count).
|
|
383
393
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -4,11 +4,14 @@ export function registerAudienceCommands(program, client) {
|
|
|
4
4
|
.command("audience")
|
|
5
5
|
.description("follower counts + growth per connected account (instagram, youtube, tiktok, twitter, facebook pages); counts cache for 15 min, youtube refreshes about hourly in the background, and captured_at is always the actual read time, not the serve time")
|
|
6
6
|
.option("-w, --window-days <n>", "growth window in days (max 365)", "30")
|
|
7
|
+
.option("--instagram-insights", "add instagram growth analytics: follows/unfollows per day, followers over time, velocity, reach split, contact taps")
|
|
7
8
|
.action(async (opts) => {
|
|
8
9
|
try {
|
|
9
10
|
const params = {};
|
|
10
11
|
if (opts.windowDays)
|
|
11
12
|
params.window_days = opts.windowDays;
|
|
13
|
+
if (opts.instagramInsights)
|
|
14
|
+
params.include = "instagram_insights";
|
|
12
15
|
const result = await client.get("/audience", params);
|
|
13
16
|
formatOutput(result, getFormat(program.opts()));
|
|
14
17
|
}
|
|
@@ -16,4 +19,93 @@ export function registerAudienceCommands(program, client) {
|
|
|
16
19
|
handleError(error);
|
|
17
20
|
}
|
|
18
21
|
});
|
|
22
|
+
program
|
|
23
|
+
.command("trial-reels")
|
|
24
|
+
.description("instagram trial reel results: totals, one entry per reel, and net followers since the first trial reel (stored numbers, no instagram request)")
|
|
25
|
+
.action(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const result = await client.get("/analytics/trial-reels");
|
|
28
|
+
formatOutput(result, getFormat(program.opts()));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
handleError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
program
|
|
35
|
+
.command("best-hours")
|
|
36
|
+
.description("when your instagram followers are online (last 30 days): average per hour and per weekday-hour, and the busiest hours at least two hours apart; accounts with 100+ followers")
|
|
37
|
+
.option("--timezone <zone>", "IANA timezone for the hours (default: autopilot timezone, else UTC)")
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.get("/audience/best-hours", opts.timezone ? { timezone: opts.timezone } : undefined);
|
|
41
|
+
formatOutput(result, getFormat(program.opts()));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
handleError(error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command("demographics")
|
|
49
|
+
.description("who your instagram audience is: age, gender, country, and city of followers and of accounts that engaged in a timeframe; accounts with 100+ followers")
|
|
50
|
+
.option("--timeframe <period>", "engaged-audience period: this_week, this_month (default), last_90_days")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await client.get("/audience/demographics", opts.timeframe ? { timeframe: opts.timeframe } : undefined);
|
|
54
|
+
formatOutput(result, getFormat(program.opts()));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
handleError(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command("reels-read")
|
|
62
|
+
.description("claude's read of your recent instagram reels (top, middle, and bottom by views over the last 90 days); shows the latest stored read, or makes a new one with --generate (pro and premium, one a day per account, 30-60 seconds)")
|
|
63
|
+
.option("--generate", "make a new read instead of showing the stored ones")
|
|
64
|
+
.option("--connection-id <id>", "instagram connection to read with --generate (default: the first instagram account)")
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
try {
|
|
67
|
+
const result = opts.generate
|
|
68
|
+
? await client.post("/analytics/reels-read", opts.connectionId ? { connection_id: opts.connectionId } : {})
|
|
69
|
+
: await client.get("/analytics/reels-read");
|
|
70
|
+
formatOutput(result, getFormat(program.opts()));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
handleError(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const queue = program
|
|
77
|
+
.command("comment-queue")
|
|
78
|
+
.description("comments still waiting for you across your recent instagram and youtube posts");
|
|
79
|
+
queue
|
|
80
|
+
.command("list")
|
|
81
|
+
.description("waiting comments, newest first, with claude's suggested action and reply draft")
|
|
82
|
+
.action(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const result = await client.get("/comments/queue");
|
|
85
|
+
formatOutput(result, getFormat(program.opts()));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
handleError(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
for (const action of ["hide", "dismiss"]) {
|
|
92
|
+
queue
|
|
93
|
+
.command(`${action} <refs...>`)
|
|
94
|
+
.description(action === "hide"
|
|
95
|
+
? "hide comments on instagram (send scope); refs are platform:post_id:comment_id"
|
|
96
|
+
: "dismiss comments from the queue (nothing changes on the platform); refs are platform:post_id:comment_id")
|
|
97
|
+
.action(async (refs) => {
|
|
98
|
+
try {
|
|
99
|
+
const comments = refs.map((ref) => {
|
|
100
|
+
const [platform, post_id, ...rest] = ref.split(":");
|
|
101
|
+
return { platform, post_id, comment_id: rest.join(":") };
|
|
102
|
+
});
|
|
103
|
+
const result = await client.post("/comments/actions", { action, comments });
|
|
104
|
+
formatOutput(result, getFormat(program.opts()));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
handleError(error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
19
111
|
}
|
|
@@ -34,7 +34,7 @@ export function registerLaunchCommands(program, client) {
|
|
|
34
34
|
.option("--persona <id>", "persona ID for tone/style")
|
|
35
35
|
.option("--dropspace-platforms <list>", "platforms for official dropspace accounts", parseList)
|
|
36
36
|
.option("--media-mode <mode>", "media mode: images or video")
|
|
37
|
-
.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). instagram supports instagram_settings.post_type: feed, story,
|
|
37
|
+
.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). instagram supports instagram_settings.post_type: feed, story, carousel (every attached image and the video as one carousel, 2 to 10 items), or trial_reel (the video as a reel shown to non-followers first; needs a professional account with 200 or more followers; instagram_settings.graduation_strategy MANUAL (default) or SS_PERFORMANCE says how it reaches followers). e.g. \'{"reddit":{"title":"...","content":"...","metadata":{"subreddits":["SideProject"],"first_comment":"..."}}}\'')
|
|
38
38
|
.option("--custom-content <text>", "single content string for all platforms")
|
|
39
39
|
.option("--reddit-title <title>", "reddit title (with custom-content)")
|
|
40
40
|
.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. facebook: bare "facebook" for your one Page, or "facebook:page:<page_id>" / "facebook:<page_id>" for a specific one (id from `connections list`); several Pages and no page_id returns a 400. linkedin: "linkedin:personal" or "linkedin:organization:<org_id>"')
|
|
@@ -108,7 +108,7 @@ export function registerLaunchCommands(program, client) {
|
|
|
108
108
|
.option("--schedule <datetime>", "ISO 8601 schedule datetime (empty to clear)")
|
|
109
109
|
.option("--status <status>", "status: draft, manual, trigger, scheduled, cancelled")
|
|
110
110
|
.option("--persona <id>", "persona ID (empty to clear)")
|
|
111
|
-
.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). instagram supports instagram_settings.post_type: feed, story,
|
|
111
|
+
.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). instagram supports instagram_settings.post_type: feed, story, carousel (every attached image and the video as one carousel, 2 to 10 items), or trial_reel (the video as a reel shown to non-followers first; needs a professional account with 200 or more followers; instagram_settings.graduation_strategy MANUAL (default) or SS_PERFORMANCE says how it reaches followers). e.g. \'{"reddit":{"title":"...","content":"...","metadata":{"subreddits":["SideProject"],"first_comment":"..."}}}\'')
|
|
112
112
|
.option("--dropspace-platforms <list>", "official dropspace account platforms", parseList)
|
|
113
113
|
.option("--attribution-mode <mode>", 'how official-account posts credit you: "handle" (default) or "anonymous"')
|
|
114
114
|
.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. facebook: bare "facebook" for your one Page, or "facebook:page:<page_id>" / "facebook:<page_id>" for a specific one (id from `connections list`); several Pages and no page_id returns a 400. linkedin: "linkedin:personal" or "linkedin:organization:<org_id>"')
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/tool-policy.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare const REDDIT_ENGAGEMENT_PAUSED = true;
|
|
|
29
29
|
export declare const PAUSED_TOOLS: ReadonlySet<string>;
|
|
30
30
|
/** The tools the package serves - every docs count and category list reads this. */
|
|
31
31
|
export declare const TOOL_POLICY: PolicyTable<ToolCategory>;
|
|
32
|
-
export declare const toolPolicy: (name: string) => ToolPolicy<"audience" | "
|
|
32
|
+
export declare const toolPolicy: (name: string) => ToolPolicy<"audience" | "comments" | "launches" | "personas" | "connections" | "webhooks" | "usage" | "credits" | "media" | "boosts" | "posts" | "API keys" | "dropspace" | "account" | "autopilot" | "recommendations" | "reddit engagement">;
|
|
33
33
|
export declare const isToolEnabled: (name: string, profile: ToolProfile) => boolean;
|
|
34
34
|
export declare const toolAnnotations: (name: string) => {
|
|
35
35
|
readOnlyHint: boolean;
|
package/dist/tool-policy.js
CHANGED
|
@@ -74,6 +74,9 @@ const FULL_TOOL_POLICY = {
|
|
|
74
74
|
hide_comment: localOnly("comments", "sensitive"),
|
|
75
75
|
// deletes on the platform with no restore
|
|
76
76
|
delete_comment: localOnly("comments", "sensitive"),
|
|
77
|
+
list_comment_queue: localOnly("comments", "read"),
|
|
78
|
+
// hide changes the post on Instagram; dismiss only leaves the queue
|
|
79
|
+
act_on_comments: localOnly("comments", "sensitive"),
|
|
77
80
|
// personas
|
|
78
81
|
list_personas: hosted("personas", "read"),
|
|
79
82
|
get_persona: hosted("personas", "read"),
|
|
@@ -118,6 +121,13 @@ const FULL_TOOL_POLICY = {
|
|
|
118
121
|
dismiss_recommendation: hosted("recommendations", "write"),
|
|
119
122
|
// audience, usage, status
|
|
120
123
|
get_audience: hosted("audience", "read"),
|
|
124
|
+
// Local-only while the hosted set submitted to Meta on 10/13/2026 is in review (server.test.ts pins it).
|
|
125
|
+
get_trial_reel_analytics: localOnly("audience", "read"),
|
|
126
|
+
get_best_posting_hours: localOnly("audience", "read"),
|
|
127
|
+
get_audience_demographics: localOnly("audience", "read"),
|
|
128
|
+
get_reels_read: localOnly("audience", "read"),
|
|
129
|
+
// spends a Claude call and up to 60 Instagram requests; changes nothing the user posted
|
|
130
|
+
generate_reels_read: localOnly("audience", "write"),
|
|
121
131
|
get_usage: hosted("usage", "read"),
|
|
122
132
|
get_changelog: hosted("usage", "read"),
|
|
123
133
|
get_dropspace_status: hosted("dropspace", "read"),
|
package/dist/tools/audience.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
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 come from each platform\'s official API and are cached for 15 minutes. youtube works differently: each youtube read spends a shared daily quota, so while you keep reading a creator\'s audience, their youtube count is refreshed in the background about once an hour, and an on-demand read re-fetches at most every 6 hours if that refresh is delayed; if the daily quota is exhausted, a youtube read returns the most recent successful count instead of null. "current.captured_at" is always when that count was actually read from the platform, never when it was served: use it to judge freshness. "series" is the stored history; youtube history is kept for 30 days (youtube\'s API storage limit), so youtube growth reaches back about 30 days whatever window_days says, while other platforms keep full 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).', {
|
|
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 come from each platform\'s official API and are cached for 15 minutes. youtube works differently: each youtube read spends a shared daily quota, so while you keep reading a creator\'s audience, their youtube count is refreshed in the background about once an hour, and an on-demand read re-fetches at most every 6 hours if that refresh is delayed; if the daily quota is exhausted, a youtube read returns the most recent successful count instead of null. "current.captured_at" is always when that count was actually read from the platform, never when it was served: use it to judge freshness. "series" is the stored history; youtube history is kept for 30 days (youtube\'s API storage limit), so youtube growth reaches back about 30 days whatever window_days says, while other platforms keep full 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). set include_instagram_insights to add instagram growth analytics to each instagram entry: follows, unfollows, net, and reach split into followers and non-followers per pacific-time day (the last 7 days are settled: false because instagram still revises them), followers_over_time (an estimate rebuilt back from the current count), weekly velocity with a trend, non_follower_reach_share, and contact-button taps. it reads up to 31 days per call, newest first, so a longer window fills over a few calls (days_pending says how many are left); instagram reports none of it for accounts under 100 followers (available: false).', {
|
|
4
4
|
window_days: z
|
|
5
5
|
.number()
|
|
6
6
|
.int()
|
|
@@ -8,9 +8,18 @@ export function registerAudienceTools(server, client) {
|
|
|
8
8
|
.max(365)
|
|
9
9
|
.optional()
|
|
10
10
|
.describe("growth window in days (default: 30, max: 365)"),
|
|
11
|
-
|
|
11
|
+
include_instagram_insights: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("add instagram growth analytics to each instagram entry (default: false)"),
|
|
15
|
+
}, async ({ window_days, include_instagram_insights }) => {
|
|
12
16
|
try {
|
|
13
|
-
const
|
|
17
|
+
const params = {};
|
|
18
|
+
if (window_days)
|
|
19
|
+
params.window_days = String(window_days);
|
|
20
|
+
if (include_instagram_insights)
|
|
21
|
+
params.include = "instagram_insights";
|
|
22
|
+
const result = await client.get("/audience", Object.keys(params).length ? params : undefined);
|
|
14
23
|
return {
|
|
15
24
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
16
25
|
};
|
|
@@ -27,4 +36,105 @@ export function registerAudienceTools(server, client) {
|
|
|
27
36
|
};
|
|
28
37
|
}
|
|
29
38
|
});
|
|
39
|
+
server.tool("get_trial_reel_analytics", "instagram trial reel results: totals across every trial reel posted through dropspace (views, reach summed per reel, likes, comments, shares, saves, plus views-weighted avg_watch_time_ms and skip_rate, the percent of plays that skipped in the first 3 seconds), one entry per reel newest first (up to 100, with the numbers last read from instagram and when), and followers_since_first: net follows minus unfollows on your instagram accounts since the pacific day of the first trial reel, from the daily counts get_audience with include_instagram_insights stores (days_counted of days_in_span says how complete it is; it counts every new follower, not only ones the trial reels brought). reads stored numbers only, so it makes no instagram request.", {}, async () => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await client.get("/analytics/trial-reels");
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
server.tool("get_best_posting_hours", "when your instagram followers are online, from instagram's online_followers insight over the last 30 days. per connected instagram account: hourly (average followers online in each hour of the day, index = hour 0-23), grid (average by weekday, index 0 = sunday, then hour), days_counted, and best_hours, the busiest hours at least two hours apart (up to 5, busiest first); the top-level best_hours combines every instagram account. every hour is in timezone (default: your autopilot timezone, else UTC). cached about 12 hours per account. instagram reports this only for accounts with 100 or more followers (available: false with unavailable_reason otherwise). to post at these hours, set autopilot posting_times to them with configure_autopilot.", {
|
|
57
|
+
timezone: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("IANA timezone for every hour in the answer, e.g. America/New_York (default: autopilot timezone, else UTC)"),
|
|
61
|
+
}, async ({ timezone }) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.get("/audience/best-hours", timezone ? { timezone } : undefined);
|
|
64
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
server.tool("get_audience_demographics", "who your instagram audience is: per connected instagram account, the age, gender (F, M, U = unknown), country (ISO code), and city of its current followers (followers) and of the accounts that engaged with it in timeframe (engaged), each split largest first with total = people instagram has demographic data for. instagram reports the top 45 rows per split and nothing for accounts under 100 followers (available: false with unavailable_reason). cached about 12 hours per account.", {
|
|
79
|
+
timeframe: z
|
|
80
|
+
.enum(["this_week", "this_month", "last_90_days"])
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("period for the engaged audience (default this_month); followers are always current"),
|
|
83
|
+
}, async ({ timeframe }) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await client.get("/audience/demographics", timeframe ? { timeframe } : undefined);
|
|
86
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
server.tool("get_reels_read", "each connected instagram account with its latest reels read (claude's comparison of the top, middle, and bottom reels by views over the last 90 days), read is absent when none was made in the last 30 days; oldest_posted_at says how far back a read reaches (up to 60 reels within 90 days). next_refresh_at is when generate_reels_read can make a new one. makes no instagram or claude request. pro and premium.", {}, async () => {
|
|
101
|
+
try {
|
|
102
|
+
const result = await client.get("/analytics/reels-read");
|
|
103
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
server.tool("generate_reels_read", "make a reels read for one instagram account: the latest reels (up to 60) from the last 90 days are ranked by views, and claude compares the top, middle, and bottom ten from their captions and numbers (it never sees the videos). returns status ok with read (summary, working and not_working findings with evidence, three next steps), not_enough_reels (needs 8), or unavailable with reason. one new read per account a day: inside that day it returns the existing read with refreshed: false. takes 30-60 seconds. pro and premium.", {
|
|
118
|
+
connection_id: z
|
|
119
|
+
.string()
|
|
120
|
+
.uuid()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("instagram connection to read (from get_reels_read); default: the first instagram account"),
|
|
123
|
+
}, async ({ connection_id }) => {
|
|
124
|
+
try {
|
|
125
|
+
const result = await client.post("/analytics/reels-read", connection_id ? { connection_id } : {});
|
|
126
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `error: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
30
140
|
}
|
package/dist/tools/comments.js
CHANGED
|
@@ -76,4 +76,31 @@ export function registerCommentTools(server, client) {
|
|
|
76
76
|
return fail(error);
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
|
+
server.tool("list_comment_queue", "every comment still waiting for you across your instagram and youtube posts made through dropspace in the last 30 days (newest 30 posts, newest 50 comments each), newest first, up to 200. waiting: not hidden, not written or answered by your account, and not replied to, hidden, deleted, or dismissed through dropspace. each comment carries its post (platform, post_id, launch_id, launch_name) and claude's read: kind (question, feedback, praise, spam, attack, other; null until sorted), suggested_action (reply, hide, dismiss), and reply_draft for questions and feedback; may_be_answered is true when youtube returned only some replies and none was yours. counts per kind; post_issues lists posts that could not be read. reply with reply_to_comment, delete with delete_comment (launch_id + post_id + comment id); hide or dismiss several with act_on_comments.", {}, async () => {
|
|
80
|
+
try {
|
|
81
|
+
return text(await client.get("/comments/queue"));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return fail(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.tool("act_on_comments", "hide (instagram only, changes the post, needs the send scope) or dismiss (seen, nothing to do; nothing changes on the platform, needs the write scope) up to 50 comments from list_comment_queue. hides are sent one a second. returns a result per comment (ok, detail); comments not on your own dropspace posts fail with the reason.", {
|
|
88
|
+
action: z.enum(["hide", "dismiss"]).describe("hide on instagram, or dismiss from the queue"),
|
|
89
|
+
comments: z
|
|
90
|
+
.array(z.object({
|
|
91
|
+
platform: z.enum(["instagram", "youtube"]),
|
|
92
|
+
post_id: z.string().min(1).max(200),
|
|
93
|
+
comment_id: z.string().min(1).max(200),
|
|
94
|
+
}))
|
|
95
|
+
.min(1)
|
|
96
|
+
.max(50)
|
|
97
|
+
.describe("comments from list_comment_queue: platform, post_id, and the comment's id"),
|
|
98
|
+
}, async ({ action, comments }) => {
|
|
99
|
+
try {
|
|
100
|
+
return text(await client.post("/comments/actions", { action, comments }));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return fail(error);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
79
106
|
}
|
package/dist/tools/launches.js
CHANGED
|
@@ -119,7 +119,7 @@ export function registerLaunchTools(server, client) {
|
|
|
119
119
|
platform_contents: z
|
|
120
120
|
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
121
121
|
.optional()
|
|
122
|
-
.describe('per-platform content — each value needs "content" (string). Instagram takes "instagram_settings": { "post_type": "feed" (default) | "carousel" | "story" } and Facebook takes "facebook_settings": { "post_type": "feed" | "story" } - "carousel" (Instagram only) posts every attached image and the launch video as ONE carousel in media order, 2 to 10 items, where "feed" would post the video as a reel and leave the images out; "story" posts each image as a 24-hour story frame with no caption. 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'),
|
|
122
|
+
.describe('per-platform content — each value needs "content" (string). Instagram takes "instagram_settings": { "post_type": "feed" (default) | "carousel" | "story" | "trial_reel", "graduation_strategy"?: "MANUAL" (default) | "SS_PERFORMANCE" } and Facebook takes "facebook_settings": { "post_type": "feed" | "story" } - "carousel" (Instagram only) posts every attached image and the launch video as ONE carousel in media order, 2 to 10 items, where "feed" would post the video as a reel and leave the images out; "story" posts each image as a 24-hour story frame with no caption; "trial_reel" (Instagram only) posts the video as a reel shown to non-followers first, off the profile until it graduates - "MANUAL" when the creator shares it in the Instagram app, "SS_PERFORMANCE" when Instagram sees it performing well; Instagram allows trial reels from professional accounts with 200 or more followers, so a connected account below that bar is refused with the reason; a trial reel needs a video and leaves photos out. 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'),
|
|
123
123
|
custom_content: z
|
|
124
124
|
.union([z.string().min(1), z.array(z.string()).min(1).max(6)])
|
|
125
125
|
.optional()
|
|
@@ -244,7 +244,7 @@ export function registerLaunchTools(server, client) {
|
|
|
244
244
|
platform_contents: z
|
|
245
245
|
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
246
246
|
.optional()
|
|
247
|
-
.describe('per-platform content update (deep-merged with existing). each value can include "content" (string). Instagram takes "instagram_settings": { "post_type": "feed" | "carousel" | "story" } ("carousel" posts every attached image and the launch video as ONE carousel, 2 to 10 items) and Facebook takes "facebook_settings": { "post_type": "feed" | "story" }. 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'),
|
|
247
|
+
.describe('per-platform content update (deep-merged with existing). each value can include "content" (string). Instagram takes "instagram_settings": { "post_type": "feed" | "carousel" | "story" | "trial_reel", "graduation_strategy"?: "MANUAL" | "SS_PERFORMANCE" } ("carousel" posts every attached image and the launch video as ONE carousel, 2 to 10 items; "trial_reel" posts the video as a reel shown to non-followers first, graduating per graduation_strategy; needs a professional account with 200 or more followers, or the update is refused with the reason) and Facebook takes "facebook_settings": { "post_type": "feed" | "story" }. 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'),
|
|
248
248
|
dropspace_platforms: z.array(z.enum(PLATFORMS)).optional().describe("official dropspace account platforms"),
|
|
249
249
|
attribution_mode: z
|
|
250
250
|
.enum(["handle", "anonymous"])
|
package/dist/types.d.ts
CHANGED
|
@@ -170,6 +170,8 @@ export interface Connection {
|
|
|
170
170
|
account_info: unknown | null;
|
|
171
171
|
account_type: string | null;
|
|
172
172
|
is_active: boolean;
|
|
173
|
+
/** Why the connection is inactive (the connection.expired reason); null while live. */
|
|
174
|
+
inactive_reason: string | null;
|
|
173
175
|
expires_at: string | null;
|
|
174
176
|
created_at: string;
|
|
175
177
|
updated_at: string;
|
package/package.json
CHANGED
|
@@ -41,7 +41,7 @@ Dropspace turns one brief into per-platform posts and publishes them on the user
|
|
|
41
41
|
- `media_source`: `ai` (generated slides and video), `library` (only the user's own media), or `mixed` (library first). `product_image_url` makes AI slides show the real product.
|
|
42
42
|
- Media library (stdio server and CLI only): `list_media_library`, `add_to_media_library`, `update_media_library_item`, `delete_media_library_item`. Autopilot chooses items by their description, so describe each one. Items without one are captioned on autopilot's next run (images from the image, videos from their `thumbnail_url`), so a video needs a description or a thumbnail.
|
|
43
43
|
- Brand kit: `update_persona` with `brand_kit` (text and outline colors, a corner logo) styles the image slides autopilot posts as that persona.
|
|
44
|
-
- Audience: `get_audience` for follower counts on Instagram, TikTok, and YouTube.
|
|
44
|
+
- Audience: `get_audience` for follower counts on Instagram, TikTok, and YouTube; its instagram insights option adds Instagram follows and unfollows per day, followers over time, growth velocity, and the non-follower reach share.
|
|
45
45
|
- Webhooks: `create_webhook` with a Slack incoming webhook URL (`https://hooks.slack.com/services/...`) posts events to that channel as readable messages.
|
|
46
46
|
|
|
47
47
|
## Spending money - confirm first
|