hermoso 0.1.214 → 0.1.216

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 +17 -2
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -2578,10 +2578,15 @@ function buildTools(rawServer, opts = {}, sink = null) {
2578
2578
  if (g && !TOOL_GROUP_NAMES.includes(g)) return { content: [{ type: 'text', text: `Unknown group "${g}". Groups: ${TOOL_GROUP_NAMES.join(', ')}.` }], isError: true };
2579
2579
  const rows = [];
2580
2580
  const tokenWeight = tokenWeightFor(ctx);
2581
+ const _offGroup = new Map(); // name → { grp, h } for tools the group filter excluded (see below)
2581
2582
  for (const [name, h] of Object.entries(ctx.handleOf)) {
2582
2583
  if (!h) continue;
2583
2584
  const grp = ctx.groupOf[name] || 'core';
2584
- if (g && grp !== g) continue;
2585
+ // A GROUP FILTER NARROWS, IT NEVER HIDES (2026-09-07). `find_tools({query:'tiktok_creator_info', group:'channels'})` reported a
2586
+ // dead end because the tool lives in channel_admin — the exact-name hit was thrown away by the filter, and the agent
2587
+ // was told we have no such tool. Off-group matches are kept aside; if the scoped search finds nothing, they are
2588
+ // returned WITH their real group so the caller learns which group to enable instead of giving up.
2589
+ if (g && grp !== g) { if (!_offGroup.has(name)) _offGroup.set(name, { grp, h }); continue; }
2585
2590
  const desc = String(h.description || '');
2586
2591
  let score = 0;
2587
2592
  if (q) {
@@ -2615,6 +2620,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
2615
2620
  // THE MOST VALUABLE ROW ON THE DEFECT BOARD: what a user asked for, in their agent's words, that our catalog could
2616
2621
  // not name. Unquoted and lowercased on purpose — the ledger collapses quoted strings to <q>, and one group per
2617
2622
  // distinct ask is exactly what we want to read.
2623
+ if (!total && g && _offGroup.size) {
2624
+ // Re-score the excluded tools by NAME only (the cheap, unambiguous half): an exact or token hit outside the
2625
+ // asked-for group is an answer, not a dead end — "it exists, in channel_admin; enable that group".
2626
+ const qw = String(q || '').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
2627
+ const off = [..._offGroup.entries()].filter(([name]) => qw.some((w) => name === w || name.includes(w))).slice(0, cap)
2628
+ .map(([name, { grp, h }]) => `• ${name} [${grp}, not in the ${g} group] — ${String(h.description || '').replace(/\s+/g, ' ').slice(0, 200)}`);
2629
+ if (off.length) return ok(`Nothing in the ${g} group matches, but these tools do — they live in another group (enable it with enable_tools, or call them by name through run_tool):\n${off.join('\n')}`, { query: q, group: g, offGroup: off.length });
2630
+ }
2618
2631
  if (!total) reportDeadEnd('no_match', 'find_tools', `find_tools found nothing for: ${(q || '(empty)').replace(/["'`]/g, '').slice(0, 80)}${g ? ' in group ' + g : ''}`, { query: q, group: g });
2619
2632
  for (const r of top) r.params = compactParams(ctx.handleOf[r.name]);
2620
2633
  const lines = top.map((r) => `• ${r.name} [${r.group}${r.inRoster ? '' : ', not in your list'}${r.hold ? ', ' + r.hold : ''}] — ${r.description}\n params: ${Object.entries(r.params).map(([k, v]) => `${k}: ${v}`).join(' | ') || '(none)'}`);
@@ -17511,7 +17524,9 @@ function memoryNoteVerdict(text) {
17511
17524
  },
17512
17525
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
17513
17526
  }, wrap(async ({ query, limit }) => {
17514
- const d = await apiGet('/api/sc/run', { __path: '/v2/instagram/reels/search', query });
17527
+ let d;
17528
+ try { d = await apiGet('/api/sc/run', { __path: '/v2/instagram/reels/search', query }); }
17529
+ catch (e) { if (e?.status === 404) d = { reels: [] }; else throw e; } // a vendor 404 is "no reels match", not a failure (2026-09-07)
17515
17530
  const all = (d.reels || d.items || []).map((r) => {
17516
17531
  const o = r.owner || r.user || {};
17517
17532
  return qp({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.214",
3
+ "version": "0.1.216",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 801 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",