clipbait 1.4.1 → 1.6.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 +16 -1
- package/index.js +109 -6
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -31,4 +31,19 @@ npx clipbait status <jobId> # get finished clip URLs
|
|
|
31
31
|
|
|
32
32
|
Auth via `CLIPBAIT_API_KEY` env var or `~/.clipbait.json`. Requires Node 18+.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
## MCP server (Claude Desktop, Cursor, Cline, Windsurf)
|
|
35
|
+
This package is also an MCP server. `npx clipbait@latest` auto-configures **Claude Code**. For other clients, add this to their MCP config:
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"clipbait": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "clipbait@latest", "mcp"],
|
|
42
|
+
"env": { "CLIPBAIT_API_KEY": "cbk_live_your_key_here" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
Tools: `generate_clips`, `get_job`, `list_recent_clips`, `get_credits`, `start_live_autoclip`, `probe_video`, and **`download_clip`** (saves a finished clip to your `~/Downloads`).
|
|
48
|
+
|
|
49
|
+
Using **ChatGPT / claude.ai** (URL connectors, no local process)? Point them at the hosted MCP instead: `https://app.clipbait.ai/api/mcp/<your_api_key>`.
|
package/index.js
CHANGED
|
@@ -68,6 +68,34 @@ function prompt(question) {
|
|
|
68
68
|
return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a); }));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Register the Clipbait MCP server by editing Claude Code's config directly.
|
|
72
|
+
// This is the reliable path: the `claude` CLI often isn't on PATH (agents,
|
|
73
|
+
// sandboxes), but the config file is always the source of truth. Claude Code
|
|
74
|
+
// reads MCP servers from ~/.claude.json under projects[cwd].mcpServers (local
|
|
75
|
+
// scope) and, when present, a top-level mcpServers (user scope) — we write
|
|
76
|
+
// both so it shows up regardless of which directory Claude opens in.
|
|
77
|
+
function writeMcpConfig(mcpUrl) {
|
|
78
|
+
const cfgPath = path.join(os.homedir(), ".claude.json");
|
|
79
|
+
let cfg;
|
|
80
|
+
try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); }
|
|
81
|
+
catch { return false; } // no/unreadable config → caller prints manual steps
|
|
82
|
+
const entry = { type: "http", url: mcpUrl };
|
|
83
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
84
|
+
cfg.mcpServers.clipbait = entry;
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
cfg.projects = cfg.projects || {};
|
|
87
|
+
cfg.projects[cwd] = cfg.projects[cwd] || {};
|
|
88
|
+
cfg.projects[cwd].mcpServers = cfg.projects[cwd].mcpServers || {};
|
|
89
|
+
cfg.projects[cwd].mcpServers.clipbait = entry;
|
|
90
|
+
try {
|
|
91
|
+
// Atomic write (temp + rename) so a failure can't corrupt the config.
|
|
92
|
+
const tmp = cfgPath + ".clipbait.tmp";
|
|
93
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
94
|
+
fs.renameSync(tmp, cfgPath);
|
|
95
|
+
return true;
|
|
96
|
+
} catch { return false; }
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
// The /clipbait Claude Code slash command. Installed by setup so users can type
|
|
72
100
|
// "/clipbait <url>" and let Claude drive the whole clip pipeline.
|
|
73
101
|
const SLASH_COMMAND = `---
|
|
@@ -119,22 +147,96 @@ async function runSetup(providedKey) {
|
|
|
119
147
|
fs.writeFileSync(path.join(cmdDir, "clipbait.md"), SLASH_COMMAND);
|
|
120
148
|
console.log("✓ Installed the /clipbait command for Claude Code");
|
|
121
149
|
|
|
122
|
-
// 2) Register the MCP server with Claude Code
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
150
|
+
// 2) Register the MCP server with Claude Code. Prefer editing the config
|
|
151
|
+
// file directly (works everywhere); fall back to the `claude` CLI, then to
|
|
152
|
+
// printing manual steps.
|
|
153
|
+
if (writeMcpConfig(mcpUrl)) {
|
|
126
154
|
console.log("✓ Connected the Clipbait MCP server to Claude Code");
|
|
127
155
|
} else {
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
const { spawnSync } = require("child_process");
|
|
157
|
+
const r = spawnSync("claude", ["mcp", "add", "--transport", "http", "clipbait", mcpUrl], { stdio: "ignore" });
|
|
158
|
+
if (!r.error && r.status === 0) {
|
|
159
|
+
console.log("✓ Connected the Clipbait MCP server to Claude Code");
|
|
160
|
+
} else {
|
|
161
|
+
console.log("• Couldn't auto-connect the MCP server. Add it manually:");
|
|
162
|
+
console.log(" claude mcp add --transport http clipbait " + mcpUrl);
|
|
163
|
+
}
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
console.log("\n🎬 Done! Restart Claude Code, then type:\n /clipbait https://youtube.com/watch?v=…\n");
|
|
133
167
|
}
|
|
134
168
|
|
|
169
|
+
// Download a clip URL to ~/Downloads and return the saved path. Presigned
|
|
170
|
+
// storage URLs need no auth; our own proxy URLs need the key — only send the
|
|
171
|
+
// key to clipbait.ai hosts so we never leak it to third-party storage.
|
|
172
|
+
async function downloadToFile(url, filename) {
|
|
173
|
+
const dir = path.join(os.homedir(), "Downloads");
|
|
174
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
175
|
+
const dest = path.join(dir, filename);
|
|
176
|
+
const headers = /(^|\.)clipbait\.ai/i.test(new URL(url).host) ? { "X-API-Key": loadKey() } : {};
|
|
177
|
+
const res = await fetch(url, { headers });
|
|
178
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
|
|
179
|
+
const len = Number(res.headers.get("content-length") || 0);
|
|
180
|
+
if (len > 500 * 1024 * 1024) throw new Error("clip is larger than 500 MB");
|
|
181
|
+
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
182
|
+
return dest;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Local stdio MCP server. Lets Claude Desktop, Cursor, Cline, Windsurf, etc.
|
|
186
|
+
// drive Clipbait via `npx clipbait mcp`. Tools proxy the REST API; download_clip
|
|
187
|
+
// runs locally so it can save to disk (a remote MCP can't). NEVER write to
|
|
188
|
+
// stdout here — it's the MCP transport; diagnostics go to stderr only.
|
|
189
|
+
async function runMcpServer() {
|
|
190
|
+
if (!loadKey()) { console.error("No API key. Set CLIPBAIT_API_KEY or run: npx clipbait <cbk_live_...>"); process.exit(1); }
|
|
191
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
192
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
193
|
+
const { z } = require("zod");
|
|
194
|
+
const server = new McpServer({ name: "clipbait", version: require("./package.json").version });
|
|
195
|
+
const ok = (t) => ({ content: [{ type: "text", text: t }] });
|
|
196
|
+
const fail = (e) => ({ content: [{ type: "text", text: "Error: " + (e.message || String(e)) }], isError: true });
|
|
197
|
+
|
|
198
|
+
server.tool("generate_clips", "Generate short vertical viral clips from a long video (YouTube, Twitch VOD, Rumble, Ganjing). Returns a job id; poll get_job.",
|
|
199
|
+
{ videoUrl: z.string(), aspectRatio: z.enum(["9:16", "16:9"]).optional(), maxClips: z.number().int().min(1).max(20).optional() },
|
|
200
|
+
async (a) => { try { const d = await api("POST", "/clips/generate", { videoUrl: a.videoUrl, aspectRatio: a.aspectRatio || "9:16", maxClips: a.maxClips || 9 }); return ok(`Clip job started: ${d.jobId}${d.duplicate ? " (duplicate of a recent job — no extra credits)" : ""}. Poll get_job("${d.jobId}").`); } catch (e) { return fail(e); } });
|
|
201
|
+
|
|
202
|
+
server.tool("get_job", "Check a clip job's status and get finished clip URLs.", { jobId: z.string() },
|
|
203
|
+
async (a) => { try { const d = await api("GET", "/clips/status/" + a.jobId); const clips = (d.clips || []).map((x, i) => `#${i + 1} ${x.hook || ""} — ${x.url || "(rendering)"}`).join("\n"); return ok(`Status: ${d.status} (${d.progress ?? 0}%)\n${clips || "No clips yet."}`); } catch (e) { return fail(e); } });
|
|
204
|
+
|
|
205
|
+
server.tool("list_recent_clips", "List your recent clip jobs and how many clips each produced.", {},
|
|
206
|
+
async () => { try { const d = await api("GET", "/clips/jobs"); const jobs = (d.jobs || d || []).slice(0, 10).map((j) => `${j._id} · ${j.status} · ${(j.clips || []).length} clips · ${j.videoTitle || j.streamerName || ""}`).join("\n"); return ok(jobs || "No jobs yet."); } catch (e) { return fail(e); } });
|
|
207
|
+
|
|
208
|
+
server.tool("get_credits", "Check remaining Clipbait credits and plan before starting a job (1 credit ≈ 1 minute of source video).", {},
|
|
209
|
+
async () => { try { const d = await api("GET", "/clips/credits"); return ok(`Plan: ${d.plan}. Credits remaining: ${d.credits_remaining}${d.credits_total ? ` of ${d.credits_total}` : ""}.`); } catch (e) { return fail(e); } });
|
|
210
|
+
|
|
211
|
+
server.tool("start_live_autoclip", "Start continuous auto-clipping of a LIVE Twitch stream (Pro). Clips are generated automatically as viral moments happen.",
|
|
212
|
+
{ channelUrl: z.string(), cadenceMin: z.number().int().optional() },
|
|
213
|
+
async (a) => { try { const d = await api("POST", "/live/start", { videoUrl: a.channelUrl, cadenceMin: a.cadenceMin || 7 }); return ok(`Live auto-clipping started (${d.liveSessionId || d.session?._id || "?"}).`); } catch (e) { return fail(e); } });
|
|
214
|
+
|
|
215
|
+
server.tool("probe_video", "Get a video's duration before clipping.", { url: z.string() },
|
|
216
|
+
async (a) => { try { const d = await api("POST", "/clips/probe-url", { url: a.url }); return ok(d.durationSeconds != null ? `Duration: ${d.durationSeconds}s (~${Math.round(d.durationSeconds / 60)} min)` : "Duration unknown for this platform."); } catch (e) { return fail(e); } });
|
|
217
|
+
|
|
218
|
+
server.tool("download_clip", "Save a finished clip to the user's ~/Downloads folder and return the file path. Call after get_job shows the job is complete.",
|
|
219
|
+
{ jobId: z.string(), clipIndex: z.number().int().min(1).optional().describe("1-based, default 1") },
|
|
220
|
+
async (a) => {
|
|
221
|
+
try {
|
|
222
|
+
const d = await api("GET", "/clips/status/" + a.jobId);
|
|
223
|
+
const clips = d.clips || [];
|
|
224
|
+
if (!clips.length) return fail(new Error("No clips are ready for this job yet."));
|
|
225
|
+
const idx = (a.clipIndex || 1) - 1;
|
|
226
|
+
const clip = clips[idx];
|
|
227
|
+
if (!clip || !clip.url) return fail(new Error(`Clip #${a.clipIndex || 1} isn't available.`));
|
|
228
|
+
const p = await downloadToFile(clip.url, `clipbait-${a.jobId}-${idx + 1}.mp4`);
|
|
229
|
+
return ok(`Saved to ${p}`);
|
|
230
|
+
} catch (e) { return fail(e); }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await server.connect(new StdioServerTransport());
|
|
234
|
+
}
|
|
235
|
+
|
|
135
236
|
const HELP = `clipbait — AI clip generation
|
|
136
237
|
|
|
137
238
|
npx clipbait@latest set up Claude Code (recommended)
|
|
239
|
+
clipbait mcp run the stdio MCP server (for Claude Desktop/Cursor/Cline)
|
|
138
240
|
clipbait login <apiKey> just save your API key
|
|
139
241
|
clipbait generate <url> [--aspect 9:16] [--type talking_head] [--max 9]
|
|
140
242
|
clipbait status <jobId> check a job + get clip URLs
|
|
@@ -159,6 +261,7 @@ Auth: CLIPBAIT_API_KEY env var or ~/.clipbait.json (via 'clipbait login').`;
|
|
|
159
261
|
console.log("✓ Saved API key to " + CFG);
|
|
160
262
|
return;
|
|
161
263
|
}
|
|
264
|
+
if (cmd === "mcp") { await runMcpServer(); return; } // stdio MCP server for local clients
|
|
162
265
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") { console.log(HELP); return; }
|
|
163
266
|
|
|
164
267
|
if (!loadKey()) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clipbait",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Clipbait CLI — turn any video into viral clips, and auto-clip live streams, from your terminal or AI agent.",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Clipbait CLI + MCP server — turn any video into viral clips, and auto-clip live streams, from your terminal or any AI agent.",
|
|
5
5
|
"bin": { "clipbait": "index.js" },
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"engines": { "node": ">=18" },
|
|
8
|
-
"keywords": ["clipbait", "clips", "video", "ai", "agent", "cli", "twitch", "shorts"],
|
|
8
|
+
"keywords": ["clipbait", "clips", "video", "ai", "agent", "mcp", "cli", "twitch", "shorts"],
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
11
|
+
"zod": "^3.25.0"
|
|
12
|
+
},
|
|
9
13
|
"license": "MIT"
|
|
10
14
|
}
|