hermoso 0.1.178 → 0.1.180

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/mcp/client.mjs CHANGED
@@ -1,7 +1,7 @@
1
- // Tiny fetch wrapper around the Hermoso HTTP API, shared by the MCP server (mcp/tools.mjs) and the CLI (bin/heist.mjs).
1
+ // Tiny fetch wrapper around the Hermoso HTTP API, shared by the MCP server (mcp/tools.mjs) and the CLI (bin/hermoso.mjs).
2
2
  // LOCAL today: no auth needed — the server's local auth adapter resolves the fixed dev account, so requireAuth/
3
- // gateSpend pass. When real auth lands, set HEIST_TOKEN (a Bearer) and the SAME calls become authoritative — no
4
- // changes here. We attach the x-heist-plan / x-heist-user fallbacks the browser also sends, purely for parity;
3
+ // gateSpend pass. Set HERMOSO_TOKEN (a Bearer) and the SAME calls become authoritative — no
4
+ // changes here. We attach the x-heist-plan / x-heist-user headers (legacy wire names the server still reads) the browser also sends, purely for parity;
5
5
  // the server treats them as non-authoritative (identity comes from the verified token / local dev user).
6
6
  import { readFile } from 'node:fs/promises';
7
7
  import path from 'node:path';
@@ -11,14 +11,18 @@ import { AsyncLocalStorage } from 'node:async_hooks';
11
11
  // makes carries THAT caller's bearer (bills their account). stdio keeps using the env token — ctx is simply unset.
12
12
  export const mcpCtx = new AsyncLocalStorage();
13
13
 
14
- export const API_BASE = (process.env.HEIST_API_BASE || 'http://localhost:3000').replace(/\/+$/, '');
15
- const TOKEN = process.env.HEIST_TOKEN || '';
14
+ // ENV NAMES (2026-09-01): HERMOSO_* is the documented prefix (README, `claude mcp add … -e HERMOSO_TOKEN=…`); HEIST_* is the
15
+ // pre-rebrand name still honoured as a fallback. Measured before this fix: a stdio server started with ONLY HERMOSO_TOKEN and
16
+ // HERMOSO_API_BASE fell back to localhost:3000 and could not reach Hermoso — the documented setup did not authenticate.
17
+ // The published package defaults to the hosted API; a self-hoster sets HERMOSO_API_BASE.
18
+ export const API_BASE = ((process.env.HERMOSO_API_BASE ?? process.env.HEIST_API_BASE) || 'https://app.hermoso.ai').replace(/\/+$/, '');
19
+ const TOKEN = (process.env.HERMOSO_TOKEN ?? process.env.HEIST_TOKEN) || '';
16
20
  // PINNED profile, or '' when the caller hasn't pinned one. This MUST stay unset by default: the server resolves
17
21
  // an API key's profile as header > key.keyProfileId > 'default' (adapters/auth/middleware.js), so a client
18
22
  // that ALWAYS sends the header permanently masks the brand `use_brand` saved against the key. Live 2026-07-27:
19
23
  // use_brand reported "Now acting on Hermoso", and every connector tool still answered for the default brand —
20
24
  // so Meta/Google Ads/YouTube/OneDrive all looked disconnected over MCP while being connected in the web app.
21
- export const PROFILE = process.env.HEIST_PROFILE || '';
25
+ export const PROFILE = (process.env.HERMOSO_PROFILE ?? process.env.HEIST_PROFILE) || '';
22
26
  // SHARED TEAM WORKSPACE: the OWNING account. The web client sends this as x-hermoso-owner from PROFILE_OWNER
23
27
  // (public/app.js ctxHeaders) whenever the active brand belongs to someone else's account; the MCP twins never did,
24
28
  // so a member driving Hermoso headlessly resolved every brand-scoped read against their OWN empty account —
@@ -29,10 +33,10 @@ export const PROFILE = process.env.HEIST_PROFILE || '';
29
33
  // default — sending an owner for your own account would make resolveWs take the shared branch against yourself.
30
34
  // PAIR IT WITH THE PROFILE UUID, not the slug: profile_members keys on profiles.id, so a client_slug is the one
31
35
  // thing isMember() cannot match and it 403s. list_brands names both values for every workspace you can enter.
32
- export const OWNER = process.env.HEIST_OWNER || '';
36
+ export const OWNER = (process.env.HERMOSO_OWNER ?? process.env.HEIST_OWNER) || '';
33
37
  // The env-var prefix THIS build reads. tools.mjs is byte-identical across the two twins, so it cannot
34
38
  // hardcode either name when it tells a user which variables to set — it asks its own client.
35
- export const ENV_PREFIX = 'HEIST';
39
+ export const ENV_PREFIX = 'HERMOSO';
36
40
 
37
41
  // WHICH TOOL IS RUNNING. The error ledger groups on the OP, and a path alone cannot name the tool: `plan_ad`,
38
42
  // `render_ad` and `make_template_ad` all fail through POST /api/create, so without this every MCP defect would be
@@ -44,7 +48,7 @@ export const toolCtx = new AsyncLocalStorage();
44
48
  function headers(extra = {}) {
45
49
  const ctx = mcpCtx.getStore();
46
50
  // A HOSTED-CONNECTOR request (mcp/http.mjs) is a DIFFERENT TENANT from the process serving it, so its ctx is the
47
- // ONLY scope it may carry: falling through to this process's HEIST_PROFILE / HEIST_OWNER would scope one
51
+ // ONLY scope it may carry: falling through to this process's HERMOSO_PROFILE / HERMOSO_OWNER would scope one
48
52
  // customer's tool call to whatever workspace the SERVER's environment happens to name — a cross-tenant leak that
49
53
  // is invisible because it succeeds. stdio/CLI keeps the env fallback: there the process and the caller are the
50
54
  // same person. Presence of the ctx store IS "remote" (see isRemote below).
@@ -304,7 +308,7 @@ export async function pollJob(id, { intervalMs = 3000, timeoutMs = 10 * 60 * 100
304
308
  onTick?.(job);
305
309
  if (job.status === 'done') return { job, result: jobResult(job) };
306
310
  if (job.status === 'error') throw new Error(job.error || 'Render failed');
307
- if (Date.now() > deadline) throw Object.assign(new Error('Render timed out — check `heist jobs get ' + id + '`'), { jobId: id });
311
+ if (Date.now() > deadline) throw Object.assign(new Error('Render timed out — check `hermoso jobs get ' + id + '`'), { jobId: id });
308
312
  await new Promise(r => setTimeout(r, intervalMs));
309
313
  }
310
314
  }
@@ -2,8 +2,8 @@
2
2
  // Hermoso MCP server (stdio transport) — lets Claude Code / Cursor / Codex (and any stdio MCP client) drive Hermoso:
3
3
  // research competitors, plan ads, and generate images/videos/avatars, all against the running Hermoso server.
4
4
  //
5
- // Local (today): node mcp/hermoso-mcp.mjs # talks to http://localhost:3000 (HEIST_API_BASE to override)
6
- // Auth (today): none — the local server resolves the dev account. Set HEIST_TOKEN once real auth lands.
5
+ // Local (today): node mcp/hermoso-mcp.mjs # talks to https://app.hermoso.ai (HERMOSO_API_BASE to override)
6
+ // Auth (today): none — the local server resolves the dev account. Set HERMOSO_TOKEN (an agent key from the app).
7
7
  //
8
8
  // stdout is the JSON-RPC channel — NEVER print to it. All logging goes to stderr (console.error).
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -23,7 +23,7 @@ const server = new McpServer({ name: 'hermoso-mcp', version: '1.0.0' }, {
23
23
  // no reconnect. HERMOSO_TOOLS=all restores the full roster; HERMOSO_TOOLS=create,channels narrows it further.
24
24
  // An unknown group EXITS rather than silently serving all of them — a scoped connection you did not get is
25
25
  // worse than one you were told you could not have.
26
- // Both env names are read: HERMOSO_TOOLS is the current prefix, HEIST_TOOLS the pre-rebrand one that is live in
26
+ // Both env names are read: HERMOSO_TOOLS is the current prefix, HEIST_TOOLS the pre-rebrand name that is live in
27
27
  // people's configs today. Renaming a variable someone already set is how a working setup goes quiet.
28
28
  const _scope = parseToolScope(process.env.HERMOSO_TOOLS || process.env.HEIST_TOOLS);
29
29
  if (_scope.error) { console.error(`[hermoso-mcp] ${_scope.error}`); process.exit(1); }
package/mcp/http.mjs CHANGED
@@ -4,11 +4,11 @@
4
4
  //
5
5
  // It is written so the cloud step is a CONFIG FLIP, not a rewrite — but it is intentionally OFF and will REFUSE
6
6
  // to mount until BOTH are true:
7
- // (1) HEIST_MCP_REMOTE=1, and
7
+ // (1) HERMOSO_MCP_REMOTE=1 (or the legacy HEIST_MCP_REMOTE), and
8
8
  // (2) a real token verifier is wired (verifyBearer) — i.e. Firebase Auth (or equivalent) is configured.
9
9
  // Why it must stay off locally: a public money-spending endpoint cannot exist without authenticated identity
10
10
  // (the no-anon-spend rule), there is no hosted origin yet, and per the rollout plan cloud is provisioned
11
- // COLLABORATIVELY, never solo. Until then, use the local stdio server (mcp/heist-mcp.mjs) + the CLI + skills.
11
+ // COLLABORATIVELY, never solo. Until then, use the local stdio server (mcp/hermoso-mcp.mjs) + the CLI + skills.
12
12
  //
13
13
  // When the cloud step happens, the remaining work is small and explicit (see ENABLE CHECKLIST at the bottom).
14
14
  // ───────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -21,12 +21,12 @@ import { mcpCtx, connectedProviders } from './client.mjs';
21
21
  // Mount the remote connector onto the Express app. No-op unless explicitly enabled + auth-backed.
22
22
  // `verifyBearer(token) -> {userId, accountId, email} | null` MUST be supplied by the caller (the real auth seam).
23
23
  export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
24
- if (process.env.HEIST_MCP_REMOTE !== '1') return false; // gate 1: off by default
24
+ if ((process.env.HERMOSO_MCP_REMOTE ?? process.env.HEIST_MCP_REMOTE) !== '1') return false; // gate 1: off by default
25
25
  if (typeof verifyBearer !== 'function') { // gate 2: refuse without real auth
26
26
  console.error('[mcp-remote] REFUSING to mount: no token verifier wired. A remote, money-spending MCP must authenticate every caller (no-anon-spend). Wire Firebase Auth → verifyBearer first.');
27
27
  return false;
28
28
  }
29
- const BASE = (publicBaseUrl || process.env.HEIST_PUBLIC_URL || '').replace(/\/+$/, '');
29
+ const BASE = (publicBaseUrl || process.env.HERMOSO_PUBLIC_URL || process.env.HEIST_PUBLIC_URL || '').replace(/\/+$/, '');
30
30
 
31
31
  // RFC 9728 protected-resource metadata — tells Claude.ai where to get a token. (Authorization-server metadata
32
32
  // is served by the auth provider itself, e.g. Firebase/your IdP.) Scopes match the AS metadata + minted token
@@ -56,7 +56,7 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
56
56
  const MCP_PATH = '/mcp'; // the resource's path component — app.all(MCP_PATH) below
57
57
  const protectedResourceMetadata = (req, res) => res.json({
58
58
  resource: `${BASE}${MCP_PATH}`,
59
- authorization_servers: [process.env.HEIST_OAUTH_ISSUER].filter(Boolean),
59
+ authorization_servers: [process.env.HERMOSO_OAUTH_ISSUER || process.env.HEIST_OAUTH_ISSUER].filter(Boolean),
60
60
  scopes_supported: ['hermoso.research', 'hermoso.generate'],
61
61
  bearer_methods_supported: ['header'],
62
62
  });
@@ -293,7 +293,7 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
293
293
  }
294
294
  // The caller's bearer rides into every /api call the tools make — spend bills THEIR account. `remote: true`
295
295
  // says what this store IS: a per-request tenant scope on a shared, multi-tenant process. client.mjs treats the
296
- // presence of this store as the signal to STOP falling back to the process's own HEIST_PROFILE / HEIST_OWNER,
296
+ // presence of this store as the signal to STOP falling back to the process's own HERMOSO_PROFILE / HERMOSO_OWNER,
297
297
  // which belong to whoever runs the box, not to whoever is calling.
298
298
  //
299
299
  // NOTE WHAT IS DELIBERATELY *NOT* HERE: a profile or an owner read off the request. There is nowhere honest to
@@ -304,14 +304,14 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
304
304
  await mcpCtx.run({ token, remote: true, client: entry.client || '' }, () => entry.transport.handleRequest(req, res, req.body));
305
305
  });
306
306
 
307
- console.error(`[mcp-remote] mounted at ${BASE || '(set HEIST_PUBLIC_URL)'}/mcp`);
307
+ console.error(`[mcp-remote] mounted at ${BASE || '(set HERMOSO_PUBLIC_URL)'}/mcp`);
308
308
  return true;
309
309
  }
310
310
 
311
311
  // ── ENABLE CHECKLIST (cloud step, collaborative) ──────────────────────────────────────────────────────────
312
- // 1. Provision a hosted origin (Cloud Run) + Firebase Auth; set HEIST_PUBLIC_URL + HEIST_OAUTH_ISSUER.
312
+ // 1. Provision a hosted origin (Cloud Run) + Firebase Auth; set HERMOSO_PUBLIC_URL + HERMOSO_OAUTH_ISSUER.
313
313
  // 2. Implement verifyBearer(token) via the Firebase auth adapter (adapters/auth/firebase.js) and pass it here.
314
314
  // 3. Thread the authenticated user into mcp/client.mjs's outbound /api calls (AsyncLocalStorage) so reserve()/
315
315
  // gateSpend bill the right account — the server-side enforcement is already authoritative once req.user is real.
316
- // 4. Set HEIST_MCP_REMOTE=1. Then in server.js: `import { mountRemoteMcp } from './mcp/http.mjs'; mountRemoteMcp(app, { verifyBearer, publicBaseUrl })`.
317
- // 5. The published connector URL becomes `${HEIST_PUBLIC_URL}/mcp` — paste into Claude.ai → Settings → Connectors.
316
+ // 4. Set HERMOSO_MCP_REMOTE=1. Then in server.js: `import { mountRemoteMcp } from './mcp/http.mjs'; mountRemoteMcp(app, { verifyBearer, publicBaseUrl })`.
317
+ // 5. The published connector URL becomes `${HERMOSO_PUBLIC_URL}/mcp` — paste into Claude.ai → Settings → Connectors.
package/mcp/tools.mjs CHANGED
@@ -16871,7 +16871,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16871
16871
  // motion and the ORIGINAL AUDIO untouched. The plan is one server call; every variant is an ordinary videoedit job.
16872
16872
  server.registerTool('multiply_ad', {
16873
16873
  title: 'Multiply an ad',
16874
- description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant.",
16874
+ description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). Sources up to 15 seconds are edited as they are (a 15s spot works); longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:15}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant.",
16875
16875
  inputSchema: {
16876
16876
  video: z.string().describe('the source video URL'),
16877
16877
  count: z.number().optional().describe('how many variants, 1-12 (default 6)'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.178",
3
+ "version": "0.1.180",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 745 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
6
6
  "type": "module",