@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 +25 -1
- package/dist/index.js +89 -8
- package/package.json +3 -4
- package/src/client.ts +3 -3
- package/src/index.ts +9 -1
- package/src/login.ts +19 -17
- package/dist/login.js +0 -377
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
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:
|
|
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
|
-
});
|