hermoso 0.1.495 → 0.1.496
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/openai-extensions.mjs +31 -3
- package/mcp/tool-hints.mjs +19 -0
- package/mcp/tools.mjs +55 -4
- package/package.json +1 -1
|
@@ -276,6 +276,8 @@ h3{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--mute
|
|
|
276
276
|
.quick{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:12px 0 4px}
|
|
277
277
|
.chip{font-size:13px;background:var(--card)}
|
|
278
278
|
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
|
|
279
|
+
.ccard{display:flex;flex-direction:column;gap:6px;margin:14px 0 4px;border:1px solid var(--line);border-radius:var(--radius);background:var(--card);padding:12px}
|
|
280
|
+
.ccard .row{margin-top:4px}
|
|
279
281
|
.gcard{text-align:left;border:1px solid var(--line);border-radius:var(--radius);background:var(--card);padding:12px;cursor:var(--cur)}
|
|
280
282
|
.gcard b{display:block;margin-bottom:2px}.gcard span{color:var(--muted);font-size:12px}
|
|
281
283
|
.field{margin:12px 0}
|
|
@@ -360,7 +362,7 @@ const APP_SCRIPT = String.raw`<script>
|
|
|
360
362
|
var TOOL_VIEW = { open_hermoso_home: 'home', open_library: 'library', open_ad_brief: 'create', set_up_hermoso: 'setup', show_ad_concepts: 'concepts', show_ad_results: 'results' };
|
|
361
363
|
var CARD_VIEWS = ['concepts', 'results'];
|
|
362
364
|
var PANEL_VIEWS = ['home', 'library', 'jobs', 'calendar', 'ads', 'create', 'setup'];
|
|
363
|
-
var SECTIONS_FOR = { home: ['brand', 'brands', 'library', 'jobs', 'prefs'], library: ['library', 'jobs', 'brands', 'calendar'], jobs: ['jobs', 'brands'], calendar: ['calendar', 'brands', 'prefs'], ads: ['ads', 'brands'], create: ['brand', 'brands', 'library', 'creators', 'products', 'prefs'], setup: ['brand', 'brands', 'connectors'] };
|
|
365
|
+
var SECTIONS_FOR = { home: ['brand', 'brands', 'library', 'jobs', 'prefs', 'connectors'], library: ['library', 'jobs', 'brands', 'calendar'], jobs: ['jobs', 'brands'], calendar: ['calendar', 'brands', 'prefs'], ads: ['ads', 'brands'], create: ['brand', 'brands', 'library', 'creators', 'products', 'prefs'], setup: ['brand', 'brands', 'connectors'] };
|
|
364
366
|
var EDITABLE = ['queued', 'scheduled'];
|
|
365
367
|
var state = {
|
|
366
368
|
view: 'home', surface: 'panel', data: null, host: {}, caps: {}, selected: [], focus: null, busy: false, pollTimer: null, pollStarted: 0, routed: false, ready: false,
|
|
@@ -659,6 +661,7 @@ const APP_SCRIPT = String.raw`<script>
|
|
|
659
661
|
+ '<button class="chip" data-act="create" data-format="video">Make a video ad</button>'
|
|
660
662
|
+ '<button class="chip" data-act="msg" data-text="Help me schedule a post from my Hermoso Library.">Schedule a post</button>'
|
|
661
663
|
+ '</div></section>';
|
|
664
|
+
html += connectCardHtml(d);
|
|
662
665
|
html += '<h3>Get started</h3><div class="cards">'
|
|
663
666
|
+ gcard('tab', 'library', 'Library', 'Your images and videos')
|
|
664
667
|
+ gcard('tab', 'create', 'Create', 'Pick options, see concepts, then render')
|
|
@@ -672,6 +675,14 @@ const APP_SCRIPT = String.raw`<script>
|
|
|
672
675
|
if (d.notes && d.notes.brand) html += sectionNote('brand');
|
|
673
676
|
return html;
|
|
674
677
|
}
|
|
678
|
+
// NOTHING CONNECTED YET: posting from here starts with one browser sign-in. Absent while the read is pending or failed,
|
|
679
|
+
// because an unread list is not an empty one. The links leave through openLink, like every other link in this app.
|
|
680
|
+
function connectCardHtml(d){
|
|
681
|
+
var c = d.connectCard; if (!c || !c.channels || !c.channels.length) return '';
|
|
682
|
+
return '<div class="ccard" data-connect-card="1"><b>Post what you make from this chat</b><span class="s">Nothing is connected yet. Sign in once on the platform, then ask here to post or schedule.</span><div class="row">'
|
|
683
|
+
+ c.channels.map(function(x){ return '<button class="btn primary" data-act="open-link" data-url="' + esc(x.url) + '">' + esc(x.name) + '</button>'; }).join('')
|
|
684
|
+
+ '<button class="btn" data-act="setup" data-step="connections">All channels</button></div></div>';
|
|
685
|
+
}
|
|
675
686
|
function gcard(act, target, title, sub){
|
|
676
687
|
return '<button class="gcard" data-act="' + act + '" ' + (act === 'setup' ? 'data-step' : 'data-view') + '="' + target + '"><b>' + esc(title) + '</b><span>' + esc(sub) + '</span></button>';
|
|
677
688
|
}
|
|
@@ -1142,6 +1153,7 @@ const APP_SCRIPT = String.raw`<script>
|
|
|
1142
1153
|
if (a === 's-draft') return draftBrand();
|
|
1143
1154
|
if (a === 's-step') { state.setup.step = el.getAttribute('data-step'); if (state.setup.step === 'connections') load(['connectors']); return render(); }
|
|
1144
1155
|
if (a === 's-check') return load(['connectors']);
|
|
1156
|
+
if (a === 'open-link') { var ou = el.getAttribute('data-url'); if (ou) openLink(ou); return; }
|
|
1145
1157
|
if (a === 's-connect') { var pv = el.getAttribute('data-p'); var c = (d.connections || []).filter(function(x){ return x.provider === pv; })[0]; if (c && c.connectUrl) { openLink(c.connectUrl).then(function(){ startWatch(pv); render(); }); } return; }
|
|
1146
1158
|
// concepts
|
|
1147
1159
|
if (a === 'c-pick') { state.concepts.open = i; return render(); }
|
|
@@ -1287,6 +1299,17 @@ const CHANNELS_OF = { meta: ['facebook', 'instagram'], instagram: ['instagram'],
|
|
|
1287
1299
|
const CHANNEL_LABEL = { facebook: 'Facebook', instagram: 'Instagram', threads: 'Threads', tiktok: 'TikTok', youtube: 'YouTube', linkedin: 'LinkedIn', x: 'X', pinterest: 'Pinterest', google_business: 'Google Business Profile', bluesky: 'Bluesky', telegram: 'Telegram' };
|
|
1288
1300
|
export const ALL_CHANNELS = Object.freeze(Object.keys(CHANNEL_LABEL));
|
|
1289
1301
|
|
|
1302
|
+
// THE HOME SCREEN'S CARD FOR AN ACCOUNT WITH NOTHING TO POST TO (2026-11-02): the most connected channels on offer, first
|
|
1303
|
+
// to last by real connection counts (the server's `popular`), each with its one-click link. A connected posting channel
|
|
1304
|
+
// answers null: there is nothing to ask for. `via` names this surface on the admin Funnel tab.
|
|
1305
|
+
export const HOME_CONNECT_VIA = 'chatgpt-home';
|
|
1306
|
+
export function homeConnectCard(connections = [], popular = []) {
|
|
1307
|
+
const rows = (Array.isArray(connections) ? connections : []).filter((c) => c && CHANNELS_OF[c.provider] && c.provider !== 'google_business');
|
|
1308
|
+
if (!rows.length || rows.some((c) => c.connected)) return null;
|
|
1309
|
+
const rank = (p) => { const i = (Array.isArray(popular) ? popular : []).indexOf(p); return i < 0 ? 999 : i; };
|
|
1310
|
+
return { channels: [...rows].sort((a, b) => rank(a.provider) - rank(b.provider)).slice(0, 3).map((c) => ({ provider: c.provider, name: c.name, url: `${c.connectUrl}&via=${HOME_CONNECT_VIA}` })) };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1290
1313
|
export function libraryPayload(reads = {}, { view = 'library', origins = [], proxyBase = '', abs = (u) => u } = {}) {
|
|
1291
1314
|
const appOrigin = (() => { try { return new URL(proxyBase).origin; } catch { return ''; } })();
|
|
1292
1315
|
const own = (u) => { try { return origins.includes(new URL(u).origin); } catch { return false; } };
|
|
@@ -1376,6 +1399,7 @@ export function libraryPayload(reads = {}, { view = 'library', origins = [], pro
|
|
|
1376
1399
|
// The channels a post can be scheduled to right now: schedule_post's ids, from the connections that are on.
|
|
1377
1400
|
// Google Business Profile is left out because schedule_post refuses it at enqueue (held back by Google).
|
|
1378
1401
|
out.channels = [...new Set(out.connections.filter((c) => c.connected).flatMap((c) => CHANNELS_OF[c.provider] || []))].filter((id) => id !== 'google_business').map((id) => ({ id, label: CHANNEL_LABEL[id] }));
|
|
1402
|
+
if (view === 'home') out.connectCard = homeConnectCard(out.connections, d.popular);
|
|
1379
1403
|
}
|
|
1380
1404
|
}
|
|
1381
1405
|
if (reads.ads) {
|
|
@@ -1821,7 +1845,7 @@ export function libraryToolDefs() {
|
|
|
1821
1845
|
// proxyBase (our /api/img), abs (absolute asset URLs), call(name, args, extra), which runs an EXISTING tool's handler
|
|
1822
1846
|
// for this session, and prefs {read, write} for the per-brand defaults this app adds.
|
|
1823
1847
|
export function registerLibraryApp(server, deps) {
|
|
1824
|
-
const { resourceMeta, origins = [], proxyBase = '', abs = (u) => u, call = async () => null, prefs = null } = deps || {};
|
|
1848
|
+
const { resourceMeta, origins = [], proxyBase = '', abs = (u) => u, call = async () => null, prefs = null, markHomeConnectCard = null } = deps || {};
|
|
1825
1849
|
const meta = libraryResourceMeta(resourceMeta, origins);
|
|
1826
1850
|
const description = meta['openai/widgetDescription'];
|
|
1827
1851
|
server.registerResource('hermoso-library-app', LIBRARY_APP_URI, { description, mimeType: LIBRARY_APP_MIME, _meta: meta },
|
|
@@ -1850,7 +1874,11 @@ export function registerLibraryApp(server, deps) {
|
|
|
1850
1874
|
const handles = {};
|
|
1851
1875
|
const reg = (name, fn) => { handles[name] = server.registerTool(name, defs[name], fn); };
|
|
1852
1876
|
|
|
1853
|
-
reg(EXT_HOME_TOOL, async (args, extra) =>
|
|
1877
|
+
reg(EXT_HOME_TOOL, async (args, extra) => {
|
|
1878
|
+
const out = await answer(args, extra, ['brand', 'brands', 'library', 'jobs', 'prefs', 'connectors'], 'home');
|
|
1879
|
+
if (out?.structuredContent?.connectCard && typeof markHomeConnectCard === 'function') { try { Promise.resolve(markHomeConnectCard()).catch(() => {}); } catch {} } // counted on the admin Funnel tab; never holds the screen up
|
|
1880
|
+
return out;
|
|
1881
|
+
});
|
|
1854
1882
|
reg(EXT_ENTRY_TOOL, async (args, extra) => answer(args, extra, ['library', 'jobs', 'brands', 'calendar'], PANEL_VIEWS.includes(args?.view) ? args.view : 'library'));
|
|
1855
1883
|
reg(EXT_BRIEF_TOOL, async (args, extra) => answer(args, extra, ['brand', 'brands', 'library', 'creators', 'products', 'prefs'], 'create', { surface: 'card', payload: { brief: { format: args?.format === 'video' ? 'video' : args?.format === 'image' ? 'image' : '', product: clip(args?.product, 600) } } }));
|
|
1856
1884
|
reg(EXT_SETUP_TOOL, async (args, extra) => answer(args, extra, ['brand', 'brands', 'connectors'], 'setup', { surface: 'card', payload: { step: args?.step === 'connections' ? 'connections' : 'brand' } }));
|
package/mcp/tool-hints.mjs
CHANGED
|
@@ -109,6 +109,25 @@ export function videoChoiceText(tool, choice, opts = {}) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
// ── A FINISHED RENDER, READ FROM THE REPLY (2026-11-02) ─────────────────────────────────────────────────────────────
|
|
113
|
+
// Which replies may carry the "post this" next step (lib/connect-prompt.mjs): the tools that make a file, and only once
|
|
114
|
+
// that file exists. A hosted video answers {stillRendering} and arrives through get_job, so get_job is on the list and a
|
|
115
|
+
// queued, running or failed job is not a finished render. Pure, no imports — the twin rule above.
|
|
116
|
+
export const RENDER_REPLY_TOOLS = Object.freeze(['generate_image', 'generate_video', 'render_ad', 'make_template_ad', 'make_motion_film', 'make_thumbnail', 'make_explainer', 'generate_avatar', 'stitch_video', 'product_sizzle', 'recast_motion', 'finish_video', 'multiply_ad', 'clone_video', 'clone_static', 'remix_static', 'get_job']);
|
|
117
|
+
const _VIDEO_FILE = /\.(mp4|mov|webm|m4v)(\?|#|$)/i, _IMAGE_FILE = /\.(png|jpe?g|webp|gif)(\?|#|$)/i;
|
|
118
|
+
export function finishedRenderKind(tool, r) {
|
|
119
|
+
if (!RENDER_REPLY_TOOLS.includes(String(tool || '')) || !r || r.isError || !Array.isArray(r.content)) return '';
|
|
120
|
+
const s = r.structuredContent;
|
|
121
|
+
if (!s || typeof s !== 'object' || s.stillRendering) return '';
|
|
122
|
+
if (typeof s.status === 'string' && s.status && s.status !== 'done') return '';
|
|
123
|
+
const inner = [s, s.raw, s.result, s.result && s.result.data].filter((x) => x && typeof x === 'object');
|
|
124
|
+
const has = (k) => inner.some((x) => (Array.isArray(x[k]) ? x[k].length > 0 : !!x[k]));
|
|
125
|
+
const url = String(s.url || '');
|
|
126
|
+
if (has('video') || _VIDEO_FILE.test(url)) return 'video';
|
|
127
|
+
if (has('image') || has('images') || has('thumbnails') || has('slides') || _IMAGE_FILE.test(url)) return 'image';
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
// ── OUT OF CREDITS ON A HOST THAT FORBIDS SELLING CREDITS (2026-09-30, OpenAI's plugin guidelines) ─────────────────
|
|
113
132
|
// "Plugins may: explain that a certain feature is not available with the user's current plan or entitlement; link to
|
|
114
133
|
// an informational page … Plugins may not: link directly to a checkout … or promote upgrades." ChatGPT is not offered
|
package/mcp/tools.mjs
CHANGED
|
@@ -32,7 +32,7 @@ function metaAlternativeNote(name) { return metaAlternativeNoteOf(canonicalToolN
|
|
|
32
32
|
import { toolCostClass, costLabel, costKindOf, creditRangeFrom } from './tool-cost.mjs';
|
|
33
33
|
import { recordToolOutcome, toolHealth, healthLabel, healthPenalty } from './tool-health.mjs';
|
|
34
34
|
// THE NEXT STEP, NAMED. The prose we already write stays; this is the same advice as an addressable field.
|
|
35
|
-
import { withHints, videoChoiceText, videoChoiceHints, neutralCreditsText, creditsLowHints, welcomeClaimHint } from './tool-hints.mjs';
|
|
35
|
+
import { withHints, hintsOf, videoChoiceText, videoChoiceHints, neutralCreditsText, creditsLowHints, welcomeClaimHint, finishedRenderKind } from './tool-hints.mjs';
|
|
36
36
|
// THE CHATGPT EXTENSIONS APP (home, Library panel, concept picker, brief, setup, settings, @-mentions), registered only
|
|
37
37
|
// behind HERMOSO_CHATGPT_EXT=1 + `?ext=1` on a widget host — see registerChatGPTExtensions() below and
|
|
38
38
|
// ./openai-extensions.mjs. Same twin-safe specifier rule.
|
|
@@ -521,6 +521,28 @@ export const withAccountArg = (name, def) => {
|
|
|
521
521
|
return { ...def, inputSchema: { ...s, account: ACCOUNT_ARG } };
|
|
522
522
|
};
|
|
523
523
|
|
|
524
|
+
// A FINISHED RENDER ON AN ACCOUNT WITH NOTHING CONNECTED ENDS BY OFFERING TO POST IT (2026-11-02). The server decides
|
|
525
|
+
// (once, at most twice, never after a connection; lib/connect-prompt.mjs) and words it, so every host and an older CLI
|
|
526
|
+
// say one thing. It rides the reply and never a tool description, which OpenAI scans. A separate text block, so the block
|
|
527
|
+
// a card parses stays exactly as it was. Bounded and best effort: the render already succeeded, so a slow or failed read
|
|
528
|
+
// of this next step says nothing and costs the caller nothing. The REST passthrough (/v1/tools, the one store with no
|
|
529
|
+
// `client`) is an integration parsing a response, not a person in a chat, so it is left out.
|
|
530
|
+
const CONNECT_PROMPT_WAIT_MS = 2000;
|
|
531
|
+
export async function withConnectPrompt(tool, r, post = apiPost) {
|
|
532
|
+
try {
|
|
533
|
+
const kind = finishedRenderKind(tool, r);
|
|
534
|
+
if (!kind) return r;
|
|
535
|
+
const store = mcpCtx.getStore();
|
|
536
|
+
if (store && (store.connectPromptQuiet || (store.remote && store.client === undefined))) return r;
|
|
537
|
+
const sc = r.structuredContent || {};
|
|
538
|
+
const job = String(sc.jobId || sc.id || '').slice(0, 80);
|
|
539
|
+
const client = store ? String(store.client || '') : 'hermoso';
|
|
540
|
+
const d = await Promise.race([Promise.resolve(post('/api/mcp/connect-prompt', { tool, kind, job, client })).catch(() => null), new Promise((res) => { const t = setTimeout(() => res(null), CONNECT_PROMPT_WAIT_MS); if (t && typeof t.unref === 'function') t.unref(); })]);
|
|
541
|
+
if (!d || d.show !== true || typeof d.text !== 'string' || !d.text) return r;
|
|
542
|
+
const out = { ...r, content: [...r.content, { type: 'text', text: d.text }], structuredContent: { ...sc, connectPrompt: { channels: Array.isArray(d.channels) ? d.channels : [] } } };
|
|
543
|
+
return withHints(out, [...hintsOf(r), { do: 'offer to post this; hand the user one of the connect links in this reply', why: 'no publishing channel is connected, and connecting needs a browser sign-in on the platform' }]);
|
|
544
|
+
} catch { return r; }
|
|
545
|
+
}
|
|
524
546
|
const wrap = (fn) => {
|
|
525
547
|
// A NAMED closure, so registerTools' proxy can stamp the tool name onto the very function it registers and this
|
|
526
548
|
// body can read it back. Reading a name off `fn` instead would be wrong: `fn` is the inner handler and the thing
|
|
@@ -560,7 +582,7 @@ const wrap = (fn) => {
|
|
|
560
582
|
}
|
|
561
583
|
}
|
|
562
584
|
} catch {}
|
|
563
|
-
return r;
|
|
585
|
+
return await withConnectPrompt(_tool, r);
|
|
564
586
|
}
|
|
565
587
|
catch (e) {
|
|
566
588
|
try { recordToolOutcome(_tool, { ok: false, ms: Date.now() - _t0 }); } catch {}
|
|
@@ -1020,6 +1042,7 @@ const AD_RESULT_HTML = String.raw`<div id="root"></div>
|
|
|
1020
1042
|
.dl { border: 1px solid rgba(128,128,128,.45); border-radius: 999px; padding: 2px 10px; font-size: 12px; font-weight: 600;
|
|
1021
1043
|
text-decoration: none; color: inherit; opacity: .8; }
|
|
1022
1044
|
.dl:hover { opacity: 1; background: rgba(128,128,128,.14); }
|
|
1045
|
+
.next { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 0 12px 10px; font-size: 12.5px; }
|
|
1023
1046
|
.spacer { flex: 1; }
|
|
1024
1047
|
.wordmark { font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .4; }
|
|
1025
1048
|
.empty { padding: 18px 16px; font-size: 13px; opacity: .75; }
|
|
@@ -1041,6 +1064,28 @@ const AD_RESULT_HTML = String.raw`<div id="root"></div>
|
|
|
1041
1064
|
// free, read-only get_job over the host bridge (get_job carries openai/widgetAccessible for exactly this) until
|
|
1042
1065
|
// the media exists, then renders it. The model's own get_job loop still works and is unaffected — whichever
|
|
1043
1066
|
// arrives first wins, because both paths land in the same view().
|
|
1067
|
+
// THE "POST THIS" LINKS (2026-11-02). On ChatGPT a hosted video finishes through THIS card's get_job poll, so that answer
|
|
1068
|
+
// never reaches the model; without a row here the next step could be said to nobody. Read from structuredContent, or
|
|
1069
|
+
// from the prompt's text block on a host that hands callTool the content array alone. Only our own app's connect links.
|
|
1070
|
+
var postLinks = [];
|
|
1071
|
+
function postLinksFrom(r) {
|
|
1072
|
+
if (!r || typeof r !== 'object') return [];
|
|
1073
|
+
var okUrl = function (u) { return /^https:\/\/([a-z0-9-]+\.)*hermoso\.ai\/\?connect=[a-z_]{1,40}(&[A-Za-z0-9_=-]+)*$/.test(String(u || '')); };
|
|
1074
|
+
var q = [r, r.structuredContent, r.toolOutput, r.result], out = [];
|
|
1075
|
+
for (var i = 0; i < q.length; i++) {
|
|
1076
|
+
var ch = q[i] && q[i].connectPrompt && q[i].connectPrompt.channels;
|
|
1077
|
+
if (Array.isArray(ch)) { for (var j = 0; j < ch.length; j++) if (ch[j] && ch[j].label && okUrl(ch[j].url)) out.push({ label: String(ch[j].label), url: String(ch[j].url) }); }
|
|
1078
|
+
if (out.length) return out;
|
|
1079
|
+
}
|
|
1080
|
+
var blocks = Array.isArray(r.content) ? r.content : (r.result && Array.isArray(r.result.content) ? r.result.content : []);
|
|
1081
|
+
for (var b = 0; b < blocks.length && b < 8; b++) {
|
|
1082
|
+
var t = blocks[b] && blocks[b].text;
|
|
1083
|
+
if (typeof t !== 'string' || t.indexOf('?connect=') < 0) continue;
|
|
1084
|
+
var lines = t.split('\n');
|
|
1085
|
+
for (var n = 0; n < lines.length; n++) { var m = /^- (.+): (https:\/\/\S+)$/.exec(lines[n]); if (m && okUrl(m[2])) out.push({ label: m[1], url: m[2] }); }
|
|
1086
|
+
}
|
|
1087
|
+
return out;
|
|
1088
|
+
}
|
|
1044
1089
|
var live = null; // the payload OUR poll resolved — it is newer than a frozen toolOutput, so it wins
|
|
1045
1090
|
var polls = 0, timer = null, pollErr = '', missed = 0, timeouts = 0, busy = 0, backoff = 0, gone = false, lastJob = null;
|
|
1046
1091
|
// MAX_MISSES IS A TIME BUDGET, NOT A COUNT, AND 3 WAS SHORTER THAN A RENDER (2026-08-24). With the backoff
|
|
@@ -1262,6 +1307,7 @@ const AD_RESULT_HTML = String.raw`<div id="root"></div>
|
|
|
1262
1307
|
var guard = setTimeout(function () { if (settled) return; settled = true; timeouts++; backoff++; pollErr = 'timeout'; render(); }, POLL_TIMEOUT_MS); // a bridge that did not answer in 8s must not be hit again in 5
|
|
1263
1308
|
api.callTool('get_job', { id: v.jobId }).then(once(function (r) {
|
|
1264
1309
|
var d = jobFrom(r);
|
|
1310
|
+
var pl = postLinksFrom(r); if (pl.length) postLinks = pl;
|
|
1265
1311
|
if (d && typeof d === 'object' && d.status === 'not_found') { gone = true; return; } // FINAL — one answer settles it; burning three misses on it would only delay the truth
|
|
1266
1312
|
// A POLL ANSWER MUST BE A get_job RESULT, AND get_job ALWAYS CARRIES A status FIELD (2026-08-24).
|
|
1267
1313
|
// Measured: 29 consecutive polls, every one readable, not one of them ever done, against a job that had
|
|
@@ -1351,7 +1397,9 @@ const AD_RESULT_HTML = String.raw`<div id="root"></div>
|
|
|
1351
1397
|
return;
|
|
1352
1398
|
}
|
|
1353
1399
|
if (!body) { root.innerHTML = ''; stop(); collapse(); return; }
|
|
1354
|
-
|
|
1400
|
+
var _ol = postLinksFrom({ structuredContent: (window.openai && window.openai.toolOutput) || null }); if (_ol.length) postLinks = _ol;
|
|
1401
|
+
var nextRow = (!v.pending && !v.error && postLinks.length) ? '<div class="next"><span>Post this:</span>' + postLinks.map(function (l) { return '<a class="dl" href="' + esc(l.url) + '" target="_blank" rel="noopener">' + esc(l.label) + '</a>'; }).join('') + '</div>' : '';
|
|
1402
|
+
root.innerHTML = '<div class="card">' + body + footer + nextRow + '</div>';
|
|
1355
1403
|
if (v.pending) { schedule(); } else { stop(); }
|
|
1356
1404
|
}
|
|
1357
1405
|
render();
|
|
@@ -3546,11 +3594,14 @@ function registerChatGPTExtensions(rawServer, ctx, opts) {
|
|
|
3546
3594
|
// unchanged (awaitRenderJob reads waitMs only after submitJob).
|
|
3547
3595
|
call: (name, args, extra, o) => {
|
|
3548
3596
|
const s = ctx.srcOf[name]; if (!s || typeof s.handler !== 'function') return null;
|
|
3549
|
-
|
|
3597
|
+
// connectPromptQuiet: the app reads through the tool handlers, and that answer goes to a screen rather than the
|
|
3598
|
+
// model, so it must not spend a showing of the "post this" next step (withConnectPrompt).
|
|
3599
|
+
const run = () => { const cur = mcpCtx.getStore(); return cur ? mcpCtx.run({ ...cur, connectPromptQuiet: true }, () => s.handler(args || {}, extra)) : s.handler(args || {}, extra); };
|
|
3550
3600
|
const store = o && o.noWait ? mcpCtx.getStore() : null;
|
|
3551
3601
|
return store ? mcpCtx.run({ ...store, waitMs: 0 }, run) : run();
|
|
3552
3602
|
},
|
|
3553
3603
|
prefs: { read: () => readStore(EXT_PREFS_KEY), write: (v) => writeStore(EXT_PREFS_KEY, v) },
|
|
3604
|
+
markHomeConnectCard: () => apiPost('/api/mcp/connect-prompt/home', {}),
|
|
3554
3605
|
});
|
|
3555
3606
|
installEntrypointIcons(rawServer);
|
|
3556
3607
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.496",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1109 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 30 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",
|