hermoso 0.1.267 → 0.1.268

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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +14 -3
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -2318,9 +2318,14 @@ export const holdReasonText = (name, why, ctx = null) => {
2318
2318
  // ticked accounts on the same brand). The gate is a snapshot; the refusal must not be. Before saying "not connected",
2319
2319
  // re-read the connection set ONCE and re-gate (the same regateForWorkspace use_brand runs); a failed read fails open
2320
2320
  // as everywhere else, so the worst case is the old answer, never a wrong refusal of a connector that exists.
2321
- async function holdReasonRechecked(name, ctx) {
2321
+ async function holdReasonRechecked(name, ctx, args = null) {
2322
2322
  let why = holdReasonFor(name, ctx);
2323
2323
  if (why !== 'not_connected') return why;
2324
+ // A PER-CALL BRAND IS NOT THE PIN (2026-09-21). The connection snapshot above is the PINNED brand's; a tool that
2325
+ // takes `brand` runs against the brand the caller NAMED, which may have the connector the pin lacks. Refusing it
2326
+ // here answered "not connected" for an account that was connected. Run it: the route gates the named brand itself
2327
+ // and answers the one "not connected" shape (401 + connector) when it really is missing.
2328
+ if (callNamesOwnBrand(name, ctx, args)) return null;
2324
2329
  // The re-read must be a GOOD read before it replaces the snapshot: regateForWorkspace fails OPEN on a failed read
2325
2330
  // (right for use_brand, wrong here — it would turn "not connected" into "run it and 401"), so a read that did not
2326
2331
  // succeed keeps the snapshot's answer and the refusal it already earned.
@@ -2330,6 +2335,12 @@ async function holdReasonRechecked(name, ctx) {
2330
2335
  try { await regateForWorkspace(ctx, fresh); } catch { return why; }
2331
2336
  return holdReasonFor(name, ctx);
2332
2337
  }
2338
+ function callNamesOwnBrand(name, ctx, args) {
2339
+ const b = args && typeof args === 'object' ? args.brand : null;
2340
+ if (typeof b !== 'string' || !b.trim()) return false;
2341
+ const h = ctx && ctx.handleOf && ctx.handleOf[name];
2342
+ try { return !!(h && h.inputSchema && h.inputSchema.shape && Object.prototype.hasOwnProperty.call(h.inputSchema.shape, 'brand')); } catch { return false; }
2343
+ }
2333
2344
  // MODULE SCOPE, NOT INSIDE buildTools — and the reason is worth keeping. tools/generation-matrix-check.mjs
2334
2345
  // derives the generation surface by slicing the source between registerTool() calls and asking which
2335
2346
  // /api/generate/* routes each slice touches. Written just above find_tools, this helper's apiGet fell
@@ -2376,7 +2387,7 @@ export function installHeldToolCalls(mcp, ctx) {
2376
2387
  const h = name && ctx.handleOf[name];
2377
2388
  if (!h && LEGACY_TOOL_NAMES[name]) return legacyToolAnswer(name, request, extra, ctx); // a name only an old snapshot still holds
2378
2389
  if (h && h.enabled === false) {
2379
- const why = await holdReasonRechecked(name, ctx);
2390
+ const why = await holdReasonRechecked(name, ctx, request?.params?.arguments);
2380
2391
  if (why) { const t = holdReasonText(name, why, ctx); reportDeadEnd(why, name, t); return withHints({ content: [{ type: 'text', text: t }], isError: true }, holdHints(name, why, ctx)); }
2381
2392
  // The call itself is the evidence: this host's tool list still names a tool the session holds out on size,
2382
2393
  // i.e. the host is serving a stale roster. Run it (that is the point) and record that it happened.
@@ -3103,7 +3114,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
3103
3114
  return { content: [{ type: 'text', text: `No tool named "${n}".${near.length ? ` Did you mean: ${near.join(', ')}?` : ''} find_tools({query}) searches every tool by name or task.` }], isError: true };
3104
3115
  }
3105
3116
  if (n === 'call_tool' || n === 'find_tools' || n === 'enable_tools') return { content: [{ type: 'text', text: `${n} is a roster tool; call it directly.` }], isError: true };
3106
- const why = await holdReasonRechecked(n, ctx);
3117
+ const why = await holdReasonRechecked(n, ctx, args);
3107
3118
  // ONE sentence per hold, from holdReasonText. call_tool used to spell its own copies, so the connect link added there
3108
3119
  // never reached claude.ai or ChatGPT, the two hosts that run held tools through here.
3109
3120
  if (why) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.267",
3
+ "version": "0.1.268",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Marketing on autopilot, run from your own AI agent. 855 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. 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",