hermoso 0.1.407 → 0.1.409
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 +20 -11
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -378,6 +378,7 @@ export const MCP_INSTRUCTIONS_WIDGET = [
|
|
|
378
378
|
'• WORKSPACE: list_brands, use_brand, create_brand, hermoso_credits, report_bug, request_feature.',
|
|
379
379
|
"CREDITS: tools that run an AI model or ad research use credits from the user's Hermoso account, and the reply says how many. If the balance is too low, say so plainly; the user manages their account at hermoso.ai. Never start a purchase.",
|
|
380
380
|
'CONFIRM with the user before scheduling a post, deleting anything, or replacing a saved profile (draft_brand with save:true).',
|
|
381
|
+
'SHOWING RESULTS: the widget already shows each finished image and video. Never repeat one as a markdown image or an image link (the chat cannot load it and shows a broken tile); describe it in words and give the link only if the user asks for it.',
|
|
381
382
|
].join('\n');
|
|
382
383
|
// `opts.widgetHost` is the transport's own host decision (mcp/http.mjs isWidgetHost), the same one registerTools gets.
|
|
383
384
|
export const instructionsFor = (scope, opts = {}) => (scope && scope.directory === 'scoped' ? MCP_INSTRUCTIONS_DIRECTORY : (opts && opts.widgetHost ? MCP_INSTRUCTIONS_WIDGET : MCP_INSTRUCTIONS));
|
|
@@ -7122,11 +7123,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7122
7123
|
}));
|
|
7123
7124
|
server.registerTool('list_x_chats', {
|
|
7124
7125
|
title: 'List X Chat conversations',
|
|
7125
|
-
description: "The connected X account's X CHAT conversations: id (group ids start with g), type, muted, disappearing-message timer, screenshot controls, admins, members, PENDING join requests, which actions are admin-only, and the active invite link. X Chat is END-TO-END ENCRYPTED, so message text and group names cannot be read here (list_x_dms reads the plain direct-message history). view 'admin_requests' (id = a group the brand admins) lists who asked to become an admin; view 'invite' (inviteLink, or id + inviteToken) previews an invite link: group name, member count, whether the brand is in it. withMembers:true resolves member @handles, and each user X returns is billed. Read-only. Billed per conversation returned, so keep maxResults small." + XC_SCOPE_NOTE,
|
|
7126
|
+
description: "The connected X account's X CHAT conversations: id (group ids start with g), type, muted, disappearing-message timer, screenshot controls, admins, members, PENDING join requests, which actions are admin-only, and the active invite link. X Chat is END-TO-END ENCRYPTED, so message text and group names cannot be read here (list_x_dms reads the plain direct-message history). view 'admin_requests' (id = a group the brand admins) lists who asked to become an admin; view 'invite' (inviteLink, or id + inviteToken) previews an invite link: group name, member count, whether the brand is in it. view 'message_requests' lists the MESSAGE REQUESTS waiting outside the inbox (inbox 'priority', the default, or 'low_quality' for X's filtered tab): who sent each one (@handle, name, followers, verified), which accounts the brand follows also follow them, and whether they changed their name or picture recently (X's impersonation hint); the message itself is encrypted. Show that to the user before manage_x_chat accept_request. withMembers:true resolves member @handles, and each user X returns is billed. Read-only. Billed per conversation or request returned, plus one user lookup per request sender, so keep maxResults small." + XC_SCOPE_NOTE,
|
|
7126
7127
|
inputSchema: {
|
|
7127
|
-
view: z.enum(['conversations', 'admin_requests', 'invite']).optional().describe("default 'conversations'"),
|
|
7128
|
+
view: z.enum(['conversations', 'admin_requests', 'invite', 'message_requests']).optional().describe("default 'conversations'"),
|
|
7129
|
+
inbox: z.enum(['priority', 'low_quality']).optional().describe("message_requests: X's Priority tab (default) or the filtered low-quality one"),
|
|
7130
|
+
withSenders: z.boolean().optional().describe('message_requests: resolve who sent each request (default true, each sender billed)'),
|
|
7128
7131
|
id: z.string().optional().describe('one conversation; for admin_requests, the group (g + digits)'),
|
|
7129
|
-
maxResults: z.number().optional().describe('1-100, default 10; every conversation returned is billed'),
|
|
7132
|
+
maxResults: z.number().optional().describe('1-100 (message_requests 1-50), default 10; every conversation or request returned is billed'),
|
|
7130
7133
|
paginationToken: z.string().optional().describe('nextToken from a previous call'),
|
|
7131
7134
|
withMembers: z.boolean().optional().describe('also resolve member/admin @handles (each one billed)'),
|
|
7132
7135
|
inviteLink: z.string().optional().describe('an x.com/i/chat/group_join/... link, for view invite'),
|
|
@@ -7138,15 +7141,16 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7138
7141
|
const cost = `Cost ${d.costCredits ?? '?'} credits.`;
|
|
7139
7142
|
if (d.view === 'invite') { const i = d.invite || {}; return ok(`Invite ${i.url || '?'}: "${i.groupName || '(no name)'}", ${i.memberCount ?? '?'} members, the brand is ${i.joinState || '?'}${i.expiresAt ? `, expires ${i.expiresAt}` : ''}. ${cost}`, d); }
|
|
7140
7143
|
if (d.view === 'admin_requests') return ok(`${d.count} admin request(s) in ${d.id}:\n` + (d.requests || []).map((u) => `- ${u.username || u.id} (${u.id})`).join('\n') + `\n${cost}`, d);
|
|
7141
|
-
|
|
7142
|
-
|
|
7144
|
+
if (d.view === 'message_requests') return ok(`${d.count} message request(s) in the ${d.inbox === 'low_quality' ? 'low-quality' : 'Priority'} tab${d.nextToken ? ' (more available, pass paginationToken)' : ''}:\n` + (d.requests || []).map((m) => `- ${(m.sender && m.sender.username) || m.senderId}${m.sender && m.sender.name ? ` (${m.sender.name})` : ''}, id ${m.senderId}${m.sender && m.sender.followers != null ? `, ${m.sender.followers} followers` : ''}${m.sender && m.sender.verified ? ', verified' : ''}${m.followedByIds && m.followedByIds.length ? `, followed by ${m.followedByIds.length} account(s) you follow` : ''}${m.recentlyChangedProfile ? ', WARNING: changed name or picture recently' : ''}`).join('\n') + `\n${d.note || ''} ${cost}`.trim(), d);
|
|
7145
|
+
const rows = (d.conversations || []).slice(0, 25).map((c) => `- ${c.id} ${c.isGroup ? 'group' : '1:1'}${c.unread ? ', unread' : ''}${c.archived ? ', archived' : ''}${c.muted ? ', muted' : ''}${c.disappearAfterMs ? `, messages disappear after ${Math.round(c.disappearAfterMs / 3600e3)}h` : ''}${c.memberIds ? `, ${c.memberIds.length} members` : ''}${c.pendingJoinIds && c.pendingJoinIds.length ? `, ${c.pendingJoinIds.length} waiting to join` : ''}${c.invite && c.invite.url ? `, invite ${c.invite.url}` : ''}`);
|
|
7146
|
+
return ok(`${d.count} X Chat conversation(s)${d.nextToken ? ' (more available, pass paginationToken)' : ''}${d.hasMessageRequests ? '; message requests are waiting (view message_requests)' : ''}:\n${rows.join('\n') || '(none)'}\n${d.note || ''} ${cost}`.trim(), d);
|
|
7143
7147
|
}));
|
|
7144
7148
|
server.registerTool('manage_x_chat', {
|
|
7145
7149
|
title: 'Manage an X Chat conversation',
|
|
7146
|
-
description: "Manage one of the connected X account's X Chat conversations (id from list_x_chats). For the brand only: 'mute' / 'unmute'; 'delete' removes it from the brand's inbox for good while the others keep theirs (confirm:true). For everyone in it: 'set_disappearing' (disappearAfter like '24h' or '7d'), 'clear_disappearing', 'screen_capture' (blockScreenshots OR notifyOnScreenshot, one per call). GROUPS (id starts with g; the brand must be an admin): 'admin_settings' (adminOnly {addMember, editGroupInfo, sendMessage, startCall, editMessageTtl, blockScreenCapture}; sendMessage:true makes it announcement-only), 'add_admins' / 'remove_admins' (userIds), 'remove_members' (userIds, confirm:true), 'leave' (confirm:true), 'approve_admin_request' / 'reject_admin_request' (userId), 'reject_join_requests' (userIds; APPROVING a join re-keys the encrypted group, so it is done in the X app), 'enable_invite' (optional title, avatarUrl, welcomeMessage, expiresAt; X stores that preview UNENCRYPTED and shows it to anyone with the link), 'disable_invite' (the shared link dies for good: confirm:true). Confirm the exact conversation with the user before passing confirm:true; a refusal is free. The answer is X's own yes or no, never an echo." + XC_SCOPE_NOTE,
|
|
7150
|
+
description: "Manage one of the connected X account's X Chat conversations (id from list_x_chats). For the brand only: 'mute' / 'unmute'; 'delete' removes it from the brand's inbox for good while the others keep theirs (confirm:true). For everyone in it: 'set_disappearing' (disappearAfter like '24h' or '7d'), 'clear_disappearing', 'screen_capture' (blockScreenshots OR notifyOnScreenshot, one per call). GROUPS (id starts with g; the brand must be an admin): 'admin_settings' (adminOnly {addMember, editGroupInfo, sendMessage, startCall, editMessageTtl, blockScreenCapture}; sendMessage:true makes it announcement-only), 'add_admins' / 'remove_admins' (userIds), 'remove_members' (userIds, confirm:true), 'leave' (confirm:true), 'approve_admin_request' / 'reject_admin_request' (userId), 'reject_join_requests' (userIds; APPROVING a join re-keys the encrypted group, so it is done in the X app), 'enable_invite' (optional title, avatarUrl, welcomeMessage, expiresAt; X stores that preview UNENCRYPTED and shows it to anyone with the link), 'disable_invite' (the shared link dies for good: confirm:true). INBOX, for the brand only: 'archive' / 'unarchive', 'mark_read' (may send the other person a read receipt). MESSAGE REQUESTS: 'accept_request' (id = the sender's numeric id, or the request's conversationId from list_x_chats view message_requests) moves the conversation into the inbox; show the user who sent it first, since the message itself cannot be read here. Confirm the exact conversation with the user before passing confirm:true; a refusal is free. The answer is X's own yes or no, never an echo." + XC_SCOPE_NOTE,
|
|
7147
7151
|
inputSchema: {
|
|
7148
|
-
action: z.enum(['mute', 'unmute', 'delete', 'set_disappearing', 'clear_disappearing', 'screen_capture', 'admin_settings', 'add_admins', 'remove_admins', 'remove_members', 'leave', 'approve_admin_request', 'reject_admin_request', 'reject_join_requests', 'enable_invite', 'disable_invite']),
|
|
7149
|
-
id: z.string().describe('conversation id from list_x_chats: g + digits for a group, or the other person\'s numeric id for a 1:1'),
|
|
7152
|
+
action: z.enum(['mute', 'unmute', 'delete', 'set_disappearing', 'clear_disappearing', 'screen_capture', 'admin_settings', 'add_admins', 'remove_admins', 'remove_members', 'leave', 'approve_admin_request', 'reject_admin_request', 'reject_join_requests', 'enable_invite', 'disable_invite', 'accept_request', 'archive', 'unarchive', 'mark_read']),
|
|
7153
|
+
id: z.string().describe('conversation id from list_x_chats: g + digits for a group, or the other person\'s numeric id for a 1:1 (accept_request: the sender\'s numeric id)'),
|
|
7150
7154
|
userIds: z.array(z.string()).optional().describe('NUMERIC X account ids (add_admins, remove_admins, remove_members, reject_join_requests)'),
|
|
7151
7155
|
userId: z.string().optional().describe('NUMERIC X account id (approve_admin_request, reject_admin_request)'),
|
|
7152
7156
|
disappearAfter: z.string().optional().describe("set_disappearing: '24h', '7d', '90m' or a number of seconds"),
|
|
@@ -7163,6 +7167,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7163
7167
|
const d = await apiPost('/api/x/chats/manage', a);
|
|
7164
7168
|
const cost = `Cost ${d.costCredits ?? '?'} credits.`;
|
|
7165
7169
|
if (d.action === 'enable_invite') return ok(d.confirmed ? `Invite link on: ${d.invite && d.invite.url}${d.invite && d.invite.expiresAt ? ` (expires ${d.invite.expiresAt})` : ''}. ${d.note || ''} ${cost}` : `X did not return an invite link. ${cost}`, d);
|
|
7170
|
+
if (d.action === 'accept_request') return ok(d.confirmed ? `Accepted: the conversation with ${d.id} is in the brand's X inbox now. ${cost}` : `X did not confirm accepting the request from ${d.id}. ${cost}`, d);
|
|
7166
7171
|
return ok(`${d.confirmed ? 'Done' : 'X did not confirm it'}: ${String(d.action).replace(/_/g, ' ')} on ${d.id}${d.userIds ? ` (${d.userIds.join(', ')})` : ''}. ${cost}`, d);
|
|
7167
7172
|
}));
|
|
7168
7173
|
server.registerTool('x_user', {
|
|
@@ -17870,7 +17875,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17870
17875
|
brand: z.union([z.string(), z.object({}).passthrough()]).optional().describe('brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one'),
|
|
17871
17876
|
product: z.string().describe('what to advertise + any angle/offer the user specified'),
|
|
17872
17877
|
format: z.enum(['auto', 'image', 'video']).optional().describe("'image', 'video', or 'auto' when unspecified"),
|
|
17873
|
-
durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a
|
|
17878
|
+
durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 15s spot in one take, or 30s for a two-person format such as a street interview (2026-10-14), and a length the user names always wins.'),
|
|
17874
17879
|
hook: z.string().optional().describe('force the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "macro_asmr"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text.'),
|
|
17875
17880
|
setting: z.string().optional().describe('force the WHERE — a setting id from list_hooks (e.g. "kitchen", "gym", or a surreal one like "volcano_rim" / "airplane_wing", which are played 100% straight and never acknowledged). Omit for a neutral setting.'),
|
|
17876
17881
|
recipe: z.string().optional().describe('a recipe id from hermoso_capabilities to force an archetype'),
|
|
@@ -18042,13 +18047,17 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18042
18047
|
// how to fix it. Nothing was re-rendered; the image is still delivered so the caller can judge it.
|
|
18043
18048
|
const _imgHead = (pc) => (pc && (pc.verdict === 'mismatch' || pc.verdict === 'absent')) ? '⚠ Image delivered, but the product in it is NOT your product — do not use it as is (details below): ' : 'Image ready: ';
|
|
18044
18049
|
const _wctx = mcpCtx.getStore();
|
|
18045
|
-
|
|
18050
|
+
// THE LOCAL (stdio / CLI) TRANSPORT QUEUES TOO (2026-10-15). Its one long HTTPS request to /api/generate/image went
|
|
18051
|
+
// through the public edge, which answers 524 at 100s while the render finishes server-side: a slow provider turned a
|
|
18052
|
+
// delivered image into an error. No request context = the local transport; it now queues the same job and polls it
|
|
18053
|
+
// (awaitRenderJob: up to 10 minutes locally), so no single request outlives the edge.
|
|
18054
|
+
if (!_wctx || (_wctx.waitMs !== undefined && _wctx.waitMs !== null)) {
|
|
18046
18055
|
const q = await apiPost('/api/generate/image', { ..._imgBody, queue: true });
|
|
18047
18056
|
const r = await awaitRenderJob({ id: q.jobId });
|
|
18048
18057
|
if (r.stillRendering) return { content: [{ type: 'text', text: `Image queued as job ${r.jobId}. It is rendering; read it with get_job.${q.productNote ? `\n${q.productNote}` : ''}` }], structuredContent: { jobId: r.jobId, stillRendering: true, ...(q.productNote ? { productNote: q.productNote } : {}), ...(q.brandPhoto ? { brandPhoto: q.brandPhoto } : {}) } };
|
|
18049
18058
|
const img = await imageBlock(r.url);
|
|
18050
18059
|
const _qNote = [q.productNote, r.raw?.productNote].filter(Boolean).join(' '); // the route's note (logo, lineup) + the worker's (crop, the product check)
|
|
18051
|
-
return { content: [{ type: 'text', text: `${_imgHead(r.raw?.productCheck)}${r.url}${r.model ? ` (${r.model})` : ''}${_qNote ? `\n${_qNote}` : ''}` }, ...(img ? [img] : [])], structuredContent: { image: r.url, model: r.model, jobId: r.jobId, creditsUsed: r.creditsUsed, ...(_qNote ? { productNote: _qNote } : {}), ...(r.raw?.productCheck ? { productCheck: r.raw.productCheck } : {}), ...(r.raw?.labelPass ? { labelPass: r.raw.labelPass } : {}), ...(q.brandPhoto ? { brandPhoto: q.brandPhoto } : {}) } };
|
|
18060
|
+
return { content: [{ type: 'text', text: `${_imgHead(r.raw?.productCheck)}${r.url}${r.model ? ` (${r.model})` : ''}${switchNote({ raw: r.raw || {} })}${_qNote ? `\n${_qNote}` : ''}` }, ...(img ? [img] : [])], structuredContent: { image: r.url, model: r.model, jobId: r.jobId, creditsUsed: r.creditsUsed, ...(_qNote ? { productNote: _qNote } : {}), ...(r.raw?.productCheck ? { productCheck: r.raw.productCheck } : {}), ...(r.raw?.labelPass ? { labelPass: r.raw.labelPass } : {}), ...(r.raw?.logoCheck ? { logoCheck: r.raw.logoCheck } : {}), ...(q.brandPhoto ? { brandPhoto: q.brandPhoto } : {}) } };
|
|
18052
18061
|
}
|
|
18053
18062
|
const d = await apiPost('/api/generate/image', _imgBody); // explicit boolean so the server's saved-brand hydration default is unambiguous
|
|
18054
18063
|
const img = await imageBlock(abs(d.image)); // show the actual creative inline in Claude, not just a URL
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.409",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, 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, 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",
|