hermoso 0.1.463 → 0.1.465
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 +2 -2
- package/mcp/client.mjs +15 -2
- package/mcp/roster-scope.mjs +23 -0
- package/mcp/tools.mjs +1429 -1397
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate image & video ad
|
|
|
5
5
|
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
6
|
campaigns behind them, all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**1088 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
9
|
catalog with exact per-render credit costs plus the full capability map.
|
|
10
10
|
|
|
11
11
|
**No markup on models.** Stop overpaying markups on AI models. Plans: [hermoso.ai/pricing](https://hermoso.ai/pricing).
|
|
@@ -242,7 +242,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
242
242
|
|
|
243
243
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
244
244
|
|
|
245
|
-
### What the
|
|
245
|
+
### What the 1088 tools cover
|
|
246
246
|
|
|
247
247
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
248
248
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/client.mjs
CHANGED
|
@@ -84,10 +84,23 @@ const scopedBrand = (p) => {
|
|
|
84
84
|
const b = brandScope.getStore()?.brand;
|
|
85
85
|
return typeof b === 'string' && b.trim() && !/^\/api\/apple-ads\//.test(String(p || '')) ? b.trim() : '';
|
|
86
86
|
};
|
|
87
|
+
// ── AN ACCOUNT NAMED ON ONE TOOL CALL SCOPES EVERY REQUEST THAT CALL MAKES (2026-10-29) ──────────────────────────
|
|
88
|
+
// A brand can hold several accounts of one channel (lib/multi-account.mjs), and the server refuses a write that names
|
|
89
|
+
// none: "2 youtube accounts are connected, name one with account: …". `update_youtube_video` had no `account`
|
|
90
|
+
// argument to name one with, so a customer with two channels could not flip a video public (fb_f6f7b0371d42). Same
|
|
91
|
+
// shape as the brand above: wrap() (tools.mjs) puts the caller's `account` in toolCtx for a tool whose provider is a
|
|
92
|
+
// multi-account channel, and it rides here as `?account=`, which the server's connCtxFor reads on every method.
|
|
93
|
+
// A body `account` the handler already sends is read FIRST by the server and is the same value.
|
|
94
|
+
const scopedAccount = () => { const a = toolCtx.getStore()?.account; return typeof a === 'string' && a.trim() ? a.trim() : ''; };
|
|
95
|
+
const withAccountScope = (p) => {
|
|
96
|
+
const a = scopedAccount();
|
|
97
|
+
if (!a || /[?&]account=/.test(String(p))) return p;
|
|
98
|
+
return `${p}${String(p).includes('?') ? '&' : '?'}account=${encodeURIComponent(a)}`;
|
|
99
|
+
};
|
|
87
100
|
export const withBrandScope = (p) => {
|
|
88
101
|
const b = scopedBrand(p);
|
|
89
|
-
if (!b || /[?&]brandId=/.test(String(p))) return p;
|
|
90
|
-
return `${p}${String(p).includes('?') ? '&' : '?'}brandId=${encodeURIComponent(b)}
|
|
102
|
+
if (!b || /[?&]brandId=/.test(String(p))) return withAccountScope(p);
|
|
103
|
+
return withAccountScope(`${p}${String(p).includes('?') ? '&' : '?'}brandId=${encodeURIComponent(b)}`);
|
|
91
104
|
};
|
|
92
105
|
|
|
93
106
|
function headers(extra = {}) {
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
export const NEVER_GATE = new Set([
|
|
53
53
|
'search_meta_ads', 'search_google_ads', 'search_linkedin_ads', 'search_tiktok',
|
|
54
54
|
'search_instagram', 'search_youtube', 'search_reddit', 'search_threads', 'fetch_social_data',
|
|
55
|
+
'search_meta_places', // place search runs on our data provider and a web search, never on a Meta connection (2026-10-27)
|
|
55
56
|
]);
|
|
56
57
|
|
|
57
58
|
// Tool-name → connector provider. ORDERED: the first match wins, so the more specific pattern must come first.
|
|
@@ -127,6 +128,28 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
127
128
|
// in every listing. If Reddit ever approves the app, remove it from this set and the whole surface returns.
|
|
128
129
|
export const UNOFFERED_PROVIDERS = new Set(['reddit', 'snapchat']);
|
|
129
130
|
export function toolUnoffered(name) { const p = toolProvider(name); return !!p && UNOFFERED_PROVIDERS.has(p); }
|
|
131
|
+
|
|
132
|
+
// ── WHICH TOOLS TAKE `account` (2026-10-29, customer bug fb_f6f7b0371d42) ─────────────────────────────────────────────
|
|
133
|
+
// A brand can connect several accounts of these channels (lib/multi-account.mjs), and the server refuses a write that
|
|
134
|
+
// names none. Only the post_to_* tools declared `account`, so a user with two YouTube channels was told "name one with
|
|
135
|
+
// account: …" by update_youtube_video, which had no such argument. Every surface (both MCP twins' registration seam,
|
|
136
|
+
// the Studio agent's schema and executor) asks THIS function, so the answer cannot differ between them.
|
|
137
|
+
// The set mirrors lib/multi-account.mjs MULTI_ACCOUNT_PROVIDERS, which the npm package cannot import;
|
|
138
|
+
// tools/multi-account-arg-check.mjs fails when the two differ, and DERIVES the tool split below from server.js.
|
|
139
|
+
export const MULTI_ACCOUNT_CHANNELS = Object.freeze(new Set(['tiktok', 'x', 'youtube', 'threads', 'bluesky', 'telegram', 'reddit', 'pinterest', 'instagram']));
|
|
140
|
+
// Filed under a multi-account channel, but routed through a connection that holds ONE grant per brand, so `account`
|
|
141
|
+
// would do nothing there: every other tiktok_* tool rides the TikTok Business account-holder grant or TikTok Ads (not
|
|
142
|
+
// the per-account posting row), and these two Pinterest reads ride the ad-account (pinterest_ads) row.
|
|
143
|
+
const TIKTOK_PER_ACCOUNT_TOOLS = new Set(['post_to_tiktok', 'tiktok_post_status', 'tiktok_creator_info', 'tiktok_account', 'list_tiktok_videos']);
|
|
144
|
+
const SINGLE_GRANT_TOOLS = new Set(['pinterest_targeting_analytics', 'pinterest_audience_insights']);
|
|
145
|
+
export function accountScopedTool(name) {
|
|
146
|
+
const p = String(toolProvider(name) || '');
|
|
147
|
+
if (!MULTI_ACCOUNT_CHANNELS.has(p)) return false;
|
|
148
|
+
return p === 'tiktok' ? TIKTOK_PER_ACCOUNT_TOOLS.has(String(name)) : !SINGLE_GRANT_TOOLS.has(String(name));
|
|
149
|
+
}
|
|
150
|
+
// The channel an account-scoped tool's `account` names, or null.
|
|
151
|
+
export const accountChannelOf = (name) => (accountScopedTool(name) ? String(toolProvider(name)) : null);
|
|
152
|
+
export const ACCOUNT_ARG_TEXT = 'which connected account: its @handle or id from list_connector_accounts; needed only when several are connected';
|
|
130
153
|
export function toolProvider(name) {
|
|
131
154
|
const n = String(name || '');
|
|
132
155
|
if (!n) return null;
|