@hubfluencer/mcp 0.1.0 → 0.3.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 +25 -1
- package/dist/index.js +194 -23
- package/package.json +3 -4
- package/src/client.ts +3 -3
- package/src/index.ts +286 -24
- package/src/login.ts +19 -17
- package/src/uploads.ts +25 -0
- package/dist/login.js +0 -377
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
|
-
});
|