hermoso 0.1.410 → 0.1.413
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/README.md +5 -0
- package/bin/hermoso.mjs +16 -0
- package/mcp/client.mjs +17 -0
- package/mcp/tools.mjs +397 -120
- package/package.json +2 -2
- package/skills/hermoso-ad-from-brand/SKILL.md +1 -1
- package/skills/hermoso-generate/SKILL.md +1 -1
- package/skills/hermoso-marketing/SKILL.md +1 -1
- package/skills/hermoso-product-photoshoot/SKILL.md +1 -1
- package/skills/hermoso-research/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,11 @@ below). The ChatGPT desktop app and ChatGPT workspace admins can also read this
|
|
|
94
94
|
No browser on the machine? Create a key in the app under **MCP & CLI** and run
|
|
95
95
|
`npx -y hermoso auth login --token <key>`.
|
|
96
96
|
|
|
97
|
+
On a server, a CI job or an agent gateway that calls Hermoso many times a day, install it once
|
|
98
|
+
(`npm i -g hermoso`) and call `hermoso` directly. `npx` asks the npm registry about the package on every run,
|
|
99
|
+
and a busy machine can be rate-limited by npm. If the CLI ever says an HTTP error "did not come from Hermoso",
|
|
100
|
+
something on your side (a proxy, a firewall or the gateway) refused the request before it reached us.
|
|
101
|
+
|
|
97
102
|
## Which surface should your agent use?
|
|
98
103
|
|
|
99
104
|
Two shapes, and the right one is decided by **what your client can do**, not by which we prefer.
|
package/bin/hermoso.mjs
CHANGED
|
@@ -70,6 +70,21 @@ function parse(argv) {
|
|
|
70
70
|
|
|
71
71
|
const die = (msg) => { console.error('✗ ' + msg); process.exit(1); };
|
|
72
72
|
|
|
73
|
+
// RUN THROUGH npx, EVERY CALL ASKS THE NPM REGISTRY (2026-10-18). Measured: `npx -y hermoso …` revalidates the package
|
|
74
|
+
// with registry.npmjs.org on every run, even with it cached, and an npm 429 kills the command before this file runs
|
|
75
|
+
// (npm prints "E429 … Too Many Requests"). An agent on a server calling Hermoso hundreds of times an hour through npx
|
|
76
|
+
// is exactly what npm rate-limits, and a paying customer's gateway spent a day on that 429 believing Hermoso had
|
|
77
|
+
// blocked him. So when this CLI is running through npx it says once a week, on stderr (never in --json output),
|
|
78
|
+
// that installing it once avoids the registry entirely. Not for `mcp`: an MCP host starts it once per session.
|
|
79
|
+
const ranViaNpx = (argv1 = process.argv[1], env = process.env) => /[\\/]_npx[\\/]/.test(String(argv1 || '')) || env.npm_command === 'exec';
|
|
80
|
+
async function npxTip(group) {
|
|
81
|
+
if (group === 'mcp' || !ranViaNpx()) return;
|
|
82
|
+
const mark = path.join(CONFIG_DIR, 'npx-tip');
|
|
83
|
+
try { if (Date.now() - Number(await readFile(mark, 'utf8')) < 7 * 86400e3) return; } catch { /* first time */ }
|
|
84
|
+
try { await mkdir(CONFIG_DIR, { recursive: true }); await writeFile(mark, String(Date.now())); } catch { /* best effort */ }
|
|
85
|
+
console.error('tip: this ran through npx, which asks the npm registry on every call. For an agent or a server making many calls, install once with `npm install -g hermoso` and run `hermoso` directly: npm rate-limits busy machines (an HTTP 429 from npm, before Hermoso is reached).');
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
async function main() {
|
|
74
89
|
const { pos, flags } = parse(process.argv.slice(2));
|
|
75
90
|
const [group, sub] = pos;
|
|
@@ -101,6 +116,7 @@ async function main() {
|
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
// resolve API base + token from config (env overrides), then load the shared client
|
|
119
|
+
await npxTip(group);
|
|
104
120
|
process.env.HERMOSO_API_BASE = process.env.HERMOSO_API_BASE || cfg.apiBase || 'https://app.hermoso.ai';
|
|
105
121
|
if (cfg.token && !process.env.HERMOSO_TOKEN) process.env.HERMOSO_TOKEN = cfg.token;
|
|
106
122
|
if (cfg.profile && cfg.profile !== 'default' && !process.env.HERMOSO_PROFILE) process.env.HERMOSO_PROFILE = cfg.profile;
|
package/mcp/client.mjs
CHANGED
|
@@ -246,8 +246,25 @@ export function isFrontDoorRefusal(status, contentType, { read = false } = {}) {
|
|
|
246
246
|
return st === 429 || (read && (st === 502 || st === 503 || st === 504));
|
|
247
247
|
}
|
|
248
248
|
export const FRONT_DOOR_MESSAGE = 'Hermoso\u2019s server was briefly too busy to take the request and turned it away before the app saw it (HTTP 429). It was retried and the server was still busy. Nothing ran and nothing was charged: try again in a minute.';
|
|
249
|
+
// A REFUSAL THAT NEVER CAME FROM HERMOSO IS SAID TO BE ONE (2026-10-18). A paying customer's agent gateway got HTTP 429 on
|
|
250
|
+
// every call for a day; the CLI printed FRONT_DOOR_MESSAGE ("Hermoso's server was briefly too busy"), so he and his
|
|
251
|
+
// agent asked us to unblock his IP. Cloudflare's own logs held ZERO requests from that IP: the 429 was answered by
|
|
252
|
+
// something between his machine and us. Every reply from the hosted API, the edge's own refusals included, carries
|
|
253
|
+
// Cloudflare's cf-ray and/or our x-request-id; a refusal with neither did not come from Hermoso, and saying so is the
|
|
254
|
+
// whole diagnosis. Only judged against the hosted API (a self-hosted base has no edge). PURE, so a check runs it.
|
|
255
|
+
export const HOSTED_API_RE = /^https:\/\/app\.hermoso\.ai$/i;
|
|
256
|
+
export function foreignRefusal(status, get, apiBase = API_BASE, env = process.env) {
|
|
257
|
+
const st = Number(status);
|
|
258
|
+
if (!(st >= 400) || !HOSTED_API_RE.test(String(apiBase || ''))) return null;
|
|
259
|
+
const h = (k) => { try { return String(get?.(k) || ''); } catch { return ''; } };
|
|
260
|
+
if (h('cf-ray') || h('x-request-id')) return null;
|
|
261
|
+
const via = h('server') ? ` (it answered as "${h('server').slice(0, 60)}")` : '';
|
|
262
|
+
const proxy = env?.HTTPS_PROXY || env?.https_proxy || env?.HTTP_PROXY || env?.http_proxy ? ' This machine sends web traffic through a proxy (HTTPS_PROXY is set), which is the likeliest source.' : '';
|
|
263
|
+
return `HTTP ${st} did not come from Hermoso${via}: the reply carries none of the headers every Hermoso response has, so something between this machine and ${apiBase} refused the request, such as a proxy, a firewall or the gateway this runs on.${proxy} Nothing reached Hermoso and nothing was charged. Hermoso itself did not throttle or block this account or address.`;
|
|
264
|
+
}
|
|
249
265
|
async function throughFrontDoor(send, { read }) {
|
|
250
266
|
let res = await send();
|
|
267
|
+
{ const foreign = foreignRefusal(res.status, (k) => res.headers?.get?.(k)); if (foreign) { try { await res.arrayBuffer(); } catch { /* drain */ } throw Object.assign(new Error(foreign), { status: res.status, _foreign: true }); } }
|
|
251
268
|
for (const ms of frontDoorWaits()) {
|
|
252
269
|
if (!isFrontDoorRefusal(res.status, res.headers?.get?.('content-type'), { read })) return res;
|
|
253
270
|
try { await res.arrayBuffer(); } catch { /* drain, so the socket is reused */ }
|