hermoso 0.1.392 → 0.1.394

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/bin/hermoso.mjs CHANGED
@@ -238,6 +238,26 @@ async function main() {
238
238
  const rows = inv.filter((t) => (!wantGroup || t.group === wantGroup)
239
239
  && (!q || t.name.includes(q) || t.description.toLowerCase().includes(q) || (t.title || '').toLowerCase().includes(q)))
240
240
  .sort((a, b) => order(a.group) - order(b.group) || a.name.localeCompare(b.name));
241
+ // A PHRASE IS SEARCHED BY MEANING WHEN NO TOOL CONTAINS IT LITERALLY (2026-10-06). `--search` is a substring
242
+ // grep, so "switch autopilot to auto-publish" and "when someone comments guide send them a dm" found NOTHING
243
+ // while set_post_refill and save_dm_automation do exactly that. The words go to find_tools — the same ranked
244
+ // search (synonyms, stems, typos) the hosted connector uses — and its rows are printed instead of a dead end.
245
+ if (!rows.length && q && /\s/.test(q.trim())) {
246
+ const { client, close } = await reg.openRegistry();
247
+ try {
248
+ const r = await client.callTool({ name: 'find_tools', arguments: { query: q, limit: 8, ...(wantGroup ? { group: wantGroup } : {}) } }, undefined, { timeout: cliCallTimeoutMs(process.env.HERMOSO_CALL_TIMEOUT_MS) });
249
+ const hits = (r.structuredContent?.tools || []).map((t) => t.name);
250
+ if (hits.length) {
251
+ if (flags.names) return console.log(hits.join('\n'));
252
+ if (flags.json === true) return console.log(JSON.stringify(hits.map((n) => inv.find((t) => t.name === n)).filter(Boolean), null, 2));
253
+ const byName = new Map(inv.map((t) => [t.name, t]));
254
+ console.log(`No tool contains "${q}" word for word; closest by meaning:`);
255
+ for (const n of hits) { const t = byName.get(n); console.log(` ${n}${t ? ` (${t.group}) ${String(t.title || t.description || '').split(/(?<=[.!?])\s/)[0].slice(0, 110)}` : ''}`); }
256
+ console.log(`\nRead one with: hermoso tools <name>`);
257
+ return;
258
+ }
259
+ } finally { await close(); }
260
+ }
241
261
  if (flags.json === true) return console.log(JSON.stringify(rows, null, 2));
242
262
  if (flags.names) return console.log(rows.map((t) => t.name).join('\n'));
243
263
  if (!rows.length) return console.log(`No tool matches${wantGroup ? ` in ${wantGroup}` : ''}${q ? ` "${q}"` : ''}. Try: hermoso tools`);
package/mcp/tools.mjs CHANGED
@@ -3325,6 +3325,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
3325
3325
  // what a video SAYS: analyze_video returns the verbatim transcript (voiceover + on-screen text). 2026-10-04 dead end: "readtranscript".
3326
3326
  transcript: ['transcript', 'analyze', 'video'], transcribe: ['transcript', 'analyze', 'video'], transcrib: ['transcript', 'analyze', 'video'], transcription: ['transcript', 'analyze', 'video'],
3327
3327
  caption: ['caption', 'subtitles', 'analyze'], word: ['transcript', 'analyze'], spoken: ['analyze', 'voice'], verbatim: ['analyze'],
3328
+ // AUTOPILOT AND AUTOMATIONS IN THE USER'S WORDS (2026-10-06): "autopilot" found NOTHING and "post 2 times a day",
3329
+ // "pause autopilot", "when someone comments GUIDE send them a dm" all missed the tools, because autopilot's tools are
3330
+ // named after the "post refill" and nobody says that. A rule-shaped ask ("when someone…", "anyone who…") is an automation.
3331
+ autopilot: ['refill', 'post'], autopost: ['refill', 'post'], autoposting: ['refill', 'post'], autopublish: ['refill'], frequency: ['refill'], times: ['refill', 'schedule'], daily: ['refill', 'schedule'], cadence: ['refill', 'schedule'],
3332
+ draft: ['refill', 'draft'], approve: ['refill', 'approve'], automatic: ['automation', 'refill'], automatically: ['automation', 'refill'], auto: ['automation', 'refill'],
3333
+ automation: ['automation'], automate: ['automation', 'refill'], manychat: ['automation', 'dm'], someone: ['automation'], anyone: ['automation'], whenever: ['automation'],
3328
3334
  });
3329
3335
  const stemWord = (w) => {
3330
3336
  if (w.length <= 3) return w;
@@ -5708,8 +5714,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
5708
5714
  // list_scheduled shows and cancel_scheduled cancels. It ALWAYS makes new content within the user's render budget and
5709
5715
  // never re-posts the Library; in 'review' mode each batch waits as drafts until approved.
5710
5716
  server.registerTool('get_post_refill', {
5711
- title: 'Autoposting status',
5712
- description: 'Show autoposting (the posting refill) for this profile: whether it is on, its mode (review = each batch of fresh posts waits as drafts for approval; auto = fresh posts are scheduled straight away), WHICH CHANNELS it posts to with each one’s posts per day and posting times, roughly what it costs a day, when it next runs, how many posts are queued, the DRAFTS waiting for approval (id, time, channels, caption, the new image or video), and the notes it learned from reviews. It also names the channels that CANNOT be posted to and why. Read-only, free.',
5717
+ title: 'Autopilot posting status',
5718
+ description: 'Show AUTOPILOT POSTING (autoposting, the posting refill) for this profile — "is autopilot on?", "what is waiting for review?": whether it is on, its mode (review = each batch of fresh posts waits as drafts for approval; auto = fresh posts are scheduled straight away), WHICH CHANNELS it posts to with each one’s posts per day and posting times, roughly what it costs a day, when it next runs, how many posts are queued, the DRAFTS waiting for approval (id, time, channels, caption, the new image or video), and the notes it learned from reviews. It also names the channels that CANNOT be posted to and why. Read-only, free.',
5713
5719
  inputSchema: {},
5714
5720
  outputSchema: {
5715
5721
  enabled: z.boolean().optional(), mode: z.string().optional(), daysAhead: z.number().optional(),
@@ -5737,13 +5743,15 @@ function buildTools(rawServer, opts = {}, sink = null) {
5737
5743
  ].filter(Boolean).join('\n'), d);
5738
5744
  }));
5739
5745
  server.registerTool('set_post_refill', {
5740
- title: 'Configure autoposting',
5741
- description: 'Turn autoposting (the posting refill) on or off and set how it behaves. PASS ONLY WHAT CHANGES. It always makes NEW posts (a fresh image, carousel or video for each) and never re-posts the Library. WHERE: every connected channel by default (channels narrows it); HOW OFTEN: the brand’s posting times, and channelPostsPerDay sets fewer on a channel (e.g. {"x":1}). There are no credit limits to set — get_post_refill shows roughly what it costs a day. Switching it on is refused when no channel is connected or nothing a day would be made. SWITCHING IT ON ASKS THE BRIEF ONCE: a goal (followers / sales / launch / other) is required; content pillars (get_post_refill suggests some from the brand), formats (mix / video / carousel / image) and an avoid list are optional. Posts are written from the brief, the brand profile, what has worked on each channel (compared only within a channel), the brand’s skills and playbooks and the ads it saved. Ask the user for anything missing (goal, mode, channels) rather than guessing. `mode` picks what happens to each batch: "review" (the default) keeps the fresh posts as drafts until a human approves them (run_post_refill approve), "auto" schedules them straight away. `enabled:false` is the PAUSE — it removes the recurring job outright, and posts already queued are left alone (cancel those with cancel_scheduled). Three posting times means up to three posts a day; to post MORE per day, add posting times on the Schedule tab.',
5746
+ title: 'Set up, change or pause autopilot posting',
5747
+ description: 'Turn AUTOPILOT POSTING (autoposting, the posting refill) on, change it or pause it: "post 2 times a day on Instagram and Bluesky about …, let me review first", "make it 1 a day", "switch to auto-publish", "pause autopilot". PASS ONLY WHAT CHANGES. It always makes NEW posts (a fresh image, carousel or video for each) and never re-posts the Library. WHERE: every connected channel by default (channels narrows it); HOW OFTEN: the brand’s posting times, and channelPostsPerDay sets fewer on a channel (e.g. {"x":1}). There are no credit limits to set — get_post_refill shows roughly what it costs a day. Switching it on is refused when no channel is connected or nothing a day would be made. SWITCHING IT ON ASKS THE BRIEF ONCE: a goal (followers / sales / launch / other) is required; content pillars (get_post_refill suggests some from the brand), formats (mix / video / carousel / image) and an avoid list are optional. Posts are written from the brief, the brand profile, what has worked on each channel (compared only within a channel), the brand’s skills and playbooks and the ads it saved. Ask the user for anything missing (goal, mode, channels) rather than guessing. `mode` picks what happens to each batch: "review" (the default) keeps the fresh posts as drafts until a human approves them (run_post_refill approve), "auto" schedules them straight away. `enabled:false` is the PAUSE — it removes the recurring job outright, and posts already queued are left alone (cancel those with cancel_scheduled). Three posting times means up to three posts a day; postsPerDay sets fewer, and to post MORE per day pass postingTimes (one time per post a day) — e.g. "2 a day" on a brand with 3 posting times is postsPerDay:2, "4 a day" needs four postingTimes.',
5742
5748
  inputSchema: {
5743
5749
  enabled: z.boolean().optional().describe('on/off. true needs a connected channel and a goal (refused otherwise). false PAUSES it: the recurring job is deleted and nothing new is made. Already-queued posts are untouched.'),
5744
5750
  mode: z.enum(['review', 'auto']).optional().describe('"review" (default): each batch of fresh posts waits as drafts for approval. "auto": fresh posts are scheduled to publish automatically.'),
5745
5751
  daysAhead: z.number().optional().describe('how far ahead to keep the queue full, 1–30 (default 7)'),
5746
- postsPerDay: z.number().optional().describe('cap the posts per day BELOW the number of posting times. 0 (default) = use every posting time, which is where "3 a day" comes from. To post MORE per day, add posting times instead.'),
5752
+ postsPerDay: z.number().optional().describe('posts a day, up to the number of posting times ("2 times a day" = 2). 0 (default) = one per posting time. To post MORE per day than there are posting times, also pass postingTimes.'),
5753
+ postingTimes: z.array(z.string()).optional().describe('the brand\'s daily posting times, 24-hour "HH:MM" (e.g. ["09:00","13:00","18:00"]); REPLACES the list. Autopilot makes one post per posting time, and scheduled posts use the same times. get_post_refill shows the current ones.'),
5754
+ timezone: z.string().optional().describe('IANA timezone the posting times are in, e.g. "America/New_York" (default: the brand\'s, else UTC)'),
5747
5755
  channelPostsPerDay: z.record(z.number()).optional().describe('per channel, how many posts a day (the first N of the posting times), e.g. {"x":1,"instagram":3}. 0 turns a channel off; null resets it to every posting time. Merged into what is saved.'),
5748
5756
  goal: z.enum(['followers', 'sales', 'launch', 'other']).optional().describe('THE BRIEF: what the posts are for — grow followers, drive sales, promote a launch, or other (say what in goalNote). Required to switch it on.'),
5749
5757
  goalNote: z.string().optional().describe('THE BRIEF: a line about the goal (e.g. the launch date and what is launching).'),
@@ -5766,11 +5774,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
5766
5774
  const d = await apiPut('/api/schedule/refill', { ...rest, ...(Object.keys(brief).length ? { brief } : {}) });
5767
5775
  return ok((d.enabled
5768
5776
  ? `Autoposting is ON in ${d.mode === 'auto' ? 'AUTO-PUBLISH mode — each run makes fresh posts and schedules them to go out on their own.' : 'REVIEW mode — each run makes fresh posts and keeps them as drafts; nothing is scheduled until you approve them with run_post_refill.'} get_post_refill lists the channels it posts to and what it costs a day. Run run_post_refill (a preview by default: it renders and queues nothing, and writing its copy costs a few credits) to see a plan.`
5769
- : 'Autoposting is OFF. The recurring job is gone and nothing new will be made. Posts already in the calendar are untouched — cancel those with cancel_scheduled if you want them gone.') + (d.creditNote ? `\n${d.creditNote}` : ''), d);
5777
+ : 'Autoposting is OFF. The recurring job is gone and nothing new will be made. Posts already in the calendar are untouched — cancel those with cancel_scheduled if you want them gone.') + (Array.isArray(d.postingTimes) ? `\nPosting times are now ${d.postingTimes.join(', ')} (${d.timezone || 'UTC'}).` : '') + (d.creditNote ? `\n${d.creditNote}` : ''), d);
5770
5778
  }));
5771
5779
  server.registerTool('run_post_refill', {
5772
- title: 'Run autoposting / review its drafts',
5773
- description: 'Run autoposting NOW instead of waiting for its daily turn, or act on the drafts it made. PREVIEW BY DEFAULT: 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 a few credits (the reply says how many). 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: PAID, so without confirm:true it returns only the price; confirm only on the user’s yes. 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.',
5780
+ title: 'Run autopilot now / approve its drafts',
5781
+ description: 'Run AUTOPILOT POSTING (autoposting) NOW instead of waiting for its daily turn, or REVIEW the drafts it made — "approve them all", "delete that one", "change the caption", "too salesy, remember that". PREVIEW BY DEFAULT: 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 a few credits (the reply says how many). 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: PAID, so without confirm:true it returns only the price; confirm only on the user’s yes. 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.',
5774
5782
  inputSchema: {
5775
5783
  dryRun: z.boolean().optional().describe('default TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills a few credits). false renders fresh creative and schedules or drafts it per the mode.'),
5776
5784
  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.'),
@@ -7944,7 +7952,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
7944
7952
  }));
7945
7953
  server.registerTool('save_dm_automation', {
7946
7954
  title: 'Create or edit a DM automation (Instagram, Facebook, X)',
7947
- description: 'Create (no id) or edit (id) a ManyChat-style automation. Instagram/Facebook: a keyword comment (one post, the next post, any post; on Facebook also ads) gets a private reply in DMs/Messenger, a DM keyword or ig.me/m.me ref link gets a message, with optional link buttons and rotated public replies. X: a keyword @mention or a reply under your post gets a public reply, a DM keyword gets a DM (X allows an automated DM only after the person DMed you). Once per person per post; a DM saying STOP opts that person out. Instagram and Facebook are free; X is billed in credits for every mention, reply or DM X delivers to the account plus each reply or DM sent, and running out stops the X rules. It messages real people, so confirm the text first.',
7955
+ description: 'Create (no id) or edit (id) a ManyChat-style AUTO-DM / AUTO-REPLY automation: "when someone comments GUIDE on my posts, DM them my guide link", "reply to anyone who DMs PRICE with our pricing page", "reply to people who mention us on X". PAUSE one with {id, enabled:false}; PRACTICE MODE (dryRun:true) logs what it would send and sends nothing. Instagram/Facebook: a keyword comment (one post, the next post, any post; on Facebook also ads) gets a private reply in DMs/Messenger, a DM keyword or ig.me/m.me ref link gets a message, with optional link buttons and rotated public replies. X: a keyword @mention or a reply under your post gets a public reply, a DM keyword gets a DM (X allows an automated DM only after the person DMed you). Once per person per post; a DM saying STOP opts that person out. Instagram and Facebook are free; X is billed in credits for every mention, reply or DM X delivers to the account plus each reply or DM sent, and running out stops the X rules. It messages real people, so confirm the text first.',
7948
7956
  inputSchema: { id: z.string().optional(), ...dmaAnyShape },
7949
7957
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
7950
7958
  }, wrap(async (a) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.392",
3
+ "version": "0.1.394",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Marketing on autopilot, run from your own AI agent. 906 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, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. 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",