hermoso 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/bin/hermoso.mjs +59 -6
  2. package/package.json +1 -1
package/bin/hermoso.mjs CHANGED
@@ -43,14 +43,23 @@ async function main() {
43
43
  const [group, sub] = pos;
44
44
  const cfg = await loadConfig();
45
45
 
46
- // ---- auth: no network call today; records base URL + optional token (the OAuth seam) ----
46
+ // ---- auth: `login` opens the browser to mint + store an agent key (nothing to paste — matches the app's OAuth
47
+ // connectors); `--token <key>` is the manual fallback; a localhost base needs no auth (dev). ----
47
48
  if (group === 'auth') {
48
49
  if (sub === 'logout') { await saveConfig({}); return console.log('✓ Logged out (cleared ~/.hermoso/config.json)'); }
49
- const next = { apiBase: flags.url || cfg.apiBase || 'https://app.hermoso.ai', token: flags.token || cfg.token || '', profile: flags.profile || cfg.profile || 'default' };
50
- await saveConfig(next);
51
- console.log(`✓ Saved. API: ${next.apiBase}${next.token ? ' · token stored' : ''}`);
52
- if (!next.token) console.log(' Next: create an agent key at app.hermoso.ai Settings Agents & API, then run: hermoso auth login --token <key>');
53
- return;
50
+ const apiBase = (flags.url || cfg.apiBase || 'https://app.hermoso.ai').replace(/\/$/, '');
51
+ const profile = flags.profile || cfg.profile || 'default';
52
+ const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(apiBase);
53
+ if (flags.token) { await saveConfig({ apiBase, token: String(flags.token), profile }); return console.log(`✓ Signed in key stored (~/.hermoso/config.json). API: ${apiBase}`); }
54
+ if (isLocal) { await saveConfig({ apiBase, token: '', profile }); return console.log(`✓ Local dev — no auth required. API: ${apiBase}`); }
55
+ if (sub === 'login') { // browser sign-in: spin a loopback server, open the app's /?cliauth page, receive a minted key
56
+ const key = await browserLogin(apiBase);
57
+ if (!key) return die(`Sign-in didn’t complete. Re-run "hermoso auth login", or paste a key: hermoso auth login --token <key> (create one in the app under Agents & API keys).`);
58
+ await saveConfig({ apiBase, token: key, profile });
59
+ return console.log(`✓ Signed in — key stored (~/.hermoso/config.json). API: ${apiBase}`);
60
+ }
61
+ await saveConfig({ apiBase, token: cfg.token || '', profile }); // bare `auth` / `auth --url …`: just record the base
62
+ return console.log(`✓ Saved. API: ${apiBase}${cfg.token ? ' · token stored' : ''}`);
54
63
  }
55
64
  if (group === 'version' || flags.version) {
56
65
  console.log(`hermoso-cli 1.0.0 · API ${cfg.apiBase || process.env.HERMOSO_API_BASE || 'https://app.hermoso.ai'} · ${cfg.token ? 'authed' : 'no token — run: hermoso auth login --token <key>'}`);
@@ -145,4 +154,48 @@ add --json to any command for machine output.`);
145
154
  }
146
155
  } catch (e) { die(e?.message || String(e)); }
147
156
  }
157
+
158
+ // ---- browser sign-in (loopback OAuth, like gh/firebase): spin a 127.0.0.1 server, open the app's cli-auth page,
159
+ // which mints an agent key and redirects the browser back to our loopback with ?key=&state=. Nothing is pasted;
160
+ // the key transits only the user's own machine. Times out after 3 min. Returns the hmk_ key, or null. ----
161
+ async function browserLogin(apiBase) {
162
+ const http = await import('node:http');
163
+ const crypto = await import('node:crypto');
164
+ const state = crypto.randomBytes(16).toString('hex');
165
+ return await new Promise((resolve) => {
166
+ let done = false;
167
+ const finish = (val) => { if (done) return; done = true; try { server.close(); } catch {} resolve(val); };
168
+ const server = http.createServer((req, res) => {
169
+ try {
170
+ const u = new URL(req.url, 'http://127.0.0.1');
171
+ if (u.pathname === '/favicon.ico') { res.writeHead(204); return res.end(); }
172
+ const key = u.searchParams.get('key') || '';
173
+ const st = u.searchParams.get('state') || '';
174
+ const ok = st === state && /^hmk_[A-Za-z0-9_-]{20,}$/.test(key);
175
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
176
+ res.end(cliAuthPage(ok ? 'You’re signed in ✓' : 'Sign-in didn’t complete', ok ? 'Hermoso CLI is connected. You can close this tab and return to your terminal.' : 'Please close this tab and run the command again.'));
177
+ finish(ok ? key : null);
178
+ } catch { try { res.writeHead(500); res.end('error'); } catch {} finish(null); }
179
+ });
180
+ server.on('error', () => finish(null));
181
+ server.listen(0, '127.0.0.1', () => {
182
+ const port = server.address().port;
183
+ const authUrl = `${apiBase}/?cliauth=1&port=${port}&state=${state}`;
184
+ console.log(`\nOpening your browser to sign in…\nIf it doesn’t open, paste this into your browser:\n ${authUrl}\n`);
185
+ openBrowser(authUrl);
186
+ });
187
+ setTimeout(() => finish(null), 180000);
188
+ });
189
+ }
190
+ function openBrowser(url) {
191
+ import('node:child_process').then(({ spawn }) => {
192
+ const plat = process.platform;
193
+ const cmd = plat === 'darwin' ? 'open' : plat === 'win32' ? 'cmd' : 'xdg-open';
194
+ const args = plat === 'win32' ? ['/c', 'start', '', url] : [url];
195
+ try { const c = spawn(cmd, args, { stdio: 'ignore', detached: true }); c.on('error', () => {}); c.unref(); } catch {}
196
+ }).catch(() => {});
197
+ }
198
+ function cliAuthPage(title, body) {
199
+ return `<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Hermoso CLI</title><body style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:#0e0e10;color:#f4f1ea;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0"><div style="text-align:center;max-width:420px;padding:32px"><div style="font-family:Georgia,'Times New Roman',serif;font-size:24px;font-weight:700;letter-spacing:-.5px;margin-bottom:18px">hermoso<span style="color:#d9714e">.ai</span></div><h1 style="font-size:20px;margin:14px 0 8px;font-weight:600">${title}</h1><p style="color:#a7a29a;line-height:1.55;font-size:15px">${body}</p></div></body>`;
200
+ }
148
201
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Drive Hermoso — the AI ad studio — from any AI agent: MCP server, CLI, and Claude skills for researching winning ads and generating finished image & video ads.",
5
5
  "type": "module",
6
6
  "bin": { "hermoso": "bin/hermoso.mjs" },