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 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 */ }