hermoso 0.1.189 → 0.1.190

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 finished image &
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
- **745 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **746 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
  **What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
@@ -168,7 +168,7 @@ block entirely if you signed in above; it is there for CI, where the process can
168
168
 
169
169
  Then ask your agent: *“Generate an image ad with Hermoso.”*
170
170
 
171
- ### What the 745 tools cover
171
+ ### What the 746 tools cover
172
172
 
173
173
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
174
174
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
package/mcp/tools.mjs CHANGED
@@ -15352,9 +15352,20 @@ function buildTools(rawServer, opts = {}, sink = null) {
15352
15352
  if (!memory.length) return ok('Memory is empty for this workspace.', { memory: [] });
15353
15353
  return ok(`${memory.length} memory item(s):\n` + memory.map(m => ` • [${m.category}] ${m.text}${m.id ? ` (${m.id})` : ''}`).join('\n'), { memory });
15354
15354
  }));
15355
+ // MEMORY HYGIENE (2026-09-02) — byte-identical copies of lib/memory-hygiene.mjs's two regexes (this file ships without
15356
+ // lib/). tools/memory-hygiene-check.mjs pins the parity. Memory holds brand facts, never product/API mechanics or PII.
15357
+ const MEMORY_MECHANICS_RE = /\b(endpoint|API\b|APIs\b|webhook|rate[ -]?limit|access token|OAuth|graph\.[a-z]+|sandbox|dev(?:elopment)? mode|HTTP \d{3}|\b4\d\d\b(?: error)?|error (?:message|text|code)|'reduce the amount of data'|not (?:yet )?supported|deprecated|verified \d{4}-\d{2}-\d{2}|on this connection|Hermoso'?s? (?:own|publish record|tool|MCP|CLI)|(?:no|missing|lacks?) '?[\w ]{0,30}'? ?endpoint|tool (?:returns?|output|call)|(?:returns?|come back|comes back) (?:null|empty|zeros?|truncated|full bodies|counts only)|omit(?:s|ted)? zero rows|permission (?:edge|rows?|list|request)|subscribed_fields|paging cursor|pagination)\b/i;
15358
+ const MEMORY_PII_RE = /(\+\d[\d\s().-]{7,}\d)|(\(?\b\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b)|([\w.+-]+@[\w-]+\.[\w.-]+)/;
15359
+ function memoryNoteVerdict(text) {
15360
+ const t = String(text || '').trim();
15361
+ if (!t) return { ok: false, reason: 'empty' };
15362
+ if (MEMORY_PII_RE.test(t)) return { ok: false, reason: 'pii', message: 'Not saved: Memory never holds a phone number or an email address.' };
15363
+ if (MEMORY_MECHANICS_RE.test(t)) return { ok: false, reason: 'mechanics', message: 'Not saved: that is about how Hermoso or a platform API behaves, not about the brand. Memory holds brand, audience, offer, taste and working preferences only — product behaviour lives in the tools themselves.' };
15364
+ return { ok: true };
15365
+ }
15355
15366
  server.registerTool('remember', {
15356
15367
  title: 'Remember a fact',
15357
- description: 'Save a durable fact or PREFERENCE about the brand, audience, or the user’s creative TASTE (e.g. “audience is first-time homebuyers”, “prefers bold lime accents”, “always captions off”) into the workspace Memory so it shapes FUTURE ads. For lasting things, not one-off requests. Merges into the existing Memory (never overwrites); de-dupes on identical text.',
15368
+ description: 'Save a durable fact or PREFERENCE about the brand, audience, or the user’s creative TASTE (e.g. “audience is first-time homebuyers”, “prefers bold lime accents”, “always captions off”) into the workspace Memory so it shapes FUTURE ads. For lasting things, not one-off requests. Merges into the existing Memory (never overwrites); de-dupes on identical text. NEVER how Hermoso, a tool, a connector or a platform API behaves (what a call returns, errors, permissions, limits, ids) and never a phone number or email — those are refused.',
15358
15369
  inputSchema: {
15359
15370
  text: z.string().describe('the fact/preference, concise'),
15360
15371
  category: z.string().optional().describe('short bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General)'),
@@ -15363,6 +15374,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15363
15374
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
15364
15375
  }, wrap(async (a) => {
15365
15376
  const text = String(a.text || '').trim(); if (!text) return { content: [{ type: 'text', text: 'Nothing to remember — pass text.' }], isError: true };
15377
+ { const v = memoryNoteVerdict(text); if (!v.ok) return { content: [{ type: 'text', text: v.message }], isError: true }; }
15366
15378
  let list = await readStore('heist.memory.v1'); if (!Array.isArray(list)) list = [];
15367
15379
  if (list.some(m => m && String(m.text || '').toLowerCase() === text.toLowerCase())) return ok(`Already in memory: “${text}”.`, { ok: true });
15368
15380
  const item = { id: newId('m'), text, category: String(a.category || 'General').trim() || 'General', source: 'mcp', createdAt: Date.now() };
@@ -15392,6 +15404,30 @@ function buildTools(rawServer, opts = {}, sink = null) {
15392
15404
  // died inside one conversation, while the web user's own board sat untouched beside them.
15393
15405
  // These are typed writers on purpose (see the store_set note above): they mint ids, de-dupe on the ad `key`, respect
15394
15406
  // the per-store caps and write the two-level tombstone a genuine delete needs.
15407
+ server.registerTool('tidy_memory', {
15408
+ title: 'Tidy the Memory list',
15409
+ description: 'Clean up and consolidate the workspace Memory: drops entries that are about how Hermoso, a tool, a connector or a platform API behaves (product behaviour, not the brand), drops phone numbers and emails, and merges near-duplicate facts into one sentence each. Call with no argument to get the PROPOSAL (what would be removed and merged, with reasons) — nothing changes. Call again with confirm:true to apply it through the same typed writers the app uses (deletes carry tombstones so they stay deleted on every device). One small model call; a few credits.',
15410
+ inputSchema: { confirm: z.boolean().optional().describe('true to APPLY the proposal; omit to only see it') },
15411
+ outputSchema: { scanned: z.number().optional(), remove: z.array(z.any()).optional(), merge: z.array(z.any()).optional(), applied: z.boolean().optional() },
15412
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
15413
+ }, wrap(async (a) => {
15414
+ let list = await readStore('heist.memory.v1'); if (!Array.isArray(list)) list = [];
15415
+ if (!list.length) return ok('Memory is empty — nothing to tidy.', { scanned: 0, remove: [], merge: [], applied: false });
15416
+ const r = await apiPost('/api/memory/tidy', { items: list.map(m => ({ id: m.id, text: m.text, category: m.category })) });
15417
+ const d = r?.data || { remove: [], merge: [] };
15418
+ const byId = new Map(list.map(m => [String(m.id), m]));
15419
+ const lines = [];
15420
+ for (const x of d.remove || []) lines.push(`- remove [${x.reason}]: ${String(byId.get(x.id)?.text || x.id).slice(0, 140)}`);
15421
+ for (const m of d.merge || []) lines.push(`- merge ${m.ids.length} → “${m.text.slice(0, 140)}”`);
15422
+ if (!lines.length) return ok(`Scanned ${list.length} memories — nothing to remove or merge.`, { scanned: list.length, remove: [], merge: [], applied: false });
15423
+ if (!a.confirm) return ok(`Proposal over ${list.length} memories (nothing changed — call again with confirm:true to apply):\n${lines.join('\n')}`, { scanned: list.length, remove: d.remove, merge: d.merge, applied: false });
15424
+ const gone = new Set([...(d.remove || []).map(x => String(x.id)), ...(d.merge || []).flatMap(m => m.ids.map(String))]);
15425
+ for (const id of gone) await tombstone('heist.memory.v1', id);
15426
+ const merged = (d.merge || []).map(m => ({ id: newId('m'), text: m.text, category: m.category || 'General', source: 'tidy', createdAt: Date.now() }));
15427
+ await writeStore('heist.memory.v1', [...merged, ...list.filter(m => m && !gone.has(String(m.id)))].slice(0, 200));
15428
+ return ok(`Tidied: removed ${gone.size - (d.merge || []).flatMap(m => m.ids).length}, merged ${(d.merge || []).length} group(s). ${list.length} → ${list.length - gone.size + merged.length} memories.\n${lines.join('\n')}`, { scanned: list.length, remove: d.remove, merge: d.merge, applied: true });
15429
+ }));
15430
+
15395
15431
  const SWIPE_KEY = 'adInspo.swipefile.v1';
15396
15432
  const swipeEmpty = () => ({ collections: [{ id: 'default', name: 'My Swipefile' }], activeId: 'default', ads: [] });
15397
15433
  // Mirror the webapp's Swipe.read(): anything that isn't the {collections[], ads[]} shape is treated as an empty
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.189",
3
+ "version": "0.1.190",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
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.",
5
+ "description": "AI ad studio and marketing MCP server with 746 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",
7
7
  "bin": {
8
8
  "hermoso": "bin/hermoso.mjs"