vibespot 1.5.0 → 1.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,11 +19,18 @@
19
19
  "build": "tsup",
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
+ "eval": "tsx test/eval/run-eval.ts",
23
+ "benchmark": "tsx test/eval/benchmark.ts",
24
+ "prompts:pull": "tsx scripts/sync-prompts.ts",
25
+ "prompts:seed": "tsx scripts/sync-prompts.ts --from-local",
22
26
  "prepublishOnly": "npm run build",
23
- "contact-monitor": "tsx scripts/contact-monitor.ts"
27
+ "contact-monitor": "tsx scripts/contact-monitor.ts",
28
+ "docker:publish": "scripts/docker-publish.sh",
29
+ "docker:publish:multi": "scripts/docker-publish.sh --multi-arch",
30
+ "docker:smoke": "scripts/docker-publish.sh --smoke --dry-run"
24
31
  },
25
32
  "dependencies": {
26
- "@anthropic-ai/sdk": "^0.92.0",
33
+ "@anthropic-ai/sdk": "^0.96.0",
27
34
  "@clack/prompts": "^1.3.0",
28
35
  "@codemirror/lang-css": "^6.3.1",
29
36
  "@codemirror/lang-html": "^6.4.11",
package/ui/chat.js CHANGED
@@ -29,6 +29,10 @@ let changedModulesInRun = new Set();
29
29
  let highlightOnNextModulesUpdated = false;
30
30
  let changedListClearTimer = null;
31
31
 
32
+ // VIB-1770 — the assistant bubble of the just-finished generation, so the
33
+ // trailing `generation_cost` event can append its estimated cost line to it.
34
+ let lastGenerationBubbleEl = null;
35
+
32
36
  const messagesEl = document.getElementById("chat-messages");
33
37
  const inputEl = document.getElementById("chat-input");
34
38
  const sendBtn = document.getElementById("chat-send");
@@ -605,6 +609,9 @@ function handleWsMessage(msg) {
605
609
  if (chatHeaderTitle) chatHeaderTitle.textContent = msg.themeName || "Chat";
606
610
  if (chatHeaderContext) chatHeaderContext.textContent = msg.engine || "";
607
611
 
612
+ // Hydrate the per-project generation cost chip (VIB-1770)
613
+ updateProjectCostChip(msg.costTotal);
614
+
608
615
  // Restore chat history from server
609
616
  if (msg.messages && msg.messages.length > 0) {
610
617
  for (const m of msg.messages) {
@@ -744,6 +751,9 @@ function handleWsMessage(msg) {
744
751
  case "pipeline_partial":
745
752
  handlePipelinePartial(msg);
746
753
  break;
754
+ case "generation_cost":
755
+ handleGenerationCost(msg);
756
+ break;
747
757
  case "agentic_prompt":
748
758
  handleAgenticPrompt();
749
759
  break;
@@ -1270,6 +1280,10 @@ function handlePipelineComplete(msg) {
1270
1280
  }
1271
1281
  bubble.appendChild(stats);
1272
1282
 
1283
+ // Remember this bubble so the trailing `generation_cost` event (sent right
1284
+ // after completion) can append its cost line to the same message (VIB-1770).
1285
+ lastGenerationBubbleEl = bubble;
1286
+
1273
1287
  resetPipelineState();
1274
1288
  }
1275
1289
 
@@ -1298,6 +1312,8 @@ function handlePipelinePartial(msg) {
1298
1312
  stats.textContent = `${msg.succeeded.length} modules succeeded, ${msg.failed.length} failed in ${duration}`;
1299
1313
  bubble.appendChild(stats);
1300
1314
 
1315
+ lastGenerationBubbleEl = bubble;
1316
+
1301
1317
  resetPipelineState();
1302
1318
  }
1303
1319
 
@@ -1312,6 +1328,89 @@ function resetPipelineState() {
1312
1328
  renderChatSuggestions();
1313
1329
  }
1314
1330
 
1331
+ // ---------------------------------------------------------------------------
1332
+ // Generation cost (VIB-1770) — per-page estimate + per-project running total
1333
+ // ---------------------------------------------------------------------------
1334
+
1335
+ // Format an estimated USD amount. Sub-dollar costs show 4 decimals so a
1336
+ // fraction of a cent is still legible; larger amounts round to cents.
1337
+ function formatUsd(usd) {
1338
+ const n = Number(usd) || 0;
1339
+ if (n > 0 && n < 0.01) return "$" + n.toFixed(4);
1340
+ if (n < 1) return "$" + n.toFixed(3);
1341
+ return "$" + n.toFixed(2);
1342
+ }
1343
+
1344
+ function formatTokenCount(tokens) {
1345
+ const n = Number(tokens) || 0;
1346
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
1347
+ if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
1348
+ return String(n);
1349
+ }
1350
+
1351
+ // Build the per-page cost summary text from a PageCost object.
1352
+ // `costComplete === false` means some model wasn't in the price table, so the
1353
+ // dollar figure is a lower bound — prefix "≥" and explain it via the title.
1354
+ function buildCostText(cost) {
1355
+ if (!cost || !cost.calls) return null;
1356
+ const tokens = formatTokenCount(cost.totalTokens) + " tokens";
1357
+ if (cost.costUsd > 0 || cost.costComplete) {
1358
+ const prefix = cost.costComplete ? "" : "≥ ";
1359
+ return "Est. " + prefix + formatUsd(cost.costUsd) + " · " + tokens;
1360
+ }
1361
+ // No priced calls at all — show tokens only.
1362
+ return tokens;
1363
+ }
1364
+
1365
+ // Append (or replace) the cost line on a finished generation's bubble.
1366
+ function renderCostLineOnBubble(bubble, cost) {
1367
+ if (!bubble) return;
1368
+ const text = buildCostText(cost);
1369
+ if (!text) return;
1370
+ let line = bubble.querySelector(".pipeline-cost");
1371
+ if (!line) {
1372
+ line = document.createElement("div");
1373
+ line.className = "pipeline-cost";
1374
+ bubble.appendChild(line);
1375
+ }
1376
+ line.textContent = text;
1377
+ if (cost.costComplete === false) {
1378
+ line.title = "Some model calls had no price data — this is a lower-bound estimate.";
1379
+ } else {
1380
+ line.title = "Estimated cost of this generation. Local estimate from public model prices; not a bill.";
1381
+ }
1382
+ }
1383
+
1384
+ // Live `generation_cost` event — arrives right after pipeline completion.
1385
+ function handleGenerationCost(msg) {
1386
+ if (msg.cost) renderCostLineOnBubble(lastGenerationBubbleEl, msg.cost);
1387
+ lastGenerationBubbleEl = null;
1388
+ updateProjectCostChip(msg.projectTotal);
1389
+ }
1390
+
1391
+ // Update the persistent per-project cost chip in the chat header. Hidden when
1392
+ // there's no cost yet (new project / CLI engine).
1393
+ function updateProjectCostChip(total) {
1394
+ const chip = document.getElementById("chat-cost-total");
1395
+ if (!chip) return;
1396
+ if (!total || !total.generations) {
1397
+ chip.classList.add("hidden");
1398
+ chip.textContent = "";
1399
+ return;
1400
+ }
1401
+ const prefix = total.costComplete === false ? "≥ " : "";
1402
+ chip.textContent = "Σ " + prefix + formatUsd(total.costUsd);
1403
+ chip.title =
1404
+ "Estimated total for this project: " +
1405
+ formatUsd(total.costUsd) +
1406
+ " over " +
1407
+ total.generations +
1408
+ " generation" + (total.generations === 1 ? "" : "s") +
1409
+ " · " + formatTokenCount(total.totalTokens) + " tokens" +
1410
+ (total.costComplete === false ? " (lower bound)" : "");
1411
+ chip.classList.remove("hidden");
1412
+ }
1413
+
1315
1414
  // ---------------------------------------------------------------------------
1316
1415
  // Smart chat suggestions — contextual next-step chips after generation
1317
1416
  // ---------------------------------------------------------------------------
@@ -2110,6 +2209,12 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
2110
2209
  }
2111
2210
  const statsClass = pipeline.stats.modulesFailed > 0 ? "pipeline-stats pipeline-stats--partial" : "pipeline-stats";
2112
2211
 
2212
+ // Per-page cost line, if this generation persisted one (VIB-1770)
2213
+ const costText = buildCostText(pipeline.cost);
2214
+ const costHtml = costText
2215
+ ? `<div class="pipeline-cost">${escapeHtml(costText)}</div>`
2216
+ : "";
2217
+
2113
2218
  div.innerHTML = `
2114
2219
  <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
2115
2220
  <div class="chat-msg__content">
@@ -2119,6 +2224,7 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
2119
2224
  ${decisionHtml}
2120
2225
  ${modulesHtml}
2121
2226
  <div class="${statsClass}">${statsText}</div>
2227
+ ${costHtml}
2122
2228
  </div>
2123
2229
  </div>`;
2124
2230
  } else {
@@ -1300,20 +1300,60 @@ testimonial carousel, and a minimal footer with social links.</code></pre>
1300
1300
  ============================================================ -->
1301
1301
  <div class="doc-section" id="docker-deployment">
1302
1302
  <h2 id="docker-deployment-heading">Docker Deployment <a href="#docker-deployment" class="doc-anchor">#</a></h2>
1303
- <p>Run vibeSpot as a containerised service for your team. The Docker image bundles the server, UI, and all dependencies &mdash; just add an AI API key.</p>
1303
+ <p>Run vibeSpot as a containerised service for your team. The Docker image bundles the server, UI, and all dependencies &mdash; just add an AI API key. The image is published to the GitHub Container Registry and is <strong>public</strong>, so no login or repository checkout is required to pull it.</p>
1304
+
1305
+ <pre><code>docker pull ghcr.io/borismichel/vibespot:latest</code></pre>
1306
+ <p>Tags: <code>latest</code> (most recent release), <code>1.5.0</code> / <code>1.5</code> (specific version), <code>main</code> (latest commit, may be unstable). Pin a version for production.</p>
1304
1307
 
1305
1308
  <h3 id="docker-quick-start">Quick Start (LAN / VPN) <a href="#docker-quick-start" class="doc-anchor">#</a></h3>
1306
- <pre><code>cp .env.example .env
1307
- # Edit .env &mdash; set at least one AI API key (e.g. ANTHROPIC_API_KEY)
1308
- docker compose up -d</code></pre>
1309
- <p>Open <code>http://&lt;host-ip&gt;:4200</code> in a browser. All AI keys, engine selection, and HubSpot credentials are configured via the <code>.env</code> file &mdash; no interactive setup needed.</p>
1309
+ <p>Run the public image directly &mdash; no repository or compose file needed:</p>
1310
+ <pre><code>docker run -d --name vibespot \
1311
+ -p 4200:4200 \
1312
+ -e VIBESPOT_AI_ENGINE=anthropic-api \
1313
+ -e VIBESPOT_AGENTIC_MODE=true \
1314
+ -e ANTHROPIC_API_KEY=sk-ant-... \
1315
+ -v vibespot-config:/home/vibespot/.vibespot \
1316
+ -v vibespot-themes:/home/vibespot/vibespot-themes \
1317
+ ghcr.io/borismichel/vibespot:latest</code></pre>
1318
+ <p>Open <code>http://&lt;host-ip&gt;:4200</code> in a browser. Swap the engine and key for whichever provider you use. The two volumes keep config and generated themes across restarts.</p>
1310
1319
 
1311
1320
  <h3 id="docker-https">HTTPS with Caddy <a href="#docker-https" class="doc-anchor">#</a></h3>
1312
- <p>For public or semi-public deployments, activate the Caddy reverse proxy profile. Caddy auto-provisions a TLS certificate from Let&rsquo;s Encrypt.</p>
1313
- <pre><code>cp .env.example .env
1314
- # Set VIBESPOT_DOMAIN=vibespot.example.com and your AI key
1315
- docker compose --profile https up -d</code></pre>
1316
- <p>Caddy binds ports 80 and 443. Point your DNS A record at the host, and HTTPS works automatically.</p>
1321
+ <p>For public or semi-public deployments, put a <a href="https://caddyserver.com/" target="_blank">Caddy</a> reverse proxy in front to auto-provision a Let&rsquo;s Encrypt TLS certificate. Save a <code>docker-compose.yml</code> and <code>Caddyfile</code> in a directory, then run <code>docker compose up -d</code>.</p>
1322
+ <pre><code># docker-compose.yml
1323
+ services:
1324
+ vibespot:
1325
+ image: ghcr.io/borismichel/vibespot:latest
1326
+ restart: unless-stopped
1327
+ environment:
1328
+ VIBESPOT_AI_ENGINE: anthropic-api
1329
+ VIBESPOT_AGENTIC_MODE: "true"
1330
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
1331
+ volumes:
1332
+ - vibespot-config:/home/vibespot/.vibespot
1333
+ - vibespot-themes:/home/vibespot/vibespot-themes
1334
+ expose: ["4200"]
1335
+ caddy:
1336
+ image: caddy:2-alpine
1337
+ restart: unless-stopped
1338
+ ports: ["80:80", "443:443"]
1339
+ volumes:
1340
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
1341
+ - caddy-data:/data
1342
+ depends_on: [vibespot]
1343
+ volumes:
1344
+ vibespot-config:
1345
+ vibespot-themes:
1346
+ caddy-data:</code></pre>
1347
+ <pre><code># Caddyfile (set your real domain)
1348
+ vibespot.example.com {
1349
+ @websockets {
1350
+ header Connection *Upgrade*
1351
+ header Upgrade websocket
1352
+ }
1353
+ reverse_proxy @websockets vibespot:4200
1354
+ reverse_proxy vibespot:4200
1355
+ }</code></pre>
1356
+ <p>Caddy binds ports 80 and 443. Point your DNS A record at the host, and HTTPS works automatically. The <code>@websockets</code> rule is required &mdash; the chat and generation pipeline run over a persistent WebSocket.</p>
1317
1357
 
1318
1358
  <h3 id="docker-env-vars">Environment Variables <a href="#docker-env-vars" class="doc-anchor">#</a></h3>
1319
1359
  <p>The Docker image reads all configuration from environment variables. The <code>.env.example</code> file documents every option.</p>
@@ -1324,7 +1364,6 @@ docker compose --profile https up -d</code></pre>
1324
1364
  </thead>
1325
1365
  <tbody>
1326
1366
  <tr><td><code>VIBESPOT_PORT</code></td><td><code>4200</code></td><td>Port the server listens on inside the container</td></tr>
1327
- <tr><td><code>VIBESPOT_DOMAIN</code></td><td>&mdash;</td><td>Public domain for the Caddy HTTPS profile</td></tr>
1328
1367
  <tr><td><code>VIBESPOT_NO_OPEN</code></td><td><code>1</code></td><td>Suppress auto-open browser (set automatically in Docker)</td></tr>
1329
1368
  <tr><td><code>VIBESPOT_AI_ENGINE</code></td><td>&mdash;</td><td>Default engine: <code>anthropic-api</code>, <code>openai-api</code>, <code>gemini-api</code>, <code>langdock-api</code></td></tr>
1330
1369
  <tr><td><code>VIBESPOT_AGENTIC_MODE</code></td><td>&mdash;</td><td>Set <code>true</code> to enable the multi-stage agentic pipeline</td></tr>
@@ -1348,9 +1387,9 @@ docker compose --profile https up -d</code></pre>
1348
1387
  <tr><td><code>vibespot-themes</code></td><td><code>/home/vibespot/vibespot-themes</code></td><td>Generated HubSpot themes</td></tr>
1349
1388
  </tbody>
1350
1389
  </table>
1351
- <p>To back up:</p>
1352
- <pre><code>docker compose cp vibespot:/home/vibespot/.vibespot ./backup-config
1353
- docker compose cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code></pre>
1390
+ <p>Mount the themes volume at exactly <code>/home/vibespot/vibespot-themes</code> &mdash; that is where the app writes generated themes. Without it, themes are lost when the container is recreated. To back up:</p>
1391
+ <pre><code>docker cp vibespot:/home/vibespot/.vibespot ./backup-config
1392
+ docker cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code></pre>
1354
1393
 
1355
1394
  <h3 id="docker-reverse-proxy">Reverse Proxy &amp; Kubernetes <a href="#docker-reverse-proxy" class="doc-anchor">#</a></h3>
1356
1395
  <p>If you already have a reverse proxy (nginx, Traefik, k8s Ingress), skip the Caddy profile and point your proxy at port 4200. Key requirements:</p>
@@ -1360,8 +1399,8 @@ docker compose cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code>
1360
1399
  <li><strong>No buffering</strong> &mdash; for streaming AI responses, disable proxy buffering.</li>
1361
1400
  </ul>
1362
1401
  <div class="doc-callout doc-callout--tip">
1363
- <div class="doc-callout__label">&#128214; Full guide</div>
1364
- <p>For nginx configs, Kubernetes manifests, security considerations, and troubleshooting, see the complete <a href="https://github.com/borismichel/vibespot/blob/main/docs/docker-deployment.md" target="_blank">Docker Deployment Guide</a>.</p>
1402
+ <div class="doc-callout__label">&#9888;&#65039; Single instance &amp; auth</div>
1403
+ <p>vibeSpot keeps active sessions in memory &mdash; run a single instance (no horizontal scaling without sticky sessions). It runs as a non-root user and has <strong>no built-in authentication</strong>. For internet-facing deployments, put an authenticating proxy (OAuth2 Proxy, Authelia, Cloudflare Access) in front. Persistence is filesystem-only on the named volumes &mdash; there is no database to run.</p>
1365
1404
  </div>
1366
1405
  </div>
1367
1406
 
Binary file
package/ui/index.html CHANGED
@@ -484,6 +484,7 @@
484
484
  <div class="chat__header" id="chat-header">
485
485
  <span class="chat__header-title" id="chat-header-title">Chat</span>
486
486
  <span class="chat__header-context" id="chat-header-context"></span>
487
+ <span class="chat__cost-total hidden" id="chat-cost-total" title="Estimated total generation cost for this project"></span>
487
488
  </div>
488
489
  <div class="chat__messages" id="chat-messages" role="log" aria-live="polite">
489
490
  <div class="chat__welcome" id="chat-welcome">