hermoso 0.1.280 → 0.1.285
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 +32 -11
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -450,7 +450,7 @@ const wrap = (fn) => {
|
|
|
450
450
|
// Read from the STRUCTURED signal, exactly as the sentence above is — never from the prose.
|
|
451
451
|
// META'S SECURITY HOLD (code 31/3858385): the server's sentence already carries the steps; this names the move
|
|
452
452
|
// so an agent relays it instead of retrying or telling the user to reconnect. Read from the STRUCTURED field.
|
|
453
|
-
if (e?.metaAuthHold === true) _hints.push({ do: 'stop retrying; ask the user to clear Meta\u2019s security check: as the Facebook profile that connected Hermoso,
|
|
453
|
+
if (e?.metaAuthHold === true) _hints.push({ do: 'stop retrying; ask the user to clear Meta\u2019s security check: as the Facebook profile that connected Hermoso, on a computer (not the Ads Manager app), create a NEW draft ad in Ads Manager, click Start authentication (or Fix errors) on the error it shows and complete it, or follow a red banner if one is shown; then run the same call again', why: 'Meta refuses new or edited ads from that profile until it re-authenticates; the connection and permissions are fine and reconnecting with the same profile does not clear it' });
|
|
454
454
|
if (Number(e?.status) === 401 && e?.connector) _hints.push({ do: `have the user connect "${e.connector}" (Settings \u25b8 Connectors, or the one-click link in this message)`, why: `${e.connector} is not connected in this workspace, so this tool can only answer 401 until it is` });
|
|
455
455
|
}
|
|
456
456
|
// ── THE STRUCTURED ERROR MARKER (2026-08-26) ──────────────────────────────────────────────────────────
|
|
@@ -4174,7 +4174,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4174
4174
|
const mkLine = (c) => { const f = (v) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(v >= 1e5 ? 0 : 1)}K` : String(v); const b = []; if (c.followers != null) b.push(`${f(c.followers)} followers`); if (c.country) b.push(c.country); if (c.badges && c.badges.length) b.push(`badges: ${c.badges.join(', ')}`); if (c.email) b.push(c.email); return `• @${c.handle}${c.verified ? ' (verified)' : ''}${b.length ? `: ${b.join(', ')}` : ''}${c.bio ? `. ${String(c.bio).slice(0, 120)}` : ''}`; };
|
|
4175
4175
|
server.registerTool('find_instagram_marketplace_creators', {
|
|
4176
4176
|
title: 'Search Instagram’s creator marketplace',
|
|
4177
|
-
description: "SEARCH INSTAGRAM'S CREATOR MARKETPLACE: Meta's own directory of creators who partner with brands, searched AS the brand's Instagram account, so the recommendations are personalised to it. Use it when the user wants creators with first-party data (followers, audience, badges such as Partnership ads / Branded content / Strong hooks / Responsive, marketplace email) or Meta's recommendations. Search by query (keywords), by similarTo (up to 5 handles, not with query), by recommendation (most_relevant_for_me, high_ad_performance, most_ads_experience, similar_brands, similar_audience, interested_in_collaboration), or filters on the creator (countries, states with exactly one country, minFollowers/maxFollowers bands, ageBucket, gender, up to 5 interests, minEngaged/maxEngaged, language, followerGrowth, lastPostWithin, verifiedAccount, hasPortfolio, hasPublicContactEmail, featuredInPaidAds, excludeMessagedCreators) and their audience (audienceCountries, audienceStates, audienceAgeBucket, audienceGender, audienceDevices), or customAudienceId from list_instagram_marketplace_audiences. username looks up one creator (no other filter allowed). find_creators ranks creators by their public posts in a niche and takes marketplace:true to add this source beside it. Needs the Meta (Facebook Login) connector with the Page linked to the brand's Instagram, and Meta's instagram_creator_marketplace_discovery permission; when it cannot run it says exactly why and what fixes it. Free.",
|
|
4177
|
+
description: "SEARCH INSTAGRAM'S CREATOR MARKETPLACE: Meta's own directory of creators who partner with brands, searched AS the brand's Instagram account, so the recommendations are personalised to it. Use it when the user wants creators with first-party data (followers, audience, badges such as Partnership ads / Branded content / Strong hooks / Responsive, marketplace email) or Meta's recommendations. Search by query (keywords), by similarTo (up to 5 handles, not with query), by recommendation (most_relevant_for_me, high_ad_performance, most_ads_experience, similar_brands, similar_audience, interested_in_collaboration), or filters on the creator (countries, states with exactly one country, minFollowers/maxFollowers bands, ageBucket, gender, up to 5 interests, minEngaged/maxEngaged, language, followerGrowth, lastPostWithin, verifiedAccount, hasPortfolio, hasPublicContactEmail, featuredInPaidAds, excludeMessagedCreators) and their audience (audienceCountries, audienceStates, audienceAgeBucket, audienceGender, audienceDevices), or customAudienceId from list_instagram_marketplace_audiences. username looks up one creator (no other filter allowed). find_creators ranks creators by their public posts in a niche and takes marketplace:true to add this source beside it. Needs the Meta (Facebook Login) connector with the Page linked to the brand's Instagram, and Meta's instagram_creator_marketplace_discovery permission; when it cannot run it says exactly why and what fixes it, and when the reason is that Meta has not approved Hermoso's app for that permission yet (read LIVE from Meta, and named under Meta by list_connectors) it says so: that is Meta's decision about Hermoso, never a reason to tell the user to reconnect. Free.",
|
|
4178
4178
|
inputSchema: {
|
|
4179
4179
|
query: z.string().optional().describe('keywords, e.g. "skincare" or "trail running"'),
|
|
4180
4180
|
username: z.string().optional().describe('one creator handle; no other filter allowed with it'),
|
|
@@ -7210,7 +7210,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7210
7210
|
}));
|
|
7211
7211
|
server.registerTool('list_instagram_shopping_catalogs', {
|
|
7212
7212
|
title: 'What Instagram Shopping can tag',
|
|
7213
|
-
description: 'Whether this Instagram account can tag products at all, and which catalogs its SHOP can tag from. CALL THIS FIRST: product tagging needs an APPROVED INSTAGRAM SHOP, and if the account does not have one, tagging fails AFTER the photo is already uploaded. The reply says which of three things is true — eligible, not eligible (a Commerce Manager approval nothing in Hermoso can grant, and not a sign anything is broken), or "could not tell", which is NOT the same as not eligible. AN EMPTY CATALOG LIST IS NOT AN EMPTY CATALOG: Instagram reaches a catalog through the account\'s SHOP, while list_meta_catalogs reads the business PORTFOLIO — a merchant can have a full catalog there and nothing available here until the shop is approved. Read-only, 0 credits.',
|
|
7213
|
+
description: 'Whether this Instagram account can tag products at all, and which catalogs its SHOP can tag from. CALL THIS FIRST: product tagging needs an APPROVED INSTAGRAM SHOP, and if the account does not have one, tagging fails AFTER the photo is already uploaded. The reply says which of three things is true — eligible, not eligible (a Commerce Manager approval nothing in Hermoso can grant, and not a sign anything is broken), or "could not tell", which is NOT the same as not eligible. AN EMPTY CATALOG LIST IS NOT AN EMPTY CATALOG: Instagram reaches a catalog through the account\'s SHOP, while list_meta_catalogs reads the business PORTFOLIO — a merchant can have a full catalog there and nothing available here until the shop is approved. Read-only, 0 credits. Whether Meta has approved Hermoso\'s app for the permission this needs is read LIVE from Meta: when it has not, list_connectors says so under Meta and this tool\'s refusal says Meta hasn\'t approved Hermoso for it yet. That is Meta\'s decision about Hermoso, not the user\'s connection, so never tell them to reconnect for it.',
|
|
7214
7214
|
inputSchema: { pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected. Its linked Instagram account is the one that gets tagged.') },
|
|
7215
7215
|
outputSchema: { igId: z.string().optional(), account: z.string().optional(), eligible: z.boolean().nullable().optional(), count: z.number().optional(), catalogs: z.array(z.any()).optional(), eligibilityNote: z.string().optional(), note: z.string().optional(), readError: z.string().optional(), limits: z.any().optional() },
|
|
7216
7216
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
@@ -7223,7 +7223,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7223
7223
|
}));
|
|
7224
7224
|
server.registerTool('search_instagram_shopping_products', {
|
|
7225
7225
|
title: 'Find products to tag on Instagram',
|
|
7226
|
-
description: 'The products in one catalog that can actually be TAGGED on an Instagram post — this is where the product_id for a product tag comes from. Omit `q` to see everything tag-eligible; pass a product name or SKU to narrow it. THIS IS A SMALLER SET THAN THE CATALOG HOLDS: a product can be in the catalog, counted by list_meta_catalog_products, and still not be taggable, so an empty answer here is never evidence the catalog is empty. Meta only SHOWS a tag whose product review_status is "approved" — an unapproved one is accepted, stored and shown to nobody, so the reply flags them. Read-only, 0 credits.',
|
|
7226
|
+
description: 'The products in one catalog that can actually be TAGGED on an Instagram post — this is where the product_id for a product tag comes from. Omit `q` to see everything tag-eligible; pass a product name or SKU to narrow it. THIS IS A SMALLER SET THAN THE CATALOG HOLDS: a product can be in the catalog, counted by list_meta_catalog_products, and still not be taggable, so an empty answer here is never evidence the catalog is empty. Meta only SHOWS a tag whose product review_status is "approved" — an unapproved one is accepted, stored and shown to nobody, so the reply flags them. Read-only, 0 credits. Whether Meta has approved Hermoso\'s app for the permission this needs is read LIVE from Meta: when it has not, list_connectors says so under Meta and this tool\'s refusal says Meta hasn\'t approved Hermoso for it yet. That is Meta\'s decision about Hermoso, not the user\'s connection, so never tell them to reconnect for it.',
|
|
7227
7227
|
inputSchema: {
|
|
7228
7228
|
catalogId: z.string().describe('from list_instagram_shopping_catalogs. Meta REQUIRES it — there is no search-every-catalog form.'),
|
|
7229
7229
|
q: z.string().optional().describe('product name or SKU. Omit it to list every tag-eligible product, which is a real ask rather than a missing argument.'),
|
|
@@ -7239,7 +7239,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7239
7239
|
}));
|
|
7240
7240
|
server.registerTool('manage_instagram_product_tags', {
|
|
7241
7241
|
title: 'Read or update the product tags on a published Instagram post',
|
|
7242
|
-
description: 'READ the product tags on a post the brand has already published, or ADD/MOVE tags on it. Pass `tags` to update: Meta\'s own behaviour is "updates coordinates if the product is already tagged; otherwise adds new tag" — so it is ADD-OR-MOVE, never replace-all, and it CANNOT be used to take a tag off. THERE IS NO WAY TO REMOVE A PRODUCT TAG: Meta documents Creating, Reading and Updating on this edge and no delete at all, so Hermoso will not guess at one — deleting the post is the only thing that removes its tags, and the reply says so rather than implying otherwise. The answer is always READ BACK from Instagram, and it separates tags that are STORED from tags that will actually be SHOWN — only an "approved" product ever appears on a published post. Read is free; the update costs 0 credits too.',
|
|
7242
|
+
description: 'READ the product tags on a post the brand has already published, or ADD/MOVE tags on it. Pass `tags` to update: Meta\'s own behaviour is "updates coordinates if the product is already tagged; otherwise adds new tag" — so it is ADD-OR-MOVE, never replace-all, and it CANNOT be used to take a tag off. THERE IS NO WAY TO REMOVE A PRODUCT TAG: Meta documents Creating, Reading and Updating on this edge and no delete at all, so Hermoso will not guess at one — deleting the post is the only thing that removes its tags, and the reply says so rather than implying otherwise. The answer is always READ BACK from Instagram, and it separates tags that are STORED from tags that will actually be SHOWN — only an "approved" product ever appears on a published post. Read is free; the update costs 0 credits too. Whether Meta has approved Hermoso\'s app for the permission this needs is read LIVE from Meta: when it has not, list_connectors says so under Meta and this tool\'s refusal says Meta hasn\'t approved Hermoso for it yet. That is Meta\'s decision about Hermoso, not the user\'s connection, so never tell them to reconnect for it.',
|
|
7243
7243
|
inputSchema: {
|
|
7244
7244
|
mediaId: z.string().describe('the numeric Instagram media id, from list_instagram_media'),
|
|
7245
7245
|
tags: z.array(z.object({ product_id: z.string(), x: z.number(), y: z.number() })).optional().describe('ADD or MOVE these tags. x and y are FRACTIONS of the image, 0.0 (left/top) to 1.0 (right/bottom) — 0.5,0.5 is the middle. Omit to just read. Max 20 on a feed post.'),
|
|
@@ -17251,7 +17251,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17251
17251
|
|
|
17252
17252
|
server.registerTool('post_edit', {
|
|
17253
17253
|
title: 'Post-production edit',
|
|
17254
|
-
description: "MECHANICAL post-production on an EXISTING video (its URL): an ordered plan of whitelisted primitives run by ffmpeg (+ Chrome for type) in seconds for ~2 credits flat, NO AI model, the original untouched (returns a NEW video). Ops: a branded end card (
|
|
17254
|
+
description: "MECHANICAL post-production on an EXISTING video (its URL): an ordered plan of whitelisted primitives run by ffmpeg (+ Chrome for type) in seconds for ~2 credits flat, NO AI model, the original untouched (returns a NEW video). Ops: a branded end card (adds its seconds), trim, speed (0.5-2x), mute (whole or a window), audio_gain (-20..+6 dB), fade_out, watermark (corner logo), grain (anti-AI), text (timed words over the clip in a native look, no branding: style 'tiktok-classic' default / 'clean-minimal' / 'note-style' or a textStyle, position, start/end), join (this video FOLLOWED BY clips[], each a Library URL, a direct file or a public TikTok / Reel / Facebook / X / YouTube post link, as one 1080x1920 video with matched loudness; transition 'cut' or 'crossfade'). Up to 6 ops, in order. 'A viral hook, then our clip' = videoUrl: the hook's post link + [{op:'join', clips:[{url: ours}], bridge}], and it ALWAYS gets a bridge unless the user asks for a bare cut: {kind:'impact'} cuts the hook just before its payoff (found from the footage; cutAt overrides) and lands our clip on a punch-in and flash, with the payoff sound taken FROM THE HOOK ITSELF: its own audio carries across the cut, else a sound generated from its frames (then up to 8 credits), else a neutral impact (sound 'auto' default | 'own' never a model | 'impact' | 'whoosh' | 'none'). {kind:'text', text, then?} only when the user asks for words over the cut. No voiceover bridge: best, make our clip's host say the connecting line. matchCut = where our clip starts. NEVER use generate_video/render_ad for these.",
|
|
17255
17255
|
inputSchema: {
|
|
17256
17256
|
videoUrl: z.string().describe('the video to edit: a render / Library URL, a direct file, or a public post link'),
|
|
17257
17257
|
ops: z.array(z.object({
|
|
@@ -17263,6 +17263,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17263
17263
|
style: z.union([z.string(), z.object({}).passthrough()]).optional().describe('text: a look name or a textStyle'),
|
|
17264
17264
|
clips: z.array(z.object({ url: z.string(), start: z.number().optional(), end: z.number().optional() })).optional().describe('join: the clips after this video'),
|
|
17265
17265
|
transition: z.enum(['cut', 'crossfade']).optional().describe('join'),
|
|
17266
|
+
bridge: z.object({ kind: z.enum(['impact', 'text']), cutAt: z.number().optional().describe('omit: found from the footage'), matchCut: z.number().optional(), sound: z.enum(['auto', 'own', 'impact', 'whoosh', 'none']).optional(), flash: z.boolean().optional(), shake: z.boolean().optional(), text: z.string().optional(), then: z.string().optional() }).optional().describe('join: connects the hook to the first clip'),
|
|
17266
17267
|
factor: z.number().optional().describe('speed 0.5-2'),
|
|
17267
17268
|
db: z.number().optional().describe('audio_gain -20..+6 dB'),
|
|
17268
17269
|
seconds: z.number().optional().describe('fade_out 0.3-3s / append_card 2-5s / crossfade 0.2-1.5s'),
|
|
@@ -18228,6 +18229,15 @@ function memoryNoteVerdict(text) {
|
|
|
18228
18229
|
// cannot word one fact three ways. `unknown` prints NOTHING — a grant we could not read must never be relayed
|
|
18229
18230
|
// as a limitation ([[failed-read-is-not-empty]]).
|
|
18230
18231
|
const posting = on.filter(c => c && c.grantMode === 'posting' && String(c.grantModeNote || '').trim());
|
|
18232
|
+
// ── WAITING ON META APPROVING HERMOSO, NOT ON THE USER (2026-09-24) ─────────────────────────────────────────
|
|
18233
|
+
// A feature whose permission Meta has not approved Hermoso's app for yet, read live by the server off Meta's own
|
|
18234
|
+
// answer about the app. It is the OPPOSITE instruction to the permission gap above: nothing the user does fixes
|
|
18235
|
+
// it, so it is never phrased as a reconnect. The sentence is the SERVER'S (`awaitingApprovalNote`).
|
|
18236
|
+
const waiting = on.filter(c => c && String(c.awaitingApprovalNote || '').trim());
|
|
18237
|
+
const waitingNote = waiting.length
|
|
18238
|
+
? '\n\n' + waiting.map(c => `ℹ ${c.provider}: ${c.awaitingApprovalNote}`).join('\n')
|
|
18239
|
+
+ '\nThose tools stay callable (they work for people with a role on the Hermoso app), but for this user Meta will refuse them until it approves Hermoso: say that plainly and never suggest reconnecting for it.'
|
|
18240
|
+
: '';
|
|
18231
18241
|
const lines = on.map(c => ` • ${c.provider}${c.agentLabel ? ` — ${c.agentLabel}` : ''} (${c.status || 'active'})${c.grantMode === 'posting' ? ' · POSTING ONLY' : ''}${c.scopeDrift?.status === 'missing' ? ` ⚠ missing ${c.scopeDrift.missing.length} permission(s) — needs a reconnect` : ''}`);
|
|
18232
18242
|
const postingNote = posting.length
|
|
18233
18243
|
? '\n\n' + posting.map(c => `ℹ ${c.grantModeNote}`).join('\n')
|
|
@@ -18239,7 +18249,7 @@ function memoryNoteVerdict(text) {
|
|
|
18239
18249
|
+ '\nThis is not something an agent can fix: re-authorizing is an OAuth consent screen, so the user has to do it in a browser — Workspace ▸ Connectors ▸ the connector ▸ Reconnect. Everything the connection already carries keeps working until then.' + gap.map(c => `\n Reconnect ${c.provider}: ${linkFor(c.provider)}`).join('')
|
|
18240
18250
|
: '';
|
|
18241
18251
|
const safe = { ...d, connectors: (d.connectors || []).map(({ accountLabel, ...c }) => c) };
|
|
18242
|
-
return ok(`${on.length} connected:\n${lines.join('\n') || ' (none)'}\nAvailable to connect through a sign-in screen (needs a browser; the link is ${linkFor('<provider>')}): ${(d.providers || []).join(', ') || '(none configured)'}.\nPaste-a-key, connectable from here with connect_connector or in the app: ${Object.keys(KEY_CONNECTORS).join(', ')}.${on.length ? '' : empty}${gapNote}${postingNote}`, safe);
|
|
18252
|
+
return ok(`${on.length} connected:\n${lines.join('\n') || ' (none)'}\nAvailable to connect through a sign-in screen (needs a browser; the link is ${linkFor('<provider>')}): ${(d.providers || []).join(', ') || '(none configured)'}.\nPaste-a-key, connectable from here with connect_connector or in the app: ${Object.keys(KEY_CONNECTORS).join(', ')}.${on.length ? '' : empty}${gapNote}${waitingNote}${postingNote}`, safe);
|
|
18243
18253
|
}));
|
|
18244
18254
|
// ── CONNECTOR WRITES. Connecting needs a browser (OAuth consent) and is correctly NOT headless — but the other two
|
|
18245
18255
|
// halves of connector management are, and were web-only: DISCONNECTING, and choosing WHICH accounts a brand may
|
|
@@ -18613,22 +18623,33 @@ function memoryNoteVerdict(text) {
|
|
|
18613
18623
|
}));
|
|
18614
18624
|
server.registerTool('remove_member', {
|
|
18615
18625
|
title: 'Remove a teammate',
|
|
18616
|
-
description: 'Remove a member from this brand workspace by email — they lose access (you can re-invite them later).
|
|
18626
|
+
description: 'Remove a member from this brand workspace by email — they lose access (you can re-invite them later), AND every connection THEY made on this brand is disconnected with them: their own X, LinkedIn, TikTok, YouTube, Pinterest, Threads… connections (on a channel holding several accounts, only the accounts they added), and any extra login or authorization they added. Connections the owner or anyone else made are never touched, and one with no record of who connected it is treated as the owner\'s. Queued posts that would publish through their accounts, or that they scheduled themselves, will not go out. The unconfirmed call reports exactly which connections and how many scheduled posts — relay that to the user, then call with confirm:true.',
|
|
18617
18627
|
inputSchema: {
|
|
18618
18628
|
email: z.string().describe('the member’s email'),
|
|
18619
18629
|
confirm: z.boolean().optional().describe('REQUIRED true'),
|
|
18620
18630
|
},
|
|
18621
|
-
outputSchema: { ok: z.boolean().optional(), removed: z.boolean().optional(), email: z.string().optional(), members: z.array(z.any()).optional() },
|
|
18631
|
+
outputSchema: { ok: z.boolean().optional(), removed: z.boolean().optional(), email: z.string().optional(), members: z.array(z.any()).optional(), connectionsDisconnected: z.array(z.any()).optional(), scheduledPostsNotGoingOut: z.number().optional() },
|
|
18622
18632
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
18623
18633
|
}, wrap(async (a) => {
|
|
18624
|
-
|
|
18634
|
+
// THE CONFIRM NAMES WHAT GOES WITH THEM (2026-09-24): removing a teammate disconnects the connections they made,
|
|
18635
|
+
// so the sentence the user approves must say which, from the same plan the removal runs. A 404 (not a member)
|
|
18636
|
+
// surfaces as the error it is; a preview that could not be read is said, never rendered as "nothing".
|
|
18637
|
+
if (a.confirm !== true) {
|
|
18638
|
+
let pv = null;
|
|
18639
|
+
try { pv = await apiGet('/api/team/remove-preview', { email: a.email || '' }); }
|
|
18640
|
+
catch (e) { if (e?.status === 404 || e?.status === 403) throw e; }
|
|
18641
|
+
return ok(pv?.message
|
|
18642
|
+
? `${pv.message} Confirm with the user, then call again with confirm:true.`
|
|
18643
|
+
: `This will remove ${a.email || '(no email)'} from the workspace and disconnect any connections they made on this brand. I could not read which ones just now — check Settings ▸ Connectors before confirming. Confirm with the user, then call again with confirm:true.`,
|
|
18644
|
+
{ ok: false, ...(pv ? { connectionsDisconnected: pv.connectionsDisconnected || [], scheduledPostsNotGoingOut: pv.scheduledPostsNotGoingOut || 0 } : {}) });
|
|
18645
|
+
}
|
|
18625
18646
|
// The route 404s BY NAME when the address is not a member (a typo, a stale address, the wrong brand) — that error
|
|
18626
18647
|
// surfaces through wrap(). What reaches here is a real change or an honest no-op, and the sentence comes from the
|
|
18627
18648
|
// server's READ-BACK of the members list, never from what we asked for.
|
|
18628
18649
|
const d = await apiPost('/api/team/remove', { email: a.email });
|
|
18629
18650
|
if (d.alreadyRemoved) return ok(d.message || `${a.email} already had no access to this workspace — nothing changed.`, { ok: false, removed: false, alreadyRemoved: true, email: a.email });
|
|
18630
18651
|
const left = (d.members || []).map(m => ` \u2022 ${m.email} \u2014 ${m.role || 'member'}`);
|
|
18631
|
-
return ok(`${d.message || `${a.email} no longer has access to this workspace.`}${left.length ? `\nStill on this workspace:\n${left.join('\n')}` : ''}`, { ok: true, removed: !!d.removed, email: d.email || a.email, members: d.members });
|
|
18652
|
+
return ok(`${d.message || `${a.email} no longer has access to this workspace.`}${left.length ? `\nStill on this workspace:\n${left.join('\n')}` : ''}`, { ok: true, removed: !!d.removed, email: d.email || a.email, members: d.members, connectionsDisconnected: d.connectionsDisconnected || [], scheduledPostsNotGoingOut: d.scheduledPostsNotGoingOut || 0 });
|
|
18632
18653
|
}));
|
|
18633
18654
|
server.registerTool('set_role', {
|
|
18634
18655
|
title: 'Change a teammate’s role',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.285",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 856 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 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",
|