@jclvsh/dropspace 2.49.0 → 2.50.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 +5 -0
- package/dist/cli.js +2 -0
- package/dist/commands/song-reels.d.ts +3 -0
- package/dist/commands/song-reels.js +95 -0
- package/dist/server.js +2 -0
- package/dist/tool-policy.js +6 -0
- package/dist/tools/song-reels.d.ts +4 -0
- package/dist/tools/song-reels.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -394,6 +394,11 @@ Reddit engagement (drafted comments in other communities) is paused: its tools a
|
|
|
394
394
|
| `add_song` | Add a song from a YouTube or SoundCloud link, or a public audio file url (local package only) |
|
|
395
395
|
| `get_song_lyrics` | The song's lyrics with word timings, transcribed once (local package only) |
|
|
396
396
|
| `delete_song` | Delete a song and its audio file (local package only) |
|
|
397
|
+
| `make_song_reel` | Make a 9:16 lyric reel from a snippet of a song: clips from the media library or a gradient, lyrics burned in (local package only) |
|
|
398
|
+
| `list_song_reels` | Lyric reels, newest first, with render status and video_url once ready (local package only) |
|
|
399
|
+
| `get_song_reel` | One reel with its lyric lines (local package only) |
|
|
400
|
+
| `launch_song_reel` | A draft launch carrying a ready reel and its lyrics as the caption (local package only) |
|
|
401
|
+
| `delete_song_reel` | Delete a reel; its media library item and launch stay (local package only) |
|
|
397
402
|
|
|
398
403
|
Prompts (local package only): `instagram_audit`, `best_times_plan`, `winners_report`. Each tells the AI app which tools to call and ends with `save_report`.
|
|
399
404
|
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerRecommendationCommands } from "./commands/recommendations.js";
|
|
|
20
20
|
import { registerAudienceCommands } from "./commands/audience.js";
|
|
21
21
|
import { registerAccountCommands } from "./commands/account.js";
|
|
22
22
|
import { registerSongCommands } from "./commands/songs.js";
|
|
23
|
+
import { registerSongReelCommands } from "./commands/song-reels.js";
|
|
23
24
|
const program = new Command()
|
|
24
25
|
.name("dropspace")
|
|
25
26
|
.description("dropspace CLI - manage launches, personas, boosts, and more")
|
|
@@ -44,6 +45,7 @@ try {
|
|
|
44
45
|
registerRedditCommands(program, client);
|
|
45
46
|
registerAccountCommands(program, client);
|
|
46
47
|
registerSongCommands(program, client);
|
|
48
|
+
registerSongReelCommands(program, client);
|
|
47
49
|
await program.parseAsync(process.argv);
|
|
48
50
|
}
|
|
49
51
|
catch (error) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { formatOutput, handleError, getFormat } from "../cli-utils.js";
|
|
2
|
+
export function registerSongReelCommands(program, client) {
|
|
3
|
+
const reels = program.command("song-reels").description("lyric reels made from your songs (pro and premium)");
|
|
4
|
+
reels
|
|
5
|
+
.command("make <songId>")
|
|
6
|
+
.description("make a lyric reel from a snippet of a ready song")
|
|
7
|
+
.requiredOption("--start <seconds>", "snippet start, seconds from the song's start")
|
|
8
|
+
.requiredOption("--end <seconds>", "snippet end, 5 to 30 seconds after the start")
|
|
9
|
+
.option("--style <style>", "lower-third, center-bold, karaoke, one-word, or caps-top", "lower-third")
|
|
10
|
+
.option("--clips <ids>", "media library ids in order, comma separated (footage from your library)")
|
|
11
|
+
.option("--background <name>", "midnight, sunset, forest, sand, slate, or violet (no footage)")
|
|
12
|
+
.action(async (songId, opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const body = {
|
|
15
|
+
snippet_start: Number(opts.start),
|
|
16
|
+
snippet_end: Number(opts.end),
|
|
17
|
+
style: opts.style,
|
|
18
|
+
footage_mode: opts.clips ? "clips" : "none",
|
|
19
|
+
};
|
|
20
|
+
if (opts.clips)
|
|
21
|
+
body.clip_ids = String(opts.clips).split(",").map((id) => id.trim()).filter(Boolean);
|
|
22
|
+
if (opts.background)
|
|
23
|
+
body.background = opts.background;
|
|
24
|
+
formatOutput(await client.post(`/songs/${songId}/reels`, body), getFormat(program.opts()));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
handleError(error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
reels
|
|
31
|
+
.command("list")
|
|
32
|
+
.description("your lyric reels, newest first")
|
|
33
|
+
.option("--song <id>", "only this song's reels")
|
|
34
|
+
.option("--page <n>", "page number")
|
|
35
|
+
.option("--page-size <n>", "reels per page (max 100)")
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
try {
|
|
38
|
+
const params = {};
|
|
39
|
+
if (opts.song)
|
|
40
|
+
params.song_id = opts.song;
|
|
41
|
+
if (opts.page)
|
|
42
|
+
params.page = opts.page;
|
|
43
|
+
if (opts.pageSize)
|
|
44
|
+
params.page_size = opts.pageSize;
|
|
45
|
+
formatOutput(await client.get("/song-reels", params), getFormat(program.opts()));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
handleError(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
reels
|
|
52
|
+
.command("get <id>")
|
|
53
|
+
.description("one reel, with its lyric lines")
|
|
54
|
+
.action(async (id) => {
|
|
55
|
+
try {
|
|
56
|
+
formatOutput(await client.get(`/song-reels/${id}`), getFormat(program.opts()));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
handleError(error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
reels
|
|
63
|
+
.command("retry <id>")
|
|
64
|
+
.description("render a failed reel again")
|
|
65
|
+
.action(async (id) => {
|
|
66
|
+
try {
|
|
67
|
+
formatOutput(await client.post(`/song-reels/${id}/retry`), getFormat(program.opts()));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
handleError(error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
reels
|
|
74
|
+
.command("launch <id>")
|
|
75
|
+
.description("make a draft launch from a ready reel")
|
|
76
|
+
.action(async (id) => {
|
|
77
|
+
try {
|
|
78
|
+
formatOutput(await client.post(`/song-reels/${id}/launch`), getFormat(program.opts()));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
handleError(error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
reels
|
|
85
|
+
.command("delete <id>")
|
|
86
|
+
.description("delete a reel (its media library item and launch stay)")
|
|
87
|
+
.action(async (id) => {
|
|
88
|
+
try {
|
|
89
|
+
formatOutput(await client.delete(`/song-reels/${id}`), getFormat(program.opts()));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
handleError(error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerAccountTools } from "./tools/account.js";
|
|
|
20
20
|
import { registerTrialReelTools } from "./tools/trial-reels.js";
|
|
21
21
|
import { registerReportTools, registerWorkflowPrompts } from "./tools/reports.js";
|
|
22
22
|
import { registerSongTools } from "./tools/songs.js";
|
|
23
|
+
import { registerSongReelTools } from "./tools/song-reels.js";
|
|
23
24
|
/** Drops tools whose feature is paused before the policy registrar sees them (see PAUSED_TOOLS). */
|
|
24
25
|
function skipPausedTools(registrar) {
|
|
25
26
|
return {
|
|
@@ -52,6 +53,7 @@ export function createDropspaceServer({ client, version, profile }) {
|
|
|
52
53
|
registerTrialReelTools(registrar, client);
|
|
53
54
|
registerReportTools(registrar, client);
|
|
54
55
|
registerSongTools(registrar, client);
|
|
56
|
+
registerSongReelTools(registrar, client);
|
|
55
57
|
// Prompts are not tools, so the policy registrar does not filter them: the hosted profile
|
|
56
58
|
// (frozen for the Meta review) gets none.
|
|
57
59
|
if (profile === "full")
|
package/dist/tool-policy.js
CHANGED
|
@@ -142,6 +142,12 @@ const FULL_TOOL_POLICY = {
|
|
|
142
142
|
add_song: localOnly("media", "write"),
|
|
143
143
|
get_song_lyrics: localOnly("media", "read"),
|
|
144
144
|
delete_song: localOnly("media", "write"),
|
|
145
|
+
// lyric reels: a render spends the plan's quota; a launch is a draft the user still publishes
|
|
146
|
+
make_song_reel: localOnly("media", "write"),
|
|
147
|
+
list_song_reels: localOnly("media", "read"),
|
|
148
|
+
get_song_reel: localOnly("media", "read"),
|
|
149
|
+
launch_song_reel: localOnly("media", "write"),
|
|
150
|
+
delete_song_reel: localOnly("media", "write"),
|
|
145
151
|
get_usage: hosted("usage", "read"),
|
|
146
152
|
get_changelog: hosted("usage", "read"),
|
|
147
153
|
get_dropspace_status: hosted("dropspace", "read"),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { DropspaceClient } from "../client.js";
|
|
3
|
+
/** Lyric reels made from songs (pro and premium). Local package only. */
|
|
4
|
+
export declare function registerSongReelTools(server: McpServer, client: DropspaceClient): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function text(result) {
|
|
3
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
4
|
+
}
|
|
5
|
+
function fail(error) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: `error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
8
|
+
isError: true,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const STYLES = ["lower-third", "center-bold", "karaoke", "one-word", "caps-top"];
|
|
12
|
+
const BACKGROUNDS = ["midnight", "sunset", "forest", "sand", "slate", "violet"];
|
|
13
|
+
/** Lyric reels made from songs (pro and premium). Local package only. */
|
|
14
|
+
export function registerSongReelTools(server, client) {
|
|
15
|
+
server.tool("make_song_reel", "make a 9:16 lyric reel from a snippet of a ready song (get_song for its duration and peaks): 5 to 30 seconds of the song under footage, with the lyrics inside the snippet burned in. footage_mode clips uses the user's media library videos and images (clip_ids from list_media_library, in order; the clip changes at lyric lines, skipping cuts closer than 1.5 s apart); none puts the lyrics over a gradient (background). the lyrics are read from the song on the first reel (takes up to a minute). the render runs on the server: the reel comes back pending; read it with get_song_reel until ready (video_url, and it is in the media library) or failed. counts against the plan's song reels per billing period (pro 30, premium 100; a rendered reel keeps its slot even when deleted).", {
|
|
16
|
+
song_id: z.string().uuid(),
|
|
17
|
+
snippet_start: z.number().min(0).describe("seconds from the start of the song"),
|
|
18
|
+
snippet_end: z.number().positive().describe("5 to 30 seconds after snippet_start"),
|
|
19
|
+
style: z.enum(STYLES).describe("how the lyrics look: lower-third, center-bold, karaoke (word by word), one-word, caps-top"),
|
|
20
|
+
footage_mode: z.enum(["clips", "none"]),
|
|
21
|
+
clip_ids: z.array(z.string().uuid()).max(20).optional().describe("media library ids, in order (footage_mode clips)"),
|
|
22
|
+
background: z.enum(BACKGROUNDS).optional().describe("the gradient (footage_mode none); midnight unless given"),
|
|
23
|
+
}, async ({ song_id, ...body }) => {
|
|
24
|
+
try {
|
|
25
|
+
return text(await client.post(`/songs/${song_id}/reels`, body));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return fail(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.tool("list_song_reels", "the user's lyric reels, newest first: id, song_id, title, snippet, style, footage_mode, render_status (pending, running, ready, or failed with render_error), video_url and thumbnail_url once ready, media_library_id, launch_id. song_id narrows to one song.", {
|
|
32
|
+
song_id: z.string().uuid().optional(),
|
|
33
|
+
page: z.number().int().min(1).optional(),
|
|
34
|
+
page_size: z.number().int().min(1).max(100).optional(),
|
|
35
|
+
}, async ({ song_id, page, page_size }) => {
|
|
36
|
+
try {
|
|
37
|
+
const params = {};
|
|
38
|
+
if (song_id)
|
|
39
|
+
params.song_id = song_id;
|
|
40
|
+
if (page)
|
|
41
|
+
params.page = String(page);
|
|
42
|
+
if (page_size)
|
|
43
|
+
params.page_size = String(page_size);
|
|
44
|
+
return text(await client.get("/song-reels", params));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return fail(error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
server.tool("get_song_reel", "one lyric reel: everything list_song_reels shows plus its lines (the lyric lines inside the snippet with their timing).", { reel_id: z.string().uuid() }, async ({ reel_id }) => {
|
|
51
|
+
try {
|
|
52
|
+
return text(await client.get(`/song-reels/${reel_id}`));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return fail(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
server.tool("launch_song_reel", "make a draft launch from a ready reel: the reel is its video and the snippet's lyrics its instagram caption. the launch stays a draft until the user (or update_launch + publish) sets platforms, caption, and timing, and it counts as one of the plan's launches. returns the reel with launch_id.", { reel_id: z.string().uuid() }, async ({ reel_id }) => {
|
|
59
|
+
try {
|
|
60
|
+
return text(await client.post(`/song-reels/${reel_id}/launch`));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return fail(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
server.tool("delete_song_reel", "delete a lyric reel. its media library item and any launch made from it stay.", { reel_id: z.string().uuid() }, async ({ reel_id }) => {
|
|
67
|
+
try {
|
|
68
|
+
await client.delete(`/song-reels/${reel_id}`);
|
|
69
|
+
return text({ id: reel_id, deleted: true });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return fail(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|