hermoso 0.1.461 → 0.1.463

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
@@ -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
- **1037 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **1090 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 1037 tools cover
245
+ ### What the 1090 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
@@ -423,7 +423,10 @@ export async function connectedProviders() {
423
423
  const connectLink = typeof r?.connectLink === 'string' && r.connectLink.includes('{provider}') ? r.connectLink : '';
424
424
  // withheld: tools the server does not offer right now (roster-scope.mjs toolWithheldByServer). Absent = none.
425
425
  const withheld = new Set((Array.isArray(r?.withheld) ? r.withheld : []).filter((n) => typeof n === 'string' && n));
426
- return { connected: new Set(list.filter((p) => typeof p === 'string' && p)), readOk: true, offered, gated, connectLink, withheld };
426
+ // shopifyStore (2026-10-27): true/false when the server could tell whether this account has a Shopify store;
427
+ // anything else is null = unknown, which holds nothing back (roster-scope.mjs toolHeldForNoShopifyStore).
428
+ const shopifyStore = r?.shopifyStore === true ? true : r?.shopifyStore === false ? false : null;
429
+ return { connected: new Set(list.filter((p) => typeof p === 'string' && p)), readOk: true, offered, gated, connectLink, withheld, shopifyStore };
427
430
  } catch { return { connected: new Set(), readOk: false }; }
428
431
  }
429
432
  // Upload raw file BYTES to /api/upload (150MB, persists → returns {url,kind,bytes}). Overrides the JSON content-type so
@@ -1274,7 +1274,7 @@ const PROVIDER_INFO = {
1274
1274
  openai_ads: ['ChatGPT Ads', 'ads'], apple_ads: ['Apple Ads', 'ads'], applovin_ads: ['AppLovin Ads', 'ads'],
1275
1275
  google_drive: ['Google Drive, Sheets and Docs', 'data'], onedrive: ['OneDrive', 'data'], google_analytics: ['Google Analytics', 'data'],
1276
1276
  search_console: ['Google Search Console', 'data'], merchant_center: ['Google Merchant Center', 'data'], slack: ['Slack', 'data'],
1277
- shopify: ['Shopify', 'data'], stripe: ['Stripe', 'data'], hubspot: ['HubSpot', 'data'], notion: ['Notion', 'data'],
1277
+ shopify: ['Shopify', 'data'], stripe: ['Stripe', 'data'], hubspot: ['HubSpot', 'data'], notion: ['Notion', 'data'], canva: ['Canva', 'data'],
1278
1278
  };
1279
1279
  export const providerInfo = (p) => {
1280
1280
  const hit = PROVIDER_INFO[p];
@@ -101,6 +101,8 @@ export const TOOL_PROVIDER_RULES = [
101
101
  [/vercel/, 'vercel'],
102
102
  // Notion (2026-10-25): the user's OWN Notion workspace, offered only to a workspace that connected it.
103
103
  [/notion/, 'notion'],
104
+ // Canva (2026-10-27): the user's OWN Canva, offered only to a workspace that connected it.
105
+ [/canva/, 'canva'],
104
106
  // ── posting-only channels ──
105
107
  [/youtube/, 'youtube'],
106
108
  [/google_business|business_location|^list_business_(categories|attributes)$|^business_google_updated$/, 'google_business'],
@@ -170,7 +172,20 @@ export const metaAlternativeNote = (name) => INSTAGRAM_LOGIN_TOOLS.has(String(na
170
172
  // creator marketplace trio while META_CREATOR_MARKETPLACE_SCOPE is off). ONE server flag decides it, so flipping the
171
173
  // env var lights every surface back up with no code change. Absent or unreadable = nothing withheld (fail open, like
172
174
  // every other read of this set); a withheld tool is held like a connector-gated one and answered as 'unavailable'.
173
- export function toolWithheldByServer(name, conn) { return !!(conn && conn.withheld instanceof Set && conn.withheld.has(String(name || ''))); }
175
+ // ── SHOPIFY IS AN ACCOUNT TYPE, NOT A CONNECTOR (2026-10-27) ────────────────────────────────────────────────────────
176
+ // The Shopify tools (list_shopify_products, publish_to_shopify_product, the marketing-activity tools) work only for an
177
+ // account created by installing Hermoso from the Shopify App Store: the store is derived server-side from the verified
178
+ // account and there is no "connect Shopify" tile. So they cannot ride TOOL_PROVIDER_RULES — every provider there must
179
+ // be a CONNECTOR_INFO key (property 3), and "connect it under Settings ▸ Connectors" would be a dead end. Instead the
180
+ // same /api/connectors/providers read answers `shopifyStore`: true (this account has a live store), false (it does not,
181
+ // or Shopify is off on this deployment), or absent when the server could not tell. ONLY a definite false holds them
182
+ // back — an unreadable answer keeps them, like every other read of this set (property 1). Held, they are answered by
183
+ // their own reason ('shopify_store'), never as "not available in Hermoso yet".
184
+ export const SHOPIFY_STORE_TOOL_RE = /shopify/;
185
+ export const toolNeedsShopifyStore = (name) => SHOPIFY_STORE_TOOL_RE.test(String(name || ''));
186
+ export function toolHeldForNoShopifyStore(name, conn) { return !!(conn && conn.shopifyStore === false && toolNeedsShopifyStore(name)); }
187
+ export const SHOPIFY_STORE_NOTE = 'works only for a Shopify store that installed Hermoso from the Shopify App Store, and this account has no linked store. An existing Hermoso account cannot be linked to a store from here; a merchant who did install it opens Hermoso once from their Shopify admin (Apps \u2192 Hermoso).';
188
+ export function toolWithheldByServer(name, conn) { return !!(conn && ((conn.withheld instanceof Set && conn.withheld.has(String(name || ''))) || toolHeldForNoShopifyStore(name, conn))); }
174
189
  export function toolHeldBackByConnectors(name, conn) {
175
190
  if (toolWithheldByServer(name, conn)) return true; // the server says this capability is not offered now
176
191
  if (!conn || !conn.readOk) return false; // property 1 — fail OPEN on an unreadable store