create-volt 0.48.3 → 0.50.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to `create-volt` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.50.0] - 2026-06-30
8
+
9
+ ### Added
10
+ - **Config editor AI falls back to the hosted gateway.** With no local provider
11
+ key, `/setup/ai` routes through the voltjs.com gateway via `VOLT_AI_TOKEN`
12
+ (free-capped, then pay-as-you-go on the host's key); a local key still wins
13
+ (BYO). Clear error when neither is set. The gateway now honors the client's
14
+ stream preference, so the editor gets a normal JSON response.
15
+
16
+ ## [0.49.0] - 2026-06-29
17
+
18
+ ### Added
19
+ - **AI in the config editor.** The embedded RTEPro editor's AI button now works:
20
+ a new `/setup/ai` proxy injects the `.env` provider key server-side
21
+ (Anthropic / OpenAI / Gemini, BYO) and RTEPro is wired to it via aiProxy. The
22
+ key never reaches the browser. (Set ANTHROPIC_API_KEY etc. in the app's .env.)
23
+
7
24
  ## [0.48.3] - 2026-06-29
8
25
 
9
26
  ### Fixed
@@ -649,6 +666,8 @@ All notable changes to `create-volt` are documented here. The format follows
649
666
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
650
667
  and auto-detects npm / pnpm / yarn / bun for the install step.
651
668
 
669
+ [0.50.0]: https://github.com/MIR-2025/volt/releases/tag/v0.50.0
670
+ [0.49.0]: https://github.com/MIR-2025/volt/releases/tag/v0.49.0
652
671
  [0.48.3]: https://github.com/MIR-2025/volt/releases/tag/v0.48.3
653
672
  [0.48.2]: https://github.com/MIR-2025/volt/releases/tag/v0.48.2
654
673
  [0.48.1]: https://github.com/MIR-2025/volt/releases/tag/v0.48.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.48.3",
3
+ "version": "0.50.0",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -344,6 +344,55 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
348
+ // (BYO) when set; otherwise falls back to the voltjs.com gateway via
349
+ // VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
350
+ // key/token never reaches the browser. ---
351
+ if (req.method === "POST" && p === "/setup/ai") {
352
+ let cbody = "";
353
+ req.on("data", (c) => (cbody += c));
354
+ req.on("end", async () => {
355
+ res.setHeader("Content-Type", "application/json");
356
+ try {
357
+ const env = readEnvFile();
358
+ const body = JSON.parse(cbody || "{}");
359
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
360
+ delete body._provider;
361
+ const localKey = { anthropic: env.ANTHROPIC_API_KEY, openai: env.OPENAI_API_KEY, gemini: env.GEMINI_API_KEY }[provider];
362
+ let url, headers, payload = body;
363
+ if (localKey) {
364
+ if (provider === "anthropic") {
365
+ url = "https://api.anthropic.com/v1/messages";
366
+ headers = { "x-api-key": localKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
367
+ } else if (provider === "openai") {
368
+ url = "https://api.openai.com/v1/chat/completions";
369
+ headers = { authorization: "Bearer " + localKey, "content-type": "application/json" };
370
+ } else if (provider === "gemini") {
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${localKey}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ } else if (env.VOLT_AI_TOKEN) {
377
+ // no local key → host gateway (free-capped, then pay-as-you-go)
378
+ url = env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai";
379
+ headers = { authorization: "Bearer " + env.VOLT_AI_TOKEN, "content-type": "application/json" };
380
+ payload = { messages: body.messages, system: body.system, max_tokens: body.max_tokens, model: body.model };
381
+ } else {
382
+ throw new Error("No AI key in .env and no VOLT_AI_TOKEN — add a provider key, or a gateway token to use the hosted tier.");
383
+ }
384
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
385
+ const text = await r.text();
386
+ res.statusCode = r.status;
387
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
388
+ res.end(text);
389
+ } catch (e) {
390
+ res.statusCode = 400;
391
+ res.end(JSON.stringify({ error: e.message }));
392
+ }
393
+ });
394
+ return;
395
+ }
347
396
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
397
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
398
  // config only, never in the running app. ---
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -344,6 +344,55 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
348
+ // (BYO) when set; otherwise falls back to the voltjs.com gateway via
349
+ // VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
350
+ // key/token never reaches the browser. ---
351
+ if (req.method === "POST" && p === "/setup/ai") {
352
+ let cbody = "";
353
+ req.on("data", (c) => (cbody += c));
354
+ req.on("end", async () => {
355
+ res.setHeader("Content-Type", "application/json");
356
+ try {
357
+ const env = readEnvFile();
358
+ const body = JSON.parse(cbody || "{}");
359
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
360
+ delete body._provider;
361
+ const localKey = { anthropic: env.ANTHROPIC_API_KEY, openai: env.OPENAI_API_KEY, gemini: env.GEMINI_API_KEY }[provider];
362
+ let url, headers, payload = body;
363
+ if (localKey) {
364
+ if (provider === "anthropic") {
365
+ url = "https://api.anthropic.com/v1/messages";
366
+ headers = { "x-api-key": localKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
367
+ } else if (provider === "openai") {
368
+ url = "https://api.openai.com/v1/chat/completions";
369
+ headers = { authorization: "Bearer " + localKey, "content-type": "application/json" };
370
+ } else if (provider === "gemini") {
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${localKey}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ } else if (env.VOLT_AI_TOKEN) {
377
+ // no local key → host gateway (free-capped, then pay-as-you-go)
378
+ url = env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai";
379
+ headers = { authorization: "Bearer " + env.VOLT_AI_TOKEN, "content-type": "application/json" };
380
+ payload = { messages: body.messages, system: body.system, max_tokens: body.max_tokens, model: body.model };
381
+ } else {
382
+ throw new Error("No AI key in .env and no VOLT_AI_TOKEN — add a provider key, or a gateway token to use the hosted tier.");
383
+ }
384
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
385
+ const text = await r.text();
386
+ res.statusCode = r.status;
387
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
388
+ res.end(text);
389
+ } catch (e) {
390
+ res.statusCode = 400;
391
+ res.end(JSON.stringify({ error: e.message }));
392
+ }
393
+ });
394
+ return;
395
+ }
347
396
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
397
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
398
  // config only, never in the running app. ---
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -344,6 +344,55 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
348
+ // (BYO) when set; otherwise falls back to the voltjs.com gateway via
349
+ // VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
350
+ // key/token never reaches the browser. ---
351
+ if (req.method === "POST" && p === "/setup/ai") {
352
+ let cbody = "";
353
+ req.on("data", (c) => (cbody += c));
354
+ req.on("end", async () => {
355
+ res.setHeader("Content-Type", "application/json");
356
+ try {
357
+ const env = readEnvFile();
358
+ const body = JSON.parse(cbody || "{}");
359
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
360
+ delete body._provider;
361
+ const localKey = { anthropic: env.ANTHROPIC_API_KEY, openai: env.OPENAI_API_KEY, gemini: env.GEMINI_API_KEY }[provider];
362
+ let url, headers, payload = body;
363
+ if (localKey) {
364
+ if (provider === "anthropic") {
365
+ url = "https://api.anthropic.com/v1/messages";
366
+ headers = { "x-api-key": localKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
367
+ } else if (provider === "openai") {
368
+ url = "https://api.openai.com/v1/chat/completions";
369
+ headers = { authorization: "Bearer " + localKey, "content-type": "application/json" };
370
+ } else if (provider === "gemini") {
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${localKey}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ } else if (env.VOLT_AI_TOKEN) {
377
+ // no local key → host gateway (free-capped, then pay-as-you-go)
378
+ url = env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai";
379
+ headers = { authorization: "Bearer " + env.VOLT_AI_TOKEN, "content-type": "application/json" };
380
+ payload = { messages: body.messages, system: body.system, max_tokens: body.max_tokens, model: body.model };
381
+ } else {
382
+ throw new Error("No AI key in .env and no VOLT_AI_TOKEN — add a provider key, or a gateway token to use the hosted tier.");
383
+ }
384
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
385
+ const text = await r.text();
386
+ res.statusCode = r.status;
387
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
388
+ res.end(text);
389
+ } catch (e) {
390
+ res.statusCode = 400;
391
+ res.end(JSON.stringify({ error: e.message }));
392
+ }
393
+ });
394
+ return;
395
+ }
347
396
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
397
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
398
  // config only, never in the running app. ---
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -370,6 +370,55 @@ function startSetup() {
370
370
  }
371
371
  return;
372
372
  }
373
+ // --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
374
+ // (BYO) when set; otherwise falls back to the voltjs.com gateway via
375
+ // VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
376
+ // key/token never reaches the browser. ---
377
+ if (req.method === "POST" && p === "/setup/ai") {
378
+ let cbody = "";
379
+ req.on("data", (c) => (cbody += c));
380
+ req.on("end", async () => {
381
+ res.setHeader("Content-Type", "application/json");
382
+ try {
383
+ const env = readEnvFile();
384
+ const body = JSON.parse(cbody || "{}");
385
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
386
+ delete body._provider;
387
+ const localKey = { anthropic: env.ANTHROPIC_API_KEY, openai: env.OPENAI_API_KEY, gemini: env.GEMINI_API_KEY }[provider];
388
+ let url, headers, payload = body;
389
+ if (localKey) {
390
+ if (provider === "anthropic") {
391
+ url = "https://api.anthropic.com/v1/messages";
392
+ headers = { "x-api-key": localKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
393
+ } else if (provider === "openai") {
394
+ url = "https://api.openai.com/v1/chat/completions";
395
+ headers = { authorization: "Bearer " + localKey, "content-type": "application/json" };
396
+ } else if (provider === "gemini") {
397
+ const model = body.model || "gemini-2.0-flash";
398
+ delete body.model;
399
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${localKey}`;
400
+ headers = { "content-type": "application/json" };
401
+ } else throw new Error("unknown AI provider: " + provider);
402
+ } else if (env.VOLT_AI_TOKEN) {
403
+ // no local key → host gateway (free-capped, then pay-as-you-go)
404
+ url = env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai";
405
+ headers = { authorization: "Bearer " + env.VOLT_AI_TOKEN, "content-type": "application/json" };
406
+ payload = { messages: body.messages, system: body.system, max_tokens: body.max_tokens, model: body.model };
407
+ } else {
408
+ throw new Error("No AI key in .env and no VOLT_AI_TOKEN — add a provider key, or a gateway token to use the hosted tier.");
409
+ }
410
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
411
+ const text = await r.text();
412
+ res.statusCode = r.status;
413
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
414
+ res.end(text);
415
+ } catch (e) {
416
+ res.statusCode = 400;
417
+ res.end(JSON.stringify({ error: e.message }));
418
+ }
419
+ });
420
+ return;
421
+ }
373
422
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
374
423
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
375
424
  // config only, never in the running app. ---
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }