@viralspin/mcp 0.3.1 → 0.4.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ViralSpin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ViralSpin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,79 +1,79 @@
1
- # ViralSpin MCP — the video editor your AI agent operates
2
-
3
- [![MCP Registry](https://img.shields.io/badge/MCP-registry-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=viralspin)
4
- [![npm](https://img.shields.io/badge/npm-%40viralspin%2Fmcp-red)](https://www.npmjs.com/package/@viralspin/mcp)
5
- [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
6
-
7
- Turn a talking-head clip + a folder of B-roll into a polished vertical short — and **let Claude do the edit**. Point it at your footage in plain English; it transcribes, picks B-roll cutaways matched to what's said, adds captions + a hook, and renders a 9:16 short. Then publishes to TikTok, Instagram, or YouTube. No timeline-wrangling required.
8
-
9
- > **The agent operates a real timeline.** Swap a clip, trim it, reorder scenes, cut a span — all by asking. Edits are instant; you render once when the cut is right.
10
-
11
- ![demo](docs/demo.gif) <!-- TODO: the split-screen "Claude edits, clips stream onto the timeline" clip -->
12
-
13
- ## Quick start
14
-
15
- ```bash
16
- # Claude Code
17
- claude mcp add viralspin -- npx -y @viralspin/mcp
18
-
19
- # or Claude Desktop — add to claude_desktop_config.json:
20
- {
21
- "mcpServers": {
22
- "viralspin": { "command": "npx", "args": ["-y", "@viralspin/mcp"] }
23
- }
24
- }
25
- ```
26
-
27
- Then just ask:
28
-
29
- ```
30
- Turn my A-roll at C:/clips/talk.mp4 into a 9:16 short, using B-roll
31
- from C:/my-broll and my Google Photos.
32
-
33
- Publish that to TikTok and Instagram with the caption: posting through the chaos.
34
- ```
35
-
36
- ## Render a reel on your own computer (`render_locally`)
37
-
38
- ViralSpin plans the reel and hands over the media; **your Mac or PC does the draw**, nothing is published, 0 credits.
39
-
40
- 1. Install Node 20+ (macOS: `brew install node` - Windows: `winget install OpenJS.NodeJS.LTS`) and keep 8 GB of disk free.
41
- 2. Add the server to Claude Desktop (`claude_desktop_config.json`; on Windows use `"command": "npx.cmd"` if `npx` is not found):
42
- `"viralspin": { "command": "npx", "args": ["-y", "@viralspin/mcp"] }` - no tokens to paste.
43
- 3. Connect once: in a terminal run `npx -y @viralspin/mcp login` (your browser opens, you click **Authorize**). Skipping this is fine too: the first time you ask Claude to render, the browser opens by itself. Credentials live in `~/.viralspin/oauth.json` (Windows: `%USERPROFILE%\.viralspin`), readable only by you, and renew themselves silently for about 30 days of use. `npx -y @viralspin/mcp whoami` / `logout` show / remove them; nothing ever prints a token.
44
- 4. In the ViralSpin app: open a reel's **style picker**, choose **On my computer**, tap **Send to my computer**.
45
- 5. In Claude Desktop say: *"Render my ViralSpin reel"* - with no arguments it picks your newest waiting reel and says which. The first run downloads a ~0.5 GB kit into `~/.viralspin/kit` and Chrome Headless Shell once. The finished reel is uploaded back and shows up in the app; a copy stays in `~/.viralspin/jobs/<id>/`.
46
-
47
- Tools: `render_locally` (no arguments, or `job_id`, `render_id`, `request`, `concurrency`, `dry_run`, `complete`, `upload`, `background`) and `render_locally_status` (no arguments lists your waiting / rendering reels).
48
- If the Chrome download is blocked by antivirus or a proxy the error says so and names the manual step.
49
-
50
- ## What the agent can do
51
-
52
- | Tool | What it does |
53
- |---|---|
54
- | `create_video_assembly` | A-roll + B-roll (local files, a folder, URLs, or Google Photos) → a rendered vertical short. Local or hosted. |
55
- | `suggest_clips` / `swap_clip` | See the cutaways; swap one for better-fitting B-roll (with a `reason`). |
56
- | `trim_clip` / `remove_span` / `reorder_scenes` | Granular timeline edits — instant, no re-render. |
57
- | `render_project` | Bake captions/overlays into the final MP4 (the one render). |
58
- | `publish_now` / `schedule_post` / `send_to_drafts` | Post to TikTok / Instagram / YouTube. |
59
- | `generate_caption`, `create_kinetic_text`, carousels, image ads | The rest of the content pipeline. |
60
-
61
- Edits are **instant and free** (they patch the timeline); `render_project` is the single cloud render. Iterate the cut as much as you like, then finalize once.
62
-
63
- ## Open source
64
-
65
- The MCP server, the editor, and the agent verbs are **MIT** — use them free. Cloud generation (AI B-roll, rendering, publishing) runs on ViralSpin's infrastructure and is metered as credits; sign in at [viralspin.ai](https://www.viralspin.ai). You never paste an API key for the AI — the heavy lifting is server-side.
66
-
67
- - Listed in the official **MCP registry**: `ai.viralspin/viralspin`
68
- - Hosted connector (no install, for URL / Google Photos workflows): `https://mcp.viralspin.ai/mcp`
69
-
70
- ## Local vs hosted
71
-
72
- - **Hosted (the default):** the cloud pipeline does the heavy lifting — assembly, AI B-roll, rendering — and every edit opens in the web Swap & Refine editor. B-roll from Google Photos / URLs / your synced library. Needs a [viralspin.ai](https://www.viralspin.ai) account (generation is metered as credits); no API keys to paste. The `mcp.viralspin.ai/mcp` hosted connector is the same, zero-install.
73
- - **Local (advanced):** point `VIRALSPIN_ASSEMBLY_DIR` at a local engine checkout to read your disk — index a folder of clips, select B-roll contextually, and render locally. The npm package ships without the engine, so the local-render tools route you to the hosted path until that env var is set.
74
-
75
- Either way, the agent reads local files *to upload them* (A-roll, a B-roll folder); only on-disk rendering needs the local engine.
76
-
77
- ## License
78
-
79
- MIT — see [LICENSE](./LICENSE).
1
+ # ViralSpin MCP — the video editor your AI agent operates
2
+
3
+ [![MCP Registry](https://img.shields.io/badge/MCP-registry-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=viralspin)
4
+ [![npm](https://img.shields.io/badge/npm-%40viralspin%2Fmcp-red)](https://www.npmjs.com/package/@viralspin/mcp)
5
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
6
+
7
+ Turn a talking-head clip + a folder of B-roll into a polished vertical short — and **let Claude do the edit**. Point it at your footage in plain English; it transcribes, picks B-roll cutaways matched to what's said, adds captions + a hook, and renders a 9:16 short. Then publishes to TikTok, Instagram, or YouTube. No timeline-wrangling required.
8
+
9
+ > **The agent operates a real timeline.** Swap a clip, trim it, reorder scenes, cut a span — all by asking. Edits are instant; you render once when the cut is right.
10
+
11
+ ![demo](docs/demo.gif) <!-- TODO: the split-screen "Claude edits, clips stream onto the timeline" clip -->
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ # Claude Code
17
+ claude mcp add viralspin -- npx -y @viralspin/mcp
18
+
19
+ # or Claude Desktop — add to claude_desktop_config.json:
20
+ {
21
+ "mcpServers": {
22
+ "viralspin": { "command": "npx", "args": ["-y", "@viralspin/mcp"] }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Then just ask:
28
+
29
+ ```
30
+ Turn my A-roll at C:/clips/talk.mp4 into a 9:16 short, using B-roll
31
+ from C:/my-broll and my Google Photos.
32
+
33
+ Publish that to TikTok and Instagram with the caption: posting through the chaos.
34
+ ```
35
+
36
+ ## Render a reel on your own computer (`render_locally`)
37
+
38
+ ViralSpin plans the reel and hands over the media; **your Mac or PC does the draw**, nothing is published, 0 credits.
39
+
40
+ 1. Install Node 20+ (macOS: `brew install node` - Windows: `winget install OpenJS.NodeJS.LTS`) and keep 8 GB of disk free.
41
+ 2. Add the server to Claude Desktop (`claude_desktop_config.json`; on Windows use `"command": "npx.cmd"` if `npx` is not found):
42
+ `"viralspin": { "command": "npx", "args": ["-y", "@viralspin/mcp"] }` - no tokens to paste.
43
+ 3. Connect once: in a terminal run `npx -y @viralspin/mcp login` (your browser opens, you click **Authorize**). Skipping this is fine too: the first time you ask Claude to render, the browser opens by itself. Credentials live in `~/.viralspin/oauth.json` (Windows: `%USERPROFILE%\.viralspin`), readable only by you, and renew themselves silently for about 30 days of use. `npx -y @viralspin/mcp whoami` / `logout` show / remove them; nothing ever prints a token.
44
+ 4. In the ViralSpin app: open a reel's **style picker**, choose **On my computer**, tap **Send to my computer**.
45
+ 5. In Claude Desktop say: *"Render my ViralSpin reel"* - with no arguments it picks your newest waiting reel and says which. The first run downloads a ~0.5 GB kit into `~/.viralspin/kit` and Chrome Headless Shell once. The finished reel is uploaded back and shows up in the app; a copy stays in `~/.viralspin/jobs/<id>/`.
46
+
47
+ Tools: `render_locally` (no arguments, or `job_id`, `render_id`, `request`, `concurrency`, `dry_run`, `complete`, `upload`, `background`) and `render_locally_status` (no arguments lists your waiting / rendering reels).
48
+ If the Chrome download is blocked by antivirus or a proxy the error says so and names the manual step.
49
+
50
+ ## What the agent can do
51
+
52
+ | Tool | What it does |
53
+ |---|---|
54
+ | `create_video_assembly` | A-roll + B-roll (local files, a folder, URLs, or Google Photos) → a rendered vertical short. Local or hosted. |
55
+ | `suggest_clips` / `swap_clip` | See the cutaways; swap one for better-fitting B-roll (with a `reason`). |
56
+ | `trim_clip` / `remove_span` / `reorder_scenes` | Granular timeline edits — instant, no re-render. |
57
+ | `render_project` | Bake captions/overlays into the final MP4 (the one render). |
58
+ | `publish_now` / `schedule_post` / `send_to_drafts` | Post to TikTok / Instagram / YouTube. |
59
+ | `generate_caption`, `create_kinetic_text`, carousels, image ads | The rest of the content pipeline. |
60
+
61
+ Edits are **instant and free** (they patch the timeline); `render_project` is the single cloud render. Iterate the cut as much as you like, then finalize once.
62
+
63
+ ## Open source
64
+
65
+ The MCP server, the editor, and the agent verbs are **MIT** — use them free. Cloud generation (AI B-roll, rendering, publishing) runs on ViralSpin's infrastructure and is metered as credits; sign in at [viralspin.ai](https://www.viralspin.ai). You never paste an API key for the AI — the heavy lifting is server-side.
66
+
67
+ - Listed in the official **MCP registry**: `ai.viralspin/viralspin`
68
+ - Hosted connector (no install, for URL / Google Photos workflows): `https://mcp.viralspin.ai/mcp`
69
+
70
+ ## Local vs hosted
71
+
72
+ - **Hosted (the default):** the cloud pipeline does the heavy lifting — assembly, AI B-roll, rendering — and every edit opens in the web Swap & Refine editor. B-roll from Google Photos / URLs / your synced library. Needs a [viralspin.ai](https://www.viralspin.ai) account (generation is metered as credits); no API keys to paste. The `mcp.viralspin.ai/mcp` hosted connector is the same, zero-install.
73
+ - **Local (advanced):** point `VIRALSPIN_ASSEMBLY_DIR` at a local engine checkout to read your disk — index a folder of clips, select B-roll contextually, and render locally. The npm package ships without the engine, so the local-render tools route you to the hosted path until that env var is set.
74
+
75
+ Either way, the agent reads local files *to upload them* (A-roll, a B-roll folder); only on-disk rendering needs the local engine.
76
+
77
+ ## License
78
+
79
+ MIT — see [LICENSE](./LICENSE).
package/dist/cli.js CHANGED
@@ -1,16 +1,25 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.runCli = runCli;
7
+ const os_1 = __importDefault(require("os"));
4
8
  const client_js_1 = require("./client.js");
5
9
  const tokens_js_1 = require("./local-render/tokens.js");
6
10
  const login_js_1 = require("./local-render/login.js");
11
+ const render_js_1 = require("./local-render/render.js");
12
+ const watch_js_1 = require("./local-render/watch.js");
13
+ const startup_js_1 = require("./local-render/startup.js");
14
+ const render_locally_js_1 = require("./tools/render-locally.js");
15
+ const WATCH_VERSION = "watch-1";
7
16
  /**
8
17
  * `viralspin-mcp login | logout | whoami` - run from a terminal (not by Claude). Returns true when it handled the command,
9
18
  * so index.ts knows not to start the MCP server. Never prints a token.
10
19
  */
11
20
  async function runCli(argv, out = (s) => process.stdout.write(s + "\n")) {
12
21
  const cmd = argv[0];
13
- if (cmd !== "login" && cmd !== "logout" && cmd !== "whoami" && cmd !== "--help" && cmd !== "help")
22
+ if (cmd !== "login" && cmd !== "logout" && cmd !== "whoami" && cmd !== "watch" && cmd !== "--help" && cmd !== "help")
14
23
  return false;
15
24
  if (cmd === "login") {
16
25
  try {
@@ -22,6 +31,34 @@ async function runCli(argv, out = (s) => process.stdout.write(s + "\n")) {
22
31
  process.exitCode = 1;
23
32
  }
24
33
  }
34
+ else if (cmd === "watch") {
35
+ if (argv.includes("--install")) {
36
+ out((0, startup_js_1.installStartup)());
37
+ return true;
38
+ }
39
+ if (argv.includes("--uninstall")) {
40
+ out((0, startup_js_1.uninstallStartup)());
41
+ return true;
42
+ }
43
+ try {
44
+ const tokens = (0, render_locally_js_1.tm)();
45
+ await tokens.getAccessToken(); // signs in (opens the browser once) when this computer is not connected yet
46
+ const ac = new AbortController();
47
+ process.once("SIGINT", () => { out("Stopping."); ac.abort(); });
48
+ await (0, watch_js_1.watchLoop)({
49
+ list: () => (0, render_js_1.listPendingJobs)({ apiBase: client_js_1.PY_API_URL, tokens }),
50
+ render: async (jobId) => { await (0, render_js_1.renderLocally)({ apiBase: client_js_1.PY_API_URL, tokens, job_id: jobId, complete: true, upload: true, log: out }); },
51
+ ping: async () => { await tokens.request("POST", `${client_js_1.PY_API_URL}/v1/reel/local-agent/ping`, { version: WATCH_VERSION, host: os_1.default.hostname(), platform: process.platform }); },
52
+ log: out,
53
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
54
+ now: () => Date.now(),
55
+ }, { signal: ac.signal });
56
+ }
57
+ catch (e) {
58
+ out(`The connector stopped: ${e.message}`);
59
+ process.exitCode = 1;
60
+ }
61
+ }
25
62
  else if (cmd === "logout") {
26
63
  out((0, login_js_1.logout)() ? `Signed out (removed ${(0, tokens_js_1.defaultTokenFile)()}).` : "You were not signed in on this computer.");
27
64
  }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.defaultEnv = void 0;
7
+ exports.startupCommand = startupCommand;
8
+ exports.plistFor = plistFor;
9
+ exports.installStartup = installStartup;
10
+ exports.uninstallStartup = uninstallStartup;
11
+ const child_process_1 = require("child_process");
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ /**
16
+ * Start the connector when this person signs in to their computer (`viralspin-mcp watch --install`), so "Render on my computer" just works.
17
+ * Windows: a Run key under HKEY_CURRENT_USER (no admin). macOS: a LaunchAgent plist (written, not loaded: it starts at the next sign-in, like
18
+ * the Windows key, so a second copy never collides with one already running). Other systems: said plainly, nothing is changed.
19
+ */
20
+ const NAME = "ViralSpinConnector";
21
+ const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
22
+ const defaultEnv = () => ({ platform: process.platform, node: process.execPath, script: process.argv[1] ?? "", home: os_1.default.homedir() });
23
+ exports.defaultEnv = defaultEnv;
24
+ /** The command line the system runs at sign-in. Pure so it is tested. */
25
+ function startupCommand(e) {
26
+ return `"${e.node}" "${e.script}" watch`;
27
+ }
28
+ function plistFor(e) {
29
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30
+ return `<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0"><dict>
33
+ <key>Label</key><string>ai.viralspin.connector</string>
34
+ <key>ProgramArguments</key><array><string>${esc(e.node)}</string><string>${esc(e.script)}</string><string>watch</string></array>
35
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
36
+ <key>StandardOutPath</key><string>${esc(path_1.default.join(e.home, ".viralspin", "watch.log"))}</string>
37
+ <key>StandardErrorPath</key><string>${esc(path_1.default.join(e.home, ".viralspin", "watch.log"))}</string>
38
+ </dict></plist>
39
+ `;
40
+ }
41
+ const plistPath = (e) => path_1.default.join(e.home, "Library", "LaunchAgents", "ai.viralspin.connector.plist");
42
+ function installStartup(e = (0, exports.defaultEnv)(), run = child_process_1.execFileSync) {
43
+ if (!e.script)
44
+ return "Could not tell where this program is installed, so nothing was changed. Install it with: npm i -g @viralspin/mcp";
45
+ if (e.platform === "win32") {
46
+ run("reg", ["add", RUN_KEY, "/v", NAME, "/t", "REG_SZ", "/d", startupCommand(e), "/f"], { stdio: "ignore" });
47
+ return "The connector will start when you sign in to this computer. Start it now with: viralspin-mcp watch";
48
+ }
49
+ if (e.platform === "darwin") {
50
+ const p = plistPath(e);
51
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
52
+ fs_1.default.mkdirSync(path_1.default.join(e.home, ".viralspin"), { recursive: true });
53
+ fs_1.default.writeFileSync(p, plistFor(e));
54
+ return `The connector will start at your next sign-in (${p}). Start it now with: viralspin-mcp watch`;
55
+ }
56
+ return "Start-at-sign-in is set up for Windows and macOS only. On this system, run: viralspin-mcp watch";
57
+ }
58
+ function uninstallStartup(e = (0, exports.defaultEnv)(), run = child_process_1.execFileSync) {
59
+ if (e.platform === "win32") {
60
+ try {
61
+ run("reg", ["delete", RUN_KEY, "/v", NAME, "/f"], { stdio: "ignore" });
62
+ }
63
+ catch { /* it was not there */ }
64
+ return "The connector will no longer start when you sign in.";
65
+ }
66
+ if (e.platform === "darwin") {
67
+ try {
68
+ fs_1.default.unlinkSync(plistPath(e));
69
+ }
70
+ catch { /* it was not there */ }
71
+ return "The connector will no longer start when you sign in.";
72
+ }
73
+ return "Nothing to remove on this system.";
74
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newWatchState = exports.POLL_MS = exports.PING_EVERY_MS = void 0;
4
+ exports.watchTick = watchTick;
5
+ exports.nextDelay = nextDelay;
6
+ exports.watchLoop = watchLoop;
7
+ const render_js_1 = require("./render.js");
8
+ exports.PING_EVERY_MS = 15000;
9
+ exports.POLL_MS = 4000;
10
+ const MAX_BACKOFF_MS = 60000;
11
+ const newWatchState = () => ({ lastPing: 0, failed: new Set(), errors: 0, pingWarned: false });
12
+ exports.newWatchState = newWatchState;
13
+ async function watchTick(d, st) {
14
+ if (d.now() - st.lastPing >= exports.PING_EVERY_MS) {
15
+ st.lastPing = d.now();
16
+ try {
17
+ await d.ping();
18
+ st.pingWarned = false;
19
+ }
20
+ catch (e) {
21
+ if (!st.pingWarned) {
22
+ d.log(`[watch] could not tell ViralSpin this computer is online (${e.message}); will keep trying`);
23
+ st.pingWarned = true;
24
+ }
25
+ }
26
+ }
27
+ let jobs;
28
+ try {
29
+ jobs = await d.list();
30
+ st.errors = 0;
31
+ }
32
+ catch (e) {
33
+ st.errors++;
34
+ d.log(`[watch] could not ask ViralSpin for jobs (${e.message})`);
35
+ return { did: "error" };
36
+ }
37
+ const { job } = (0, render_js_1.pickJob)(jobs.filter((j) => !st.failed.has(j.job_id)));
38
+ // a job ViralSpin is still preparing is waited for by the render itself (it polls); starting it now only means this computer holds it from here
39
+ if (!job)
40
+ return { did: "idle" };
41
+ const name = job.title ? `"${job.title}"` : job.job_id.slice(0, 8);
42
+ d.log(`[watch] drawing ${name}${job.duration_sec ? ` (${Math.round(Number(job.duration_sec))} s)` : ""}`);
43
+ try {
44
+ await d.render(job.job_id);
45
+ d.log(`[watch] finished ${name}`);
46
+ return { did: "rendered", jobId: job.job_id };
47
+ }
48
+ catch (e) {
49
+ st.failed.add(job.job_id);
50
+ d.log(`[watch] ${name} failed on this computer: ${e.message}`);
51
+ return { did: "failed", jobId: job.job_id };
52
+ }
53
+ }
54
+ /** Wait before the next pass: soon after work, a little longer when idle, much longer while ViralSpin is unreachable. */
55
+ function nextDelay(r, st) {
56
+ if (r.did === "rendered" || r.did === "failed")
57
+ return 500;
58
+ if (r.did === "error")
59
+ return Math.min(MAX_BACKOFF_MS, exports.POLL_MS * 2 ** Math.min(st.errors, 4));
60
+ return exports.POLL_MS;
61
+ }
62
+ async function watchLoop(d, o = {}) {
63
+ const st = (0, exports.newWatchState)();
64
+ d.log("[watch] ViralSpin connector is running. Send a reel to your computer from the app and it draws here. Ctrl+C to stop.");
65
+ while (!o.signal?.aborted) {
66
+ const r = await watchTick(d, st);
67
+ await d.sleep(nextDelay(r, st));
68
+ }
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viralspin/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "ViralSpin MCP server — publish local videos and carousels to TikTok, Instagram, and YouTube from Claude Code",
5
5
  "main": "dist/index.js",
6
6
  "bin": {