@hubfluencer/mcp 0.1.0 → 0.2.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
@@ -12,6 +12,11 @@ auth is an opaque bearer token passed through from an env var.
12
12
  No install step — run it on demand with `npx` (commands below). For local development,
13
13
  build from source instead: see [Develop](#develop).
14
14
 
15
+ > **Run these in a normal terminal** (Terminal, iTerm, your IDE's terminal) — **not inside a
16
+ > running Claude Code / agent session.** A nested `claude` forwards its arguments to the model
17
+ > as a prompt instead of executing the `mcp add` subcommand, so the server is never registered
18
+ > (you'll later see `/mcp` → *Failed to reconnect: -32000*). See [Troubleshooting](#troubleshooting).
19
+
15
20
  ### 1. Connect (recommended — no copy-paste)
16
21
 
17
22
  Run the device-link login. It prints a URL + short code; you approve it in the signed-in
@@ -19,7 +24,7 @@ Hubfluencer app, and a scoped access token is saved locally to `~/.hubfluencer/c
19
24
  (mode 0600) — the MCP server reads it from there automatically.
20
25
 
21
26
  ```bash
22
- npx -y -p @hubfluencer/mcp hubfluencer-login "Claude Code"
27
+ npx -y @hubfluencer/mcp login "Claude Code"
23
28
  # 1. Open: https://hubfluencer.com/connect?code=ABCD-EFGH
24
29
  # 2. Confirm code: ABCDEFGH
25
30
  # 3. Click Approve. → ✓ Connected.
@@ -31,6 +36,12 @@ Then register the server (no token env needed — it uses the stored credential)
31
36
  claude mcp add hubfluencer -- npx -y @hubfluencer/mcp
32
37
  ```
33
38
 
39
+ Verify it connected:
40
+
41
+ ```bash
42
+ claude mcp list # → hubfluencer: ... ✓ Connected
43
+ ```
44
+
34
45
  ### Alternative: paste an access token
35
46
 
36
47
  Create a token in the app (**Settings → Access tokens**, scoped `video:generate`+`video:read` —
@@ -96,6 +107,15 @@ If it returns `terminal:false`, the render is still going — call `wait_for_com
96
107
  > TikTok/Instagram requires a human-linked social account and is out of scope —
97
108
  > return the MP4 + a suggested caption instead.
98
109
 
110
+ ## Troubleshooting
111
+
112
+ | Symptom | Cause & fix |
113
+ |---|---|
114
+ | `/mcp` → **Failed to reconnect: -32000** | The server process never started — almost always because `claude mcp add` was run **inside a Claude session** (a nested `claude` runs the line as a prompt, not the subcommand), so nothing was registered. Re-run the `claude mcp add` above in a **normal terminal**, then `claude mcp list` to confirm. |
115
+ | `npm error could not determine executable to run` | You're on a cached **0.1.x** (which shipped two bins). 0.2.0+ ships a single bin and resolves cleanly. Force a fresh fetch: `npx --ignore-existing -y @hubfluencer/mcp`. |
116
+ | `npm error 404 … @hubfluencer/mcp` immediately after a release | npm registry/CDN propagation lag (a couple of minutes after publish). Wait and retry — it self-heals. |
117
+ | Tools fail with **"Not connected to Hubfluencer"** | No token resolved. Run `npx -y @hubfluencer/mcp login`, or pass `--env HUBFLUENCER_API_TOKEN=…`. |
118
+
99
119
  ## Develop
100
120
 
101
121
  ```bash
@@ -105,6 +125,10 @@ bun run dev # runs the stdio server (talk to it via an MCP client)
105
125
  bun run build # emits dist/
106
126
  ```
107
127
 
128
+ The package exposes a single bin, `hubfluencer-mcp`: with no args it starts the stdio MCP
129
+ server; `hubfluencer-mcp login [name]` runs the device-link login. This is why
130
+ `npx -y @hubfluencer/mcp` and `npx -y @hubfluencer/mcp login` both work without `-p`.
131
+
108
132
  ## License
109
133
 
110
134
  [MIT](./LICENSE) © Monocursive
package/dist/index.js CHANGED
@@ -11964,7 +11964,7 @@ var require_dist = __commonJS((exports, module) => {
11964
11964
  });
11965
11965
 
11966
11966
  // src/index.ts
11967
- import { writeFile } from "node:fs/promises";
11967
+ import { writeFile as writeFile2 } from "node:fs/promises";
11968
11968
 
11969
11969
  // ../../node_modules/@modelcontextprotocol/sdk/node_modules/zod/v3/helpers/util.js
11970
11970
  var util;
@@ -29569,11 +29569,88 @@ function clientFromEnv() {
29569
29569
  const token = process.env.HUBFLUENCER_API_TOKEN || stored.token;
29570
29570
  const baseUrl = process.env.HUBFLUENCER_BASE_URL || stored.base_url || "https://hubfluencer.com";
29571
29571
  if (!token) {
29572
- throw new Error("Not connected to Hubfluencer. Run `hubfluencer-login` to connect, or set " + "HUBFLUENCER_API_TOKEN (create one in the app: Settings → Access tokens).");
29572
+ throw new Error("Not connected to Hubfluencer. Run `npx -y @hubfluencer/mcp login` to connect, or set " + "HUBFLUENCER_API_TOKEN (create one in the app: Settings → Access tokens).");
29573
29573
  }
29574
29574
  return new HubfluencerClient(baseUrl, token);
29575
29575
  }
29576
29576
 
29577
+ // src/login.ts
29578
+ import { chmod, mkdir, writeFile } from "node:fs/promises";
29579
+ import { dirname } from "node:path";
29580
+ var MAX_POLLS = 120;
29581
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29582
+ async function runLogin(clientName = "Claude Code") {
29583
+ const BASE = (process.env.HUBFLUENCER_BASE_URL || "https://hubfluencer.com").replace(/\/+$/, "").replace(/\/api$/, "");
29584
+ assertSafeBaseUrl(BASE);
29585
+ const startRes = await fetch(`${BASE}/api/agent-link/start`, {
29586
+ method: "POST",
29587
+ headers: { "content-type": "application/json", accept: "application/json" },
29588
+ body: JSON.stringify({
29589
+ client_name: clientName,
29590
+ scopes: ["video:generate", "video:read"]
29591
+ })
29592
+ });
29593
+ if (!startRes.ok) {
29594
+ console.error(`Could not start login (HTTP ${startRes.status}). Check HUBFLUENCER_BASE_URL.`);
29595
+ process.exit(1);
29596
+ }
29597
+ const start = await startRes.json();
29598
+ const url = start.verification_uri_complete || start.verification_uri;
29599
+ console.error(`
29600
+ Connect this agent to Hubfluencer:
29601
+ `);
29602
+ console.error(` 1. Open: ${url}`);
29603
+ console.error(` 2. Confirm code: ${start.user_code}`);
29604
+ console.error(` 3. Click Approve (you'll need to be signed in).
29605
+ `);
29606
+ console.error("Waiting for approval…");
29607
+ let intervalMs = Math.max(2, start.interval ?? 5) * 1000;
29608
+ const MAX_INTERVAL_MS = 30000;
29609
+ for (let i = 0;i < MAX_POLLS; i++) {
29610
+ await sleep(intervalMs);
29611
+ const pollRes = await fetch(`${BASE}/api/agent-link/poll`, {
29612
+ method: "POST",
29613
+ headers: {
29614
+ "content-type": "application/json",
29615
+ accept: "application/json"
29616
+ },
29617
+ body: JSON.stringify({ device_code: start.device_code })
29618
+ });
29619
+ const body = await pollRes.json().catch(() => ({}));
29620
+ if (pollRes.ok && body.status === "approved" && body.token) {
29621
+ await storeToken(body.token, BASE);
29622
+ console.error(`
29623
+ ✓ Connected. Access token saved to ${CREDENTIALS_PATH}.`);
29624
+ console.error(` Revoke anytime in the app: Settings → Access tokens.
29625
+ `);
29626
+ return;
29627
+ }
29628
+ if (body.status === "slow_down") {
29629
+ intervalMs = Math.min(intervalMs + 5000, MAX_INTERVAL_MS);
29630
+ continue;
29631
+ }
29632
+ if (body.status === "pending")
29633
+ continue;
29634
+ console.error(`
29635
+ ✗ ${body.error || body.status || `HTTP ${pollRes.status}`}. Run login again.`);
29636
+ process.exit(1);
29637
+ }
29638
+ console.error(`
29639
+ ✗ Timed out waiting for approval. Run login again.`);
29640
+ process.exit(1);
29641
+ }
29642
+ async function storeToken(token, base) {
29643
+ const dir = dirname(CREDENTIALS_PATH);
29644
+ await mkdir(dir, { recursive: true, mode: 448 });
29645
+ await writeFile(CREDENTIALS_PATH, JSON.stringify({ token, base_url: base }, null, 2), {
29646
+ mode: 384
29647
+ });
29648
+ try {
29649
+ await chmod(CREDENTIALS_PATH, 384);
29650
+ await chmod(dir, 448);
29651
+ } catch {}
29652
+ }
29653
+
29577
29654
  // src/uploads.ts
29578
29655
  import { open, readFile, stat } from "node:fs/promises";
29579
29656
  import { basename, extname, isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
@@ -29786,7 +29863,7 @@ async function downloadTo(videoUrl, savePath) {
29786
29863
  await reader.cancel().catch(() => {});
29787
29864
  }
29788
29865
  const buf = Buffer.concat(chunks, total);
29789
- await writeFile(target, buf);
29866
+ await writeFile2(target, buf);
29790
29867
  return { saved_to: target, bytes: buf.length };
29791
29868
  }
29792
29869
  function ok(payload, links = []) {
@@ -29883,7 +29960,7 @@ function tool(fn) {
29883
29960
  }
29884
29961
  };
29885
29962
  }
29886
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29963
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
29887
29964
  var kindSchema = exports_external.enum(["short", "editor"]).describe("Project kind");
29888
29965
  var RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
29889
29966
  var WRITE = {
@@ -29891,7 +29968,7 @@ var WRITE = {
29891
29968
  destructiveHint: false,
29892
29969
  openWorldHint: true
29893
29970
  };
29894
- var server = new McpServer({ name: "hubfluencer", version: "0.1.0" });
29971
+ var server = new McpServer({ name: "hubfluencer", version: "0.2.0" });
29895
29972
  var registerTool = server.registerTool.bind(server);
29896
29973
  async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
29897
29974
  const deadline = Date.now() + budgetMs;
@@ -29899,7 +29976,7 @@ async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
29899
29976
  let n = 0;
29900
29977
  while (!status.terminal && Date.now() + intervalMs <= deadline) {
29901
29978
  await reportProgress(extra, ++n, `stage: ${status.stage}`);
29902
- await sleep(intervalMs);
29979
+ await sleep2(intervalMs);
29903
29980
  status = await fetchStatus(client, kind, slug);
29904
29981
  }
29905
29982
  return status;
@@ -29921,7 +29998,7 @@ async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, i
29921
29998
  let { status, error: error2 } = await read();
29922
29999
  while (status !== "completed" && status !== "failed" && Date.now() + intervalMs <= deadline) {
29923
30000
  await reportProgress(extra, ++n, `segment ${sid}: ${status}`);
29924
- await sleep(intervalMs);
30001
+ await sleep2(intervalMs);
29925
30002
  ({ status, error: error2 } = await read());
29926
30003
  }
29927
30004
  return {
@@ -30532,7 +30609,7 @@ async function pollUploadToReady(client, slug, uploadId, extra, budgetMs, interv
30532
30609
  let { status, error: error2 } = await read();
30533
30610
  while (status !== "ready" && status !== "failed" && Date.now() + intervalMs <= deadline) {
30534
30611
  await reportProgress(extra, ++n, `upload ${uid}: ${status}`);
30535
- await sleep(intervalMs);
30612
+ await sleep2(intervalMs);
30536
30613
  ({ status, error: error2 } = await read());
30537
30614
  }
30538
30615
  return {
@@ -30780,6 +30857,10 @@ registerTool("download_result", {
30780
30857
  }, link);
30781
30858
  }));
30782
30859
  async function main() {
30860
+ if (process.argv[2] === "login") {
30861
+ await runLogin(process.argv[3]);
30862
+ return;
30863
+ }
30783
30864
  const transport = new StdioServerTransport;
30784
30865
  await server.connect(transport);
30785
30866
  console.error("hubfluencer-mcp running on stdio");
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
5
5
  "license": "MIT",
6
6
  "author": "Monocursive <contact@monocursive.com>",
7
7
  "private": false,
8
8
  "type": "module",
9
9
  "bin": {
10
- "hubfluencer-mcp": "./dist/index.js",
11
- "hubfluencer-login": "./dist/login.js"
10
+ "hubfluencer-mcp": "./dist/index.js"
12
11
  },
13
12
  "main": "./dist/index.js",
14
13
  "files": [
@@ -20,7 +19,7 @@
20
19
  ],
21
20
  "scripts": {
22
21
  "dev": "bun run src/index.ts",
23
- "build": "rm -rf dist && bun build src/index.ts src/login.ts --target node --outdir dist",
22
+ "build": "rm -rf dist && bun build src/index.ts --target node --outdir dist",
24
23
  "prepublishOnly": "bun run build",
25
24
  "start": "node dist/index.js",
26
25
  "test": "bun test",
package/src/client.ts CHANGED
@@ -185,8 +185,8 @@ export class HubfluencerClient {
185
185
 
186
186
  /**
187
187
  * Builds a client from the environment, falling back to the device-link
188
- * credential file (written by `hubfluencer-login`). Throws a clear, actionable
189
- * error if no token is available.
188
+ * credential file (written by `hubfluencer-mcp login`). Throws a clear,
189
+ * actionable error if no token is available.
190
190
  */
191
191
  export function clientFromEnv(): HubfluencerClient {
192
192
  const stored = readStoredCredentials();
@@ -198,7 +198,7 @@ export function clientFromEnv(): HubfluencerClient {
198
198
 
199
199
  if (!token) {
200
200
  throw new Error(
201
- "Not connected to Hubfluencer. Run `hubfluencer-login` to connect, or set " +
201
+ "Not connected to Hubfluencer. Run `npx -y @hubfluencer/mcp login` to connect, or set " +
202
202
  "HUBFLUENCER_API_TOKEN (create one in the app: Settings → Access tokens).",
203
203
  );
204
204
  }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  normalizeStatus,
36
36
  resolveSavePath,
37
37
  } from "./core.js";
38
+ import { runLogin } from "./login.js";
38
39
  import {
39
40
  IMAGE_EXTS,
40
41
  uploadImageFile,
@@ -322,7 +323,7 @@ const WRITE = {
322
323
  openWorldHint: true,
323
324
  };
324
325
 
325
- const server = new McpServer({ name: "hubfluencer", version: "0.1.0" });
326
+ const server = new McpServer({ name: "hubfluencer", version: "0.2.0" });
326
327
 
327
328
  // The SDK's `registerTool` is generic over the Zod input shape; with this many
328
329
  // tools its conditional types hit TS2589 ("excessively deep"). Inputs are still
@@ -2298,6 +2299,13 @@ registerTool(
2298
2299
  // ── Boot ──────────────────────────────────────────────────────────────────────
2299
2300
 
2300
2301
  async function main() {
2302
+ // `hubfluencer-mcp login [name]` runs the device-link login instead of the
2303
+ // server. Single bin keeps the install idiomatic: `npx -y @hubfluencer/mcp`
2304
+ // starts the server; `npx -y @hubfluencer/mcp login` connects an account.
2305
+ if (process.argv[2] === "login") {
2306
+ await runLogin(process.argv[3]);
2307
+ return;
2308
+ }
2301
2309
  const transport = new StdioServerTransport();
2302
2310
  await server.connect(transport);
2303
2311
  // stderr is safe; stdout is reserved for the MCP protocol.
package/src/login.ts CHANGED
@@ -1,6 +1,9 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * `hubfluencer-login` device-link CLI.
2
+ * Device-link login for the Hubfluencer MCP.
3
+ *
4
+ * Exposed as the `login` subcommand of the single `hubfluencer-mcp` bin:
5
+ *
6
+ * npx -y @hubfluencer/mcp login ["Client Name"]
4
7
  *
5
8
  * Connects this agent to a Hubfluencer account without copy-pasting a token:
6
9
  * starts a link request, prints a URL + code for the user to approve in the
@@ -14,17 +17,21 @@ import { dirname } from "node:path";
14
17
  import { assertSafeBaseUrl } from "./client.js";
15
18
  import { CREDENTIALS_PATH } from "./credentials.js";
16
19
 
17
- const BASE = (process.env.HUBFLUENCER_BASE_URL || "https://hubfluencer.com")
18
- .replace(/\/+$/, "")
19
- .replace(/\/api$/, "");
20
- // Fail before printing a code / polling if the base would leak the token.
21
- assertSafeBaseUrl(BASE);
22
20
  const MAX_POLLS = 120;
23
21
 
24
22
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
25
23
 
26
- async function main() {
27
- const clientName = process.argv[2] || "Claude Code";
24
+ /**
25
+ * Runs the device-link login flow. Resolves once the token is stored; calls
26
+ * `process.exit(1)` on a terminal failure (it's a CLI entry path). Invoked from
27
+ * index.ts when the bin is run as `hubfluencer-mcp login [name]`.
28
+ */
29
+ export async function runLogin(clientName = "Claude Code"): Promise<void> {
30
+ const BASE = (process.env.HUBFLUENCER_BASE_URL || "https://hubfluencer.com")
31
+ .replace(/\/+$/, "")
32
+ .replace(/\/api$/, "");
33
+ // Fail before printing a code / polling if the base would leak the token.
34
+ assertSafeBaseUrl(BASE);
28
35
 
29
36
  const startRes = await fetch(`${BASE}/api/agent-link/start`, {
30
37
  method: "POST",
@@ -77,7 +84,7 @@ async function main() {
77
84
  };
78
85
 
79
86
  if (pollRes.ok && body.status === "approved" && body.token) {
80
- await storeToken(body.token);
87
+ await storeToken(body.token, BASE);
81
88
  console.error(
82
89
  `\n✓ Connected. Access token saved to ${CREDENTIALS_PATH}.`,
83
90
  );
@@ -101,12 +108,12 @@ async function main() {
101
108
  process.exit(1);
102
109
  }
103
110
 
104
- async function storeToken(token: string) {
111
+ async function storeToken(token: string, base: string) {
105
112
  const dir = dirname(CREDENTIALS_PATH);
106
113
  await mkdir(dir, { recursive: true, mode: 0o700 });
107
114
  await writeFile(
108
115
  CREDENTIALS_PATH,
109
- JSON.stringify({ token, base_url: BASE }, null, 2),
116
+ JSON.stringify({ token, base_url: base }, null, 2),
110
117
  {
111
118
  mode: 0o600,
112
119
  },
@@ -122,8 +129,3 @@ async function storeToken(token: string) {
122
129
  // chmod is hardening — never fail the login over it
123
130
  }
124
131
  }
125
-
126
- main().catch((e) => {
127
- console.error("Fatal:", e instanceof Error ? e.message : String(e));
128
- process.exit(1);
129
- });
package/dist/login.js DELETED
@@ -1,377 +0,0 @@
1
- #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __getProtoOf = Object.getPrototypeOf;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __toESM = (mod, isNodeMode, target) => {
8
- target = mod != null ? __create(__getProtoOf(mod)) : {};
9
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
- for (let key of __getOwnPropNames(mod))
11
- if (!__hasOwnProp.call(to, key))
12
- __defProp(to, key, {
13
- get: () => mod[key],
14
- enumerable: true
15
- });
16
- return to;
17
- };
18
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
- var __export = (target, all) => {
20
- for (var name in all)
21
- __defProp(target, name, {
22
- get: all[name],
23
- enumerable: true,
24
- configurable: true,
25
- set: (newValue) => all[name] = () => newValue
26
- });
27
- };
28
-
29
- // src/login.ts
30
- import { chmod, mkdir, writeFile } from "node:fs/promises";
31
- import { dirname } from "node:path";
32
-
33
- // src/core.ts
34
- import { createHash } from "node:crypto";
35
- import { isAbsolute, resolve, sep } from "node:path";
36
- function isPrivateOrMetadataHost(hostname) {
37
- let host = hostname.trim().toLowerCase();
38
- if (host.startsWith("[") && host.endsWith("]"))
39
- host = host.slice(1, -1);
40
- const zone = host.indexOf("%");
41
- if (zone !== -1)
42
- host = host.slice(0, zone);
43
- const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
44
- if (v4) {
45
- const o = v4.slice(1).map(Number);
46
- if (o.some((n) => n > 255))
47
- return false;
48
- return isPrivateV4(o[0], o[1]);
49
- }
50
- if (host.includes(":")) {
51
- const mapped = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(host);
52
- if (mapped) {
53
- const o = mapped[1].split(".").map(Number);
54
- if (!o.some((n) => n > 255) && isPrivateV4(o[0], o[1])) {
55
- return true;
56
- }
57
- }
58
- if (host === "::1" || host === "::")
59
- return true;
60
- if (host === "::ffff:0:0")
61
- return true;
62
- if (/^f[cd]/.test(host))
63
- return true;
64
- if (/^fe[89ab]/.test(host))
65
- return true;
66
- return false;
67
- }
68
- return false;
69
- }
70
- function isPrivateV4(a, b) {
71
- if (a === 127)
72
- return true;
73
- if (a === 10)
74
- return true;
75
- if (a === 172 && b >= 16 && b <= 31)
76
- return true;
77
- if (a === 192 && b === 168)
78
- return true;
79
- if (a === 169 && b === 254)
80
- return true;
81
- if (a === 0)
82
- return true;
83
- return false;
84
- }
85
- function assertSafeFetchUrl(url, opts = {}) {
86
- let u;
87
- try {
88
- u = new URL(url);
89
- } catch {
90
- throw new Error(`Invalid Hubfluencer URL: ${url}`);
91
- }
92
- const host = u.hostname;
93
- const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
94
- if (u.protocol === "https:") {
95
- if (isPrivateOrMetadataHost(host)) {
96
- throw new Error(`Refusing to use a Hubfluencer URL pointing at a private/internal host (${host}). ` + "Use the public https endpoint.");
97
- }
98
- return u;
99
- }
100
- if (opts.allowLoopback && isLoopback)
101
- return u;
102
- throw new Error(`Refusing to use an insecure Hubfluencer URL (${u.protocol}//${host}). ` + "Use an https URL (or localhost for local development).");
103
- }
104
- function asRecord(v) {
105
- return v && typeof v === "object" ? v : {};
106
- }
107
- function normalizeStatus(kind, slug, data) {
108
- const d = asRecord(data);
109
- const latest = asRecord(d.latest_render);
110
- const videoUrl = latest.video_url ?? null;
111
- if (kind === "short") {
112
- const stage = d.stage ?? "unknown";
113
- return {
114
- kind,
115
- slug,
116
- stage,
117
- terminal: stage === "video_ready" || stage === "failed",
118
- ready: stage === "video_ready",
119
- video_url: videoUrl,
120
- error: d.error_message ?? null
121
- };
122
- }
123
- const autopilot = d.autopilot_status ?? "unknown";
124
- const renderStatus = latest.status ?? null;
125
- const ready = renderStatus === "completed" && Boolean(videoUrl);
126
- const terminal = ready || autopilot === "failed" || autopilot === "cancelled" || renderStatus === "failed";
127
- return {
128
- kind,
129
- slug,
130
- stage: autopilot,
131
- terminal,
132
- ready,
133
- video_url: videoUrl,
134
- error: d.autopilot_error_message ?? null
135
- };
136
- }
137
- function idemKey(...parts) {
138
- return createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 32);
139
- }
140
- function resolveSavePath(savePath) {
141
- const base = resolve(process.env.HUBFLUENCER_OUTPUT_DIR || process.cwd());
142
- const target = isAbsolute(savePath) ? resolve(savePath) : resolve(base, savePath);
143
- if (!target.toLowerCase().endsWith(".mp4")) {
144
- throw new Error("save_path must end in .mp4");
145
- }
146
- if (target !== base && !target.startsWith(base + sep)) {
147
- throw new Error(`save_path must be inside ${base} (set HUBFLUENCER_OUTPUT_DIR to change). Refusing to write to ${target}.`);
148
- }
149
- return target;
150
- }
151
- function inferKind(prompt) {
152
- const p = prompt.toLowerCase();
153
- const wantsShort = /\b(short|quick|simple|snappy|single[- ]?clip|one[- ]?clip|teaser)\b/.test(p) || /(rapide|simple|court|clip unique|tease?r)/.test(p);
154
- if (wantsShort)
155
- return "short";
156
- const wantsEditor = /\b(ads?|advert|advertisement|commercial|promo|campaign|launch|brand|story|stories|multi[- ]?scene|scenes?|narrat|explainer|chapters?|episodes?|testimonial|showcase|walkthrough|demo|spot)\b/.test(p) || /(pub(licit[ée])?|annonce|campagne|histoire|multi[- ]?sc[eè]ne|sc[eè]nes?|raconte|chapitres?|[eé]pisodes?|explicat|lancement|t[eé]moignage|d[eé]mo|vitrine)/.test(p);
157
- return wantsEditor ? "editor" : "short";
158
- }
159
-
160
- // src/credentials.ts
161
- import { readFileSync, statSync } from "node:fs";
162
- import { homedir } from "node:os";
163
- import { join } from "node:path";
164
- var CREDENTIALS_PATH = process.env.HUBFLUENCER_CREDENTIALS || join(homedir(), ".hubfluencer", "credentials.json");
165
- function readStoredCredentials() {
166
- let raw;
167
- try {
168
- raw = readFileSync(CREDENTIALS_PATH, "utf8");
169
- } catch {
170
- return {};
171
- }
172
- if (process.platform !== "win32") {
173
- try {
174
- const mode = statSync(CREDENTIALS_PATH).mode;
175
- if (mode & 63) {
176
- console.error(`Warning: ${CREDENTIALS_PATH} is accessible to other users ` + `(mode ${(mode & 511).toString(8)}). Run: chmod 600 ${CREDENTIALS_PATH}`);
177
- }
178
- } catch {}
179
- }
180
- try {
181
- return JSON.parse(raw);
182
- } catch {
183
- return {};
184
- }
185
- }
186
-
187
- // src/client.ts
188
- function makeError(status, body) {
189
- let message = `Hubfluencer API error (HTTP ${status})`;
190
- let code;
191
- if (body && typeof body === "object") {
192
- const b = body;
193
- const errVal = b.error;
194
- if (typeof errVal === "string") {
195
- if (/^[a-z][a-z0-9_]*$/.test(errVal)) {
196
- code = errVal;
197
- message = b.message || errVal;
198
- } else {
199
- message = b.message || errVal;
200
- }
201
- } else if (errVal && typeof errVal === "object") {
202
- const e = errVal;
203
- code = e.code || undefined;
204
- message = e.message || message;
205
- } else if (typeof b.message === "string") {
206
- message = b.message;
207
- }
208
- }
209
- const err = new Error(message);
210
- err.status = status;
211
- err.code = code;
212
- err.body = body;
213
- return err;
214
- }
215
- function assertSafeBaseUrl(baseUrl) {
216
- assertSafeFetchUrl(baseUrl, { allowLoopback: true });
217
- }
218
-
219
- class HubfluencerClient {
220
- baseUrl;
221
- token;
222
- constructor(baseUrl, token) {
223
- this.baseUrl = baseUrl;
224
- this.token = token;
225
- this.baseUrl = baseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
226
- assertSafeBaseUrl(this.baseUrl);
227
- }
228
- async request(method, path, opts = {}) {
229
- const url = new URL(`${this.baseUrl}/api${path}`);
230
- if (opts.query) {
231
- for (const [k, v] of Object.entries(opts.query)) {
232
- if (v !== undefined)
233
- url.searchParams.set(k, String(v));
234
- }
235
- }
236
- const headers = {
237
- authorization: `Bearer ${this.token}`,
238
- accept: "application/json"
239
- };
240
- if (opts.body !== undefined)
241
- headers["content-type"] = "application/json";
242
- if (opts.idempotencyKey)
243
- headers["idempotency-key"] = opts.idempotencyKey;
244
- let res;
245
- try {
246
- res = await fetch(url, {
247
- method,
248
- headers,
249
- body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
250
- signal: AbortSignal.timeout(60000)
251
- });
252
- } catch (e) {
253
- if (e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError")) {
254
- throw new Error(`Request to ${method} ${path} timed out after 60s.`);
255
- }
256
- throw e instanceof Error ? new Error(`Network error on ${method} ${path}: ${e.message}`) : e;
257
- }
258
- if (res.status === 204)
259
- return;
260
- const text = await res.text();
261
- let parsed;
262
- if (text) {
263
- try {
264
- parsed = JSON.parse(text);
265
- } catch {
266
- parsed = text;
267
- }
268
- }
269
- if (!res.ok)
270
- throw makeError(res.status, parsed);
271
- return parsed;
272
- }
273
- get(path, query) {
274
- return this.request("GET", path, { query });
275
- }
276
- post(path, body, idempotencyKey) {
277
- return this.request("POST", path, { body, idempotencyKey });
278
- }
279
- patch(path, body, idempotencyKey) {
280
- return this.request("PATCH", path, { body, idempotencyKey });
281
- }
282
- put(path, body, idempotencyKey) {
283
- return this.request("PUT", path, { body, idempotencyKey });
284
- }
285
- del(path) {
286
- return this.request("DELETE", path);
287
- }
288
- }
289
- function clientFromEnv() {
290
- const stored = readStoredCredentials();
291
- const token = process.env.HUBFLUENCER_API_TOKEN || stored.token;
292
- const baseUrl = process.env.HUBFLUENCER_BASE_URL || stored.base_url || "https://hubfluencer.com";
293
- if (!token) {
294
- throw new Error("Not connected to Hubfluencer. Run `hubfluencer-login` to connect, or set " + "HUBFLUENCER_API_TOKEN (create one in the app: Settings → Access tokens).");
295
- }
296
- return new HubfluencerClient(baseUrl, token);
297
- }
298
-
299
- // src/login.ts
300
- var BASE = (process.env.HUBFLUENCER_BASE_URL || "https://hubfluencer.com").replace(/\/+$/, "").replace(/\/api$/, "");
301
- assertSafeBaseUrl(BASE);
302
- var MAX_POLLS = 120;
303
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
304
- async function main() {
305
- const clientName = process.argv[2] || "Claude Code";
306
- const startRes = await fetch(`${BASE}/api/agent-link/start`, {
307
- method: "POST",
308
- headers: { "content-type": "application/json", accept: "application/json" },
309
- body: JSON.stringify({
310
- client_name: clientName,
311
- scopes: ["video:generate", "video:read"]
312
- })
313
- });
314
- if (!startRes.ok) {
315
- console.error(`Could not start login (HTTP ${startRes.status}). Check HUBFLUENCER_BASE_URL.`);
316
- process.exit(1);
317
- }
318
- const start = await startRes.json();
319
- const url = start.verification_uri_complete || start.verification_uri;
320
- console.error(`
321
- Connect this agent to Hubfluencer:
322
- `);
323
- console.error(` 1. Open: ${url}`);
324
- console.error(` 2. Confirm code: ${start.user_code}`);
325
- console.error(` 3. Click Approve (you'll need to be signed in).
326
- `);
327
- console.error("Waiting for approval…");
328
- let intervalMs = Math.max(2, start.interval ?? 5) * 1000;
329
- const MAX_INTERVAL_MS = 30000;
330
- for (let i = 0;i < MAX_POLLS; i++) {
331
- await sleep(intervalMs);
332
- const pollRes = await fetch(`${BASE}/api/agent-link/poll`, {
333
- method: "POST",
334
- headers: {
335
- "content-type": "application/json",
336
- accept: "application/json"
337
- },
338
- body: JSON.stringify({ device_code: start.device_code })
339
- });
340
- const body = await pollRes.json().catch(() => ({}));
341
- if (pollRes.ok && body.status === "approved" && body.token) {
342
- await storeToken(body.token);
343
- console.error(`
344
- ✓ Connected. Access token saved to ${CREDENTIALS_PATH}.`);
345
- console.error(` Revoke anytime in the app: Settings → Access tokens.
346
- `);
347
- return;
348
- }
349
- if (body.status === "slow_down") {
350
- intervalMs = Math.min(intervalMs + 5000, MAX_INTERVAL_MS);
351
- continue;
352
- }
353
- if (body.status === "pending")
354
- continue;
355
- console.error(`
356
- ✗ ${body.error || body.status || `HTTP ${pollRes.status}`}. Run login again.`);
357
- process.exit(1);
358
- }
359
- console.error(`
360
- ✗ Timed out waiting for approval. Run login again.`);
361
- process.exit(1);
362
- }
363
- async function storeToken(token) {
364
- const dir = dirname(CREDENTIALS_PATH);
365
- await mkdir(dir, { recursive: true, mode: 448 });
366
- await writeFile(CREDENTIALS_PATH, JSON.stringify({ token, base_url: BASE }, null, 2), {
367
- mode: 384
368
- });
369
- try {
370
- await chmod(CREDENTIALS_PATH, 384);
371
- await chmod(dir, 448);
372
- } catch {}
373
- }
374
- main().catch((e) => {
375
- console.error("Fatal:", e instanceof Error ? e.message : String(e));
376
- process.exit(1);
377
- });