hermoso 0.1.497 → 0.1.498
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/mcp/tools.mjs +18 -4
- package/package.json +1 -1
- package/skills/hermoso-marketing/SKILL.md +1 -0
package/mcp/tools.mjs
CHANGED
|
@@ -6711,7 +6711,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6711
6711
|
}));
|
|
6712
6712
|
server.registerTool('run_post_refill', {
|
|
6713
6713
|
title: 'Run autopilot now / approve its drafts',
|
|
6714
|
-
description: 'Run AUTOPILOT POSTING (autoposting, paid plans only) NOW instead of waiting for its daily turn, or
|
|
6714
|
+
description: 'Run AUTOPILOT POSTING (autoposting, paid plans only) NOW instead of waiting for its daily turn, or review the drafts it made: schedule them, delete one, change a caption, or save a note such as "too salesy". By default it is a preview: it returns the posts it WOULD make — each new picture’s scene, the caption, the channels and the price — rendering nothing and queueing nothing. The preview still WRITES the scenes and copy with a model, which bills its model cost (the reply says exactly how many credits). dryRun:false actually renders a fresh image or video for each post within the budget (credits are spent) and then, by mode, schedules them ("auto") or keeps them as drafts ("review"). It never re-posts the Library. REVIEWING A BATCH: pass approve (draft ids, or ["all"]) to schedule drafts, discard to delete them, edit ([{id, message?, captions?, title?, at?, channels?}]) to change one first (a new `at` reschedules it); get_post_refill lists them. learn saves notes future batches follow (free). redo remakes ONE draft from a note and is paid; without confirm:true it returns only the price. A scheduled post can still be pulled before it goes out with cancel_scheduled. Every caption is screened against the voice rules and a failing one is dropped, so a plan can come back shorter than the cadence — the reason is in the notes.',
|
|
6715
6715
|
inputSchema: {
|
|
6716
6716
|
dryRun: z.boolean().optional().describe('default TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills its model cost). false renders fresh creative and schedules or drafts it per the mode.'),
|
|
6717
6717
|
force: z.boolean().optional().describe('plan a preview even while autoposting is switched off — useful for showing someone what it would do before they turn it on.'),
|
|
@@ -24142,6 +24142,20 @@ function memoryNoteVerdict(text) {
|
|
|
24142
24142
|
return ok(`Found ${d.candidates?.length || 0}: ${list}`, d);
|
|
24143
24143
|
}));
|
|
24144
24144
|
|
|
24145
|
+
// RUN DATES THE MODEL CAN READ (2026-11-02): "Aug 20 -> now · 74d". Byte-identical inline copy of lib/ad-run-dates.mjs adRunLine
|
|
24146
|
+
// (the CLI package cannot import ../lib); tools/ad-run-dates-check.mjs pins that they agree.
|
|
24147
|
+
const adRunLine = (ad, now = Date.now()) => {
|
|
24148
|
+
const a = ad || {};
|
|
24149
|
+
const ms = (v) => { if (v == null || v === '') return 0; if (typeof v === 'number') return v < 1e12 ? v * 1000 : v; const t = Date.parse(v); return Number.isFinite(t) ? t : 0; };
|
|
24150
|
+
const s = ms(a.start_date_string ?? a.start_date ?? a.startDateString ?? a.firstShown ?? a.first_shown ?? a.startDate);
|
|
24151
|
+
if (!s) return '';
|
|
24152
|
+
const eRaw = ms(a.end_date_string ?? a.end_date ?? a.endDateString ?? a.lastShown ?? a.last_shown ?? a.endDate);
|
|
24153
|
+
const live = a.is_active === true || a.isActive === true;
|
|
24154
|
+
const e = live ? now : (eRaw || now);
|
|
24155
|
+
const d = (t) => new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
24156
|
+
const days = Math.max(0, Math.round((e - s) / 86400000));
|
|
24157
|
+
return `${d(s)} -> ${live || !eRaw ? 'now' : d(eRaw)} · ${days}d`;
|
|
24158
|
+
};
|
|
24145
24159
|
server.registerTool('pull_competitor_ads', {
|
|
24146
24160
|
_meta: openaiMeta(AD_SPY_URI, 'Pulling their live ads…', 'Competitor ads pulled'),
|
|
24147
24161
|
title: 'Pull competitor ads',
|
|
@@ -24216,7 +24230,7 @@ function memoryNoteVerdict(text) {
|
|
|
24216
24230
|
// destination rather than replacing it. Meta hands us that page as `ad.url`; Google as `adUrl`. The
|
|
24217
24231
|
// destination survives only as the last resort, for a row that carries no library page at all.
|
|
24218
24232
|
const libraryUrl = ad.url || (ad.ad_archive_id ? 'https://www.facebook.com/ads/library?id=' + ad.ad_archive_id : '');
|
|
24219
|
-
rows.push(qp({ platform: p, advertiser: ad.page_name || ad.advertiserName || ad.advertiser || who, body: trunc(body), media, thumb: img, link: libraryUrl || ad.adUrl || s.link_url || ad.destinationUrl || null }));
|
|
24233
|
+
rows.push(qp({ platform: p, advertiser: ad.page_name || ad.advertiserName || ad.advertiser || who, body: trunc(body), dates: adRunLine(ad), media, thumb: img, link: libraryUrl || ad.adUrl || s.link_url || ad.destinationUrl || null }));
|
|
24220
24234
|
if (img && /^https?:\/\//.test(img)) urls.push(img);
|
|
24221
24235
|
}
|
|
24222
24236
|
}
|
|
@@ -24432,7 +24446,7 @@ function memoryNoteVerdict(text) {
|
|
|
24432
24446
|
advertiser: r?.advertiser || r?.ad?.page_name || r?.ad?.advertiserName || r?.ad?.advertiser || r?.author || r?.tiktok?.author || '',
|
|
24433
24447
|
body: r?.body || r?.copy || r?.desc || r?.tiktok?.desc || (typeof sn.body === 'string' ? sn.body : sn.body?.text) || r?.ad?.headline || '',
|
|
24434
24448
|
cta: sn.cta_text || r?.ad?.cta || '',
|
|
24435
|
-
dates:
|
|
24449
|
+
dates: adRunLine(r?.ad),
|
|
24436
24450
|
// `r.tiktok.link` was the one branch this chain forgot, so every TikTok row in a research_ads answer —
|
|
24437
24451
|
// 8 of 16 tiles on a real pull — rendered as a dead div while the post URL sat unread in the payload.
|
|
24438
24452
|
link: r?.link || r?.tiktok?.link || r?.ad?.adUrl || sn.link_url || r?.ad?.destinationUrl || '',
|
|
@@ -24537,7 +24551,7 @@ function memoryNoteVerdict(text) {
|
|
|
24537
24551
|
const s = x.snapshot || {};
|
|
24538
24552
|
return qp({
|
|
24539
24553
|
page_name: x.page_name, body: trunc(typeof s.body === 'string' ? s.body : s.body?.text), cta: s.cta_text, link: s.link_url,
|
|
24540
|
-
dates:
|
|
24554
|
+
dates: adRunLine(x), // start -> end · days running, from epoch seconds too (the string fields alone left rows dateless)
|
|
24541
24555
|
media: s.videos?.[0]?.video_sd_url || s.images?.[0]?.resized_image_url || s.cards?.[0]?.resized_image_url || s.cards?.[0]?.video_sd_url || s.videos?.[0]?.video_preview_image_url,
|
|
24542
24556
|
thumb: s.videos?.[0]?.video_preview_image_url || s.images?.[0]?.resized_image_url || s.cards?.[0]?.resized_image_url, // always an IMAGE url when one exists — feeds the markdown gallery (a video url can't render inline)
|
|
24543
24557
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.498",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1110 tools. No markup on models on Starter and up. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on any plan; Free includes 10 posts a month and every paid plan posts without a limit. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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",
|
|
@@ -43,6 +43,7 @@ found rather than invented, but it is never a prerequisite. Do what was asked an
|
|
|
43
43
|
- Video, avatar and stitch are job-based: keep `--wait` so the command prints the final URL, or poll with `hermoso jobs get <id> --wait`. Never report a render as done before the job finishes.
|
|
44
44
|
|
|
45
45
|
### 3. Publish and schedule
|
|
46
|
+
- A week of posts: `plan_week` drafts 5 posts with new images, captions and times for the active brand, on any plan. Without `confirm` it gives the price and spends nothing. `get_job` reads the drafts, the user changes what they like in plain words, and `run_post_refill` schedules them after they agree. On TikTok these land as drafts in the creator's inbox to finish there.
|
|
46
47
|
- Ten channels: `post_to_meta` (Facebook, Instagram, Threads), `post_to_tiktok`, `post_to_youtube`, `post_to_x`, `post_to_linkedin`, `post_to_pinterest`, `post_to_bluesky`, `post_to_telegram`. `schedule_post` queues one for later and `list_scheduled` shows the queue.
|
|
47
48
|
- `hermoso call list_connectors` says what is actually connected. A channel with no connection is not a missing feature: tell the user to connect it in the app under Settings, Connectors.
|
|
48
49
|
- Show the exact caption and the exact target account before publishing, and get a yes. Never rewrite or truncate what the user wrote: a channel that cannot take it refuses and says so.
|