@jclvsh/dropspace 2.48.0 → 2.49.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
@@ -389,6 +389,11 @@ Reddit engagement (drafted comments in other communities) is paused: its tools a
389
389
  | `save_report` | Save a markdown report to the user's reports page (local package only) |
390
390
  | `list_reports` | Saved reports, newest first, without bodies (local package only) |
391
391
  | `get_report` | One report with its markdown body (local package only) |
392
+ | `list_songs` | Your songs for song to reels, newest first (pro and premium; local package only) |
393
+ | `get_song` | One song with its waveform peaks (local package only) |
394
+ | `add_song` | Add a song from a YouTube or SoundCloud link, or a public audio file url (local package only) |
395
+ | `get_song_lyrics` | The song's lyrics with word timings, transcribed once (local package only) |
396
+ | `delete_song` | Delete a song and its audio file (local package only) |
392
397
 
393
398
  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`.
394
399
 
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { registerAutopilotCommands } from "./commands/autopilot.js";
19
19
  import { registerRecommendationCommands } from "./commands/recommendations.js";
20
20
  import { registerAudienceCommands } from "./commands/audience.js";
21
21
  import { registerAccountCommands } from "./commands/account.js";
22
+ import { registerSongCommands } from "./commands/songs.js";
22
23
  const program = new Command()
23
24
  .name("dropspace")
24
25
  .description("dropspace CLI - manage launches, personas, boosts, and more")
@@ -42,6 +43,7 @@ try {
42
43
  registerRecommendationCommands(program, client);
43
44
  registerRedditCommands(program, client);
44
45
  registerAccountCommands(program, client);
46
+ registerSongCommands(program, client);
45
47
  await program.parseAsync(process.argv);
46
48
  }
47
49
  catch (error) {
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ import type { DropspaceClient } from "../client.js";
3
+ export declare function registerSongCommands(program: Command, client: DropspaceClient): void;
@@ -0,0 +1,97 @@
1
+ import { formatOutput, handleError, getFormat } from "../cli-utils.js";
2
+ export function registerSongCommands(program, client) {
3
+ const songs = program.command("songs").description("your songs for song-to-reels (pro and premium)");
4
+ songs
5
+ .command("list")
6
+ .description("your songs, newest first")
7
+ .option("--page <n>", "page number")
8
+ .option("--page-size <n>", "songs per page (max 100)")
9
+ .action(async (opts) => {
10
+ try {
11
+ const params = {};
12
+ if (opts.page)
13
+ params.page = opts.page;
14
+ if (opts.pageSize)
15
+ params.page_size = opts.pageSize;
16
+ formatOutput(await client.get("/songs", params), getFormat(program.opts()));
17
+ }
18
+ catch (error) {
19
+ handleError(error);
20
+ }
21
+ });
22
+ songs
23
+ .command("get <id>")
24
+ .description("one song, with its waveform peaks")
25
+ .action(async (id) => {
26
+ try {
27
+ formatOutput(await client.get(`/songs/${id}`), getFormat(program.opts()));
28
+ }
29
+ catch (error) {
30
+ handleError(error);
31
+ }
32
+ });
33
+ songs
34
+ .command("add")
35
+ .description("add a song from a youtube or soundcloud link, or a public audio file url")
36
+ .option("--link <url>", "a youtube or soundcloud link")
37
+ .option("--audio <url>", "a public audio file url (mp3, m4a, wav, aac, flac, ogg)")
38
+ .option("-t, --title <text>", "the title; the link's or file's name otherwise")
39
+ .action(async (opts) => {
40
+ try {
41
+ if (!opts.link && !opts.audio)
42
+ throw new Error("give --link or --audio");
43
+ const body = opts.link ? { link_url: opts.link } : { audio_url: opts.audio };
44
+ if (opts.title)
45
+ body.title = opts.title;
46
+ formatOutput(await client.post("/songs", body), getFormat(program.opts()));
47
+ }
48
+ catch (error) {
49
+ handleError(error);
50
+ }
51
+ });
52
+ songs
53
+ .command("lyrics <id>")
54
+ .description("the song's lyrics with word timings (transcribed on first request)")
55
+ .action(async (id) => {
56
+ try {
57
+ formatOutput(await client.get(`/songs/${id}/lyrics`), getFormat(program.opts()));
58
+ }
59
+ catch (error) {
60
+ handleError(error);
61
+ }
62
+ });
63
+ songs
64
+ .command("rename <id>")
65
+ .description("rename a song")
66
+ .requiredOption("-t, --title <text>", "the new title")
67
+ .action(async (id, opts) => {
68
+ try {
69
+ formatOutput(await client.patch(`/songs/${id}`, { title: opts.title }), getFormat(program.opts()));
70
+ }
71
+ catch (error) {
72
+ handleError(error);
73
+ }
74
+ });
75
+ songs
76
+ .command("retry <id>")
77
+ .description("run a failed import again")
78
+ .action(async (id) => {
79
+ try {
80
+ formatOutput(await client.post(`/songs/${id}/retry`), getFormat(program.opts()));
81
+ }
82
+ catch (error) {
83
+ handleError(error);
84
+ }
85
+ });
86
+ songs
87
+ .command("delete <id>")
88
+ .description("delete a song and its audio file")
89
+ .action(async (id) => {
90
+ try {
91
+ formatOutput(await client.delete(`/songs/${id}`), getFormat(program.opts()));
92
+ }
93
+ catch (error) {
94
+ handleError(error);
95
+ }
96
+ });
97
+ }
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -19,6 +19,7 @@ import { registerCommentTools } from "./tools/comments.js";
19
19
  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
+ import { registerSongTools } from "./tools/songs.js";
22
23
  /** Drops tools whose feature is paused before the policy registrar sees them (see PAUSED_TOOLS). */
23
24
  function skipPausedTools(registrar) {
24
25
  return {
@@ -50,6 +51,7 @@ export function createDropspaceServer({ client, version, profile }) {
50
51
  registerAccountTools(registrar, client);
51
52
  registerTrialReelTools(registrar, client);
52
53
  registerReportTools(registrar, client);
54
+ registerSongTools(registrar, client);
53
55
  // Prompts are not tools, so the policy registrar does not filter them: the hosted profile
54
56
  // (frozen for the Meta review) gets none.
55
57
  if (profile === "full")
@@ -136,6 +136,12 @@ const FULL_TOOL_POLICY = {
136
136
  save_report: localOnly("account", "write"),
137
137
  list_reports: localOnly("account", "read"),
138
138
  get_report: localOnly("account", "read"),
139
+ // songs (song to reels): local-only while the hosted set is frozen for the Meta review
140
+ list_songs: localOnly("media", "read"),
141
+ get_song: localOnly("media", "read"),
142
+ add_song: localOnly("media", "write"),
143
+ get_song_lyrics: localOnly("media", "read"),
144
+ delete_song: localOnly("media", "write"),
139
145
  get_usage: hosted("usage", "read"),
140
146
  get_changelog: hosted("usage", "read"),
141
147
  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
+ /** Songs for song-to-reels (pro and premium). Local package only. */
4
+ export declare function registerSongTools(server: McpServer, client: DropspaceClient): void;
@@ -0,0 +1,71 @@
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
+ /** Songs for song-to-reels (pro and premium). Local package only. */
12
+ export function registerSongTools(server, client) {
13
+ server.tool("list_songs", "the user's songs, newest first: id, title, source (upload, link, or url), source_url (the link or audio url it came from), status (pending while the audio is fetched and converted, ready, or failed with error), duration_seconds, size_bytes, audio_url once ready, lyrics_status (none until lyrics are first requested, ready, or failed), created_at. at most 100 songs per user. pro and premium plans.", {
14
+ page: z.number().int().min(1).optional(),
15
+ page_size: z.number().int().min(1).max(100).optional(),
16
+ }, async ({ page, page_size }) => {
17
+ try {
18
+ const params = {};
19
+ if (page)
20
+ params.page = String(page);
21
+ if (page_size)
22
+ params.page_size = String(page_size);
23
+ return text(await client.get("/songs", params));
24
+ }
25
+ catch (error) {
26
+ return fail(error);
27
+ }
28
+ });
29
+ server.tool("get_song", "one song with its waveform: everything list_songs shows plus peaks (400 numbers from 0 to 1 across the song, for picking a snippet by ear and eye).", { song_id: z.string().uuid() }, async ({ song_id }) => {
30
+ try {
31
+ return text(await client.get(`/songs/${song_id}`));
32
+ }
33
+ catch (error) {
34
+ return fail(error);
35
+ }
36
+ });
37
+ server.tool("add_song", "add a song from a youtube or soundcloud link, or from a public audio file url (mp3, m4a, wav, aac, flac, ogg, up to 50 MB). the audio is fetched and converted in the background (longer than 15 minutes is cut there): the song comes back pending; read it again with get_song until it is ready or failed (a youtube refusal says to upload the file instead). the song must be the user's own. pro and premium plans; at most 100 songs.", {
38
+ link_url: z.string().url().optional().describe("a youtube or soundcloud link"),
39
+ audio_url: z.string().url().optional().describe("a public audio file url, when not using a link"),
40
+ title: z.string().min(1).max(200).optional().describe("the title; the link's or file's name otherwise"),
41
+ }, async ({ link_url, audio_url, title }) => {
42
+ try {
43
+ if (!link_url && !audio_url)
44
+ throw new Error("give link_url or audio_url");
45
+ const body = link_url ? { link_url } : { audio_url };
46
+ if (title)
47
+ body.title = title;
48
+ return text(await client.post("/songs", body));
49
+ }
50
+ catch (error) {
51
+ return fail(error);
52
+ }
53
+ });
54
+ server.tool("get_song_lyrics", "the song's lyrics with word timings, transcribed on first request (takes up to a minute for a long song) and kept: language, text, and words with start and end seconds. a song with no recognizable words comes back with empty words. the song must be ready.", { song_id: z.string().uuid() }, async ({ song_id }) => {
55
+ try {
56
+ return text(await client.get(`/songs/${song_id}/lyrics`));
57
+ }
58
+ catch (error) {
59
+ return fail(error);
60
+ }
61
+ });
62
+ server.tool("delete_song", "delete a song and its audio file.", { song_id: z.string().uuid() }, async ({ song_id }) => {
63
+ try {
64
+ await client.delete(`/songs/${song_id}`);
65
+ return text({ id: song_id, deleted: true });
66
+ }
67
+ catch (error) {
68
+ return fail(error);
69
+ }
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jclvsh/dropspace",
3
- "version": "2.48.0",
3
+ "version": "2.49.0",
4
4
  "description": "MCP server and CLI for the Dropspace API",
5
5
  "repository": {
6
6
  "type": "git",