lavish-axi 0.1.10 → 0.1.12

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
@@ -95,9 +95,10 @@ pnpm link
95
95
  ```
96
96
 
97
97
  - **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
98
- - **Sandboxed artifact** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and feedback controls, plus Tailwind CSS v4 and DaisyUI v5 design assets unless the HTML includes `<meta name="lavish-design" content="off">`.
98
+ - **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and feedback controls. Lavish does not inject any design system, so the saved HTML file renders identically whether you open it through `lavish-axi` or directly in a browser. Run `lavish-axi design` for a copy-pasteable CDN snippet to opt in to Tailwind CSS v4 + DaisyUI v5, or use any other design system (or none).
99
+ - **Live reload** - Lavish watches the HTML artifact file by default. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
99
100
  - **Feedback controls** - Mark buttons, choices, and other interactive elements with `data-lavish-action` so Lavish does not annotate them, then call `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` from the control handler.
100
- - **Agent presence** - The browser shows when no agent is listening, still queues feedback for the next `lavish-axi poll`, and only blocks sending while the agent is working on delivered feedback.
101
+ - **Agent presence** - The browser shows when no agent is listening, keeps queued feedback for the next successful `lavish-axi poll` send even across reloads, and only blocks sending while the agent is working on delivered feedback.
101
102
  - **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
102
103
  - **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
103
104
 
@@ -110,17 +111,19 @@ pnpm link
110
111
  | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session. |
111
112
  | `lavish-axi end <html-file>` | End a session. |
112
113
  | `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook. |
113
- | `lavish-axi design` | Show the injected Tailwind CSS and DaisyUI design reference. |
114
+ | `lavish-axi design` | Show CDN snippet + DaisyUI component reference (opt-in). |
115
+ | `lavish-axi server` | Run the local Lavish Editor server. |
114
116
 
115
117
  Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `input`, `slides`.
116
118
 
117
119
  ### Flags
118
120
 
119
- | Command | Flag | Description |
120
- | ------------------------ | --------------------- | ------------------------------------------------------------------------- |
121
- | `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
122
- | `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
123
- | `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it. |
121
+ | Command | Flag | Description |
122
+ | ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
124
+ | `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
125
+ | `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it. |
126
+ | `lavish-axi server` | `--verbose` | Log session and watcher events to stderr; can also be enabled with `LAVISH_AXI_DEBUG=1`. Detached server output is appended to `~/.lavish-axi/server.log` (or `LAVISH_AXI_STATE_DIR/server.log`) for startup and crash diagnostics. |
124
127
 
125
128
  ## Development
126
129
 
@@ -3,6 +3,7 @@
3
3
  const sessionDataElement = document.getElementById("lavish-session");
4
4
  const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
5
5
  const key = String(sessionData.key || "");
6
+ const queueStorageKey = "lavish-axi:queued:" + key;
6
7
  const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
7
8
 
8
9
  const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
@@ -16,11 +17,13 @@ const filePathInput = /** @type {HTMLInputElement} */ (document.getElementById("
16
17
  const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
17
18
  const presenceBanner = /** @type {HTMLDivElement} */ (document.getElementById("presenceBanner"));
18
19
 
19
- const queued = [];
20
+ const queued = loadQueuedPrompts();
20
21
  let annotation = true;
21
22
  let agentPresence = "waiting";
22
23
  let pendingSnapshot = "";
23
24
  let workingBubble = null;
25
+ let submitQueuedPromise = null;
26
+ let submitQueuedAgain = false;
24
27
 
25
28
  function escapeHtml(value) {
26
29
  return String(value).replace(
@@ -36,6 +39,27 @@ function escapeHtml(value) {
36
39
  );
37
40
  }
38
41
 
42
+ function loadQueuedPrompts() {
43
+ try {
44
+ const parsed = JSON.parse(sessionStorage.getItem(queueStorageKey) || "[]");
45
+ return Array.isArray(parsed) ? parsed.filter((prompt) => prompt && typeof prompt === "object") : [];
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ function persistQueuedPrompts() {
52
+ try {
53
+ if (queued.length) {
54
+ sessionStorage.setItem(queueStorageKey, JSON.stringify(queued));
55
+ } else {
56
+ sessionStorage.removeItem(queueStorageKey);
57
+ }
58
+ } catch {
59
+ // The in-memory queue still works if browser storage is unavailable.
60
+ }
61
+ }
62
+
39
63
  function render() {
40
64
  annotationPills.innerHTML = queued
41
65
  .map(
@@ -105,6 +129,7 @@ function setAgentPresence(state) {
105
129
  function removeQueuedPrompt(index, event) {
106
130
  if (event) event.stopPropagation();
107
131
  queued.splice(index, 1);
132
+ persistQueuedPrompts();
108
133
  render();
109
134
  }
110
135
 
@@ -118,6 +143,7 @@ function sendQueued() {
118
143
  const text = chatInput.value.trim();
119
144
  if (text) {
120
145
  queued.push({ uid: "", prompt: text, selector: "", tag: "message", text: "Freeform message" });
146
+ persistQueuedPrompts();
121
147
  addChat("user", text);
122
148
  chatInput.value = "";
123
149
  render();
@@ -128,14 +154,40 @@ function sendQueued() {
128
154
  }
129
155
 
130
156
  async function submitQueued() {
131
- const prompts = queued.splice(0, queued.length);
132
- render();
133
- if (agentPresence === "listening") setAgentPresence("working");
134
- await fetch("/api/" + key + "/prompts", {
157
+ if (submitQueuedPromise) {
158
+ submitQueuedAgain = true;
159
+ return submitQueuedPromise;
160
+ }
161
+
162
+ let succeeded = false;
163
+ submitQueuedPromise = submitQueuedOnce();
164
+ try {
165
+ const result = await submitQueuedPromise;
166
+ succeeded = true;
167
+ return result;
168
+ } finally {
169
+ submitQueuedPromise = null;
170
+ const shouldSubmitAgain = submitQueuedAgain;
171
+ submitQueuedAgain = false;
172
+ if (succeeded && shouldSubmitAgain && queued.length) submitQueued();
173
+ }
174
+ }
175
+
176
+ async function submitQueuedOnce() {
177
+ const prompts = queued.slice();
178
+ const response = await fetch("/api/" + key + "/prompts", {
135
179
  method: "POST",
136
180
  headers: { "content-type": "application/json" },
137
181
  body: JSON.stringify({ prompts, domSnapshot: pendingSnapshot }),
138
182
  });
183
+ if (!response.ok) throw new Error("failed to submit queued prompts");
184
+ for (const prompt of prompts) {
185
+ const index = queued.indexOf(prompt);
186
+ if (index !== -1) queued.splice(index, 1);
187
+ }
188
+ persistQueuedPrompts();
189
+ render();
190
+ if (agentPresence === "listening") setAgentPresence("working");
139
191
  }
140
192
 
141
193
  async function endSession() {
@@ -185,6 +237,7 @@ window.addEventListener("message", (event) => {
185
237
  const msg = event.data || {};
186
238
  if (msg.type === "lavish:queuePrompt") {
187
239
  queued.push(msg.prompt);
240
+ persistQueuedPrompts();
188
241
  render();
189
242
  }
190
243
  if (msg.type === "lavish:snapshot") {
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.js
4
4
  import { spawn, spawnSync } from "node:child_process";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
6
6
  import { access } from "node:fs/promises";
7
7
  import os2 from "node:os";
8
8
  import path4 from "node:path";
@@ -10,7 +10,17 @@ import { fileURLToPath } from "node:url";
10
10
  import { AxiError, runAxiCli } from "axi-sdk-js";
11
11
 
12
12
  // src/design-reference.js
13
- var DESIGN_SYSTEM_HINT = 'Lavish auto-injects Tailwind CSS browser runtime v4, DaisyUI v5, and DaisyUI themes into artifacts. Do not add these libraries separately. Set <html data-theme="luxury"> or another DaisyUI theme as appropriate, then use DaisyUI/Tailwind classes directly. Prefer using DaisyUI/Tailwind for styling to avoid having to spend tokens on writing a custom stylesheet, but in cases where you need precise control on the artifact, you can opt out of the auto-injected styles by adding <meta name="lavish-design" content="off">. Run `lavish-axi design` for technical reference.';
13
+ var TAILWIND_BROWSER_VERSION = "4.2.4";
14
+ var DAISYUI_VERSION = "5.5.19";
15
+ var DESIGN_CDN_URLS = {
16
+ tailwind: `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@${TAILWIND_BROWSER_VERSION}/dist/index.global.js`,
17
+ daisyui: `https://cdn.jsdelivr.net/npm/daisyui@${DAISYUI_VERSION}/daisyui.css`,
18
+ daisyuiThemes: `https://cdn.jsdelivr.net/npm/daisyui@${DAISYUI_VERSION}/themes.css`
19
+ };
20
+ var DESIGN_CDN_SNIPPET = `<link rel="stylesheet" href="${DESIGN_CDN_URLS.daisyui}">
21
+ <link rel="stylesheet" href="${DESIGN_CDN_URLS.daisyuiThemes}">
22
+ <script src="${DESIGN_CDN_URLS.tailwind}"></script>`;
23
+ var DESIGN_SYSTEM_HINT = "Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Tailwind CSS browser runtime v4 and DaisyUI v5 are available via CDN; run `lavish-axi design` for a copy-pasteable CDN snippet plus component reference. Prefer that CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. If the user asks for any other design system or plain HTML, follow that request.";
14
24
  var DAISYUI_THEMES = [
15
25
  "light",
16
26
  "dark",
@@ -51,11 +61,13 @@ var DAISYUI_THEMES = [
51
61
  function createDesignOutput() {
52
62
  return {
53
63
  design: {
54
- summary: "Lavish artifacts already have Tailwind CSS browser runtime v4, DaisyUI v5, and DaisyUI themes available.",
55
- rule: "Do not add Tailwind or DaisyUI script/link tags yourself.",
56
- opt_out: 'Add `<meta name="lavish-design" content="off">` to disable injection for exact-fidelity artifacts.',
64
+ summary: "Lavish does not auto-inject any design system. Artifacts stay portable HTML. Paste the CDN snippet below into your `<head>` to use Tailwind CSS browser runtime v4 + DaisyUI v5 + themes. Prefer this CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user.",
65
+ cdn_snippet: DESIGN_CDN_SNIPPET,
66
+ cdn_urls: DESIGN_CDN_URLS,
67
+ versions: { tailwind: TAILWIND_BROWSER_VERSION, daisyui: DAISYUI_VERSION },
57
68
  latest_docs: "https://daisyui.com/components/",
58
- docs_note: "Use this command for common syntax. Read the latest DaisyUI docs for full details when using advanced or unfamiliar components."
69
+ docs_note: "Use this command for common syntax. Read the latest DaisyUI docs for full details when using advanced or unfamiliar components.",
70
+ other_design_systems: "If the user asks for a different design system (Bootstrap, custom CSS, plain HTML, etc.), use that instead - Lavish does not require DaisyUI."
59
71
  },
60
72
  theme_usage: [
61
73
  'Set the page theme with `<html data-theme="luxury">`.',
@@ -460,12 +472,16 @@ function createDesignOutput() {
460
472
  import { mkdir } from "node:fs/promises";
461
473
  import os from "node:os";
462
474
  import path from "node:path";
475
+ var LOOPBACK_HOST = "127.0.0.1";
463
476
  function stateDir() {
464
477
  return process.env.LAVISH_AXI_STATE_DIR || path.join(os.homedir(), ".lavish-axi");
465
478
  }
466
479
  function stateFile() {
467
480
  return path.join(stateDir(), "state.json");
468
481
  }
482
+ function serverLogFile() {
483
+ return path.join(stateDir(), "server.log");
484
+ }
469
485
  async function ensureStateDir() {
470
486
  await mkdir(stateDir(), { recursive: true });
471
487
  }
@@ -992,25 +1008,11 @@ function createArtifactSdk() {
992
1008
  // src/html-transform.js
993
1009
  function injectLavishSdk(html, key) {
994
1010
  const script = `<script src="/sdk.js?key=${encodeURIComponent(key)}"></script>`;
995
- const design = shouldInjectDesign(html) ? '<link rel="stylesheet" href="/design/daisyui.css" data-lavish-design><script src="/design/tailwindcss-browser.js" data-lavish-design></script><link rel="stylesheet" href="/design/daisyui-themes.css" data-lavish-design>' : "";
996
- const withDesign = injectDesignAssets(html, design);
997
- if (/<\/body\s*>/i.test(withDesign)) {
998
- return withDesign.replace(/<\/body\s*>/i, `${script}</body>`);
999
- }
1000
- return `${withDesign}
1001
- ${script}`;
1002
- }
1003
- function shouldInjectDesign(html) {
1004
- if (/data-lavish-design/i.test(html)) return false;
1005
- return !/<meta\b(?=[^>]*name=["']lavish-design["'])(?=[^>]*content=["']off["'])[^>]*>/i.test(html);
1006
- }
1007
- function injectDesignAssets(html, design) {
1008
- if (!design) return html;
1009
- if (/<head\b[^>]*>/i.test(html)) {
1010
- return html.replace(/<head\b[^>]*>/i, (match) => `${match}${design}`);
1011
+ if (/<\/body\s*>/i.test(html)) {
1012
+ return html.replace(/<\/body\s*>/i, `${script}</body>`);
1011
1013
  }
1012
1014
  return `${html}
1013
- ${design}`;
1015
+ ${script}`;
1014
1016
  }
1015
1017
 
1016
1018
  // src/session-store.js
@@ -1181,7 +1183,7 @@ var designAssetUrls = {
1181
1183
  type: "application/javascript"
1182
1184
  }
1183
1185
  };
1184
- async function serve({ port, stateFile: stateFile2, version = "" }) {
1186
+ async function serve({ port, stateFile: stateFile2, version = "", debug = false, log = null, pollHeartbeatMs = 15e3 }) {
1185
1187
  const app = express();
1186
1188
  const store = new SessionStore(stateFile2);
1187
1189
  const events = new EventEmitter();
@@ -1189,6 +1191,11 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
1189
1191
  const activePolls = /* @__PURE__ */ new Map();
1190
1192
  const deliveredFeedback = /* @__PURE__ */ new Set();
1191
1193
  const sseClients = /* @__PURE__ */ new Set();
1194
+ const verbose = debug || process.env.LAVISH_AXI_DEBUG === "1";
1195
+ const writeLog = typeof log === "function" ? log : (line) => process.stderr.write(`${line}
1196
+ `);
1197
+ const logEvent = verbose ? (line) => writeLog(`[lavish] ${line}`) : null;
1198
+ let publicPort = port;
1192
1199
  app.use(express.json({ limit: "2mb" }));
1193
1200
  app.get("/health", (req, res) => {
1194
1201
  res.json({ ok: true, app: "lavish-axi", version });
@@ -1205,13 +1212,14 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
1205
1212
  try {
1206
1213
  const file = await canonicalFile(req.body.file);
1207
1214
  const key = sessionKey(file);
1208
- const url = `http://localhost:${port}/session/${key}`;
1215
+ const url = `http://${LOOPBACK_HOST}:${publicPort}/session/${key}`;
1209
1216
  const existing = await store.findByKey(key);
1210
1217
  const session = await store.upsertSession(file, url);
1211
1218
  if (existing?.status === "ended") {
1212
1219
  clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
1213
1220
  }
1214
- watchSession(session, watchers, events);
1221
+ logEvent?.(`session opened key=${key} file=${file}`);
1222
+ await watchSession(session, watchers, events, logEvent);
1215
1223
  res.json({ key, file, url, status: "opened" });
1216
1224
  } catch (error) {
1217
1225
  next(error);
@@ -1219,6 +1227,14 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
1219
1227
  });
1220
1228
  app.get("/api/poll", async (req, res, next) => {
1221
1229
  try {
1230
+ let handleRespondError = function(error) {
1231
+ if (streamHeartbeat) {
1232
+ cleanup();
1233
+ if (!res.writableEnded) res.destroy(error);
1234
+ return;
1235
+ }
1236
+ next(error);
1237
+ };
1222
1238
  const file = await canonicalFile(String(req.query.file || ""));
1223
1239
  const key = sessionKey(file);
1224
1240
  const timeoutMs = req.query.timeoutMs === void 0 ? null : Math.max(0, Math.min(Number(req.query.timeoutMs || 0), 2147483647));
@@ -1228,34 +1244,49 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
1228
1244
  res.json(immediate);
1229
1245
  return;
1230
1246
  }
1247
+ const streamHeartbeat = timeoutMs === null;
1248
+ let heartbeat = null;
1249
+ if (streamHeartbeat) {
1250
+ res.status(200).type("application/json");
1251
+ res.write(" ");
1252
+ heartbeat = setInterval(() => {
1253
+ if (!res.writableEnded) res.write(" ");
1254
+ }, pollHeartbeatMs);
1255
+ heartbeat.unref?.();
1256
+ }
1231
1257
  setPollActive(key, activePolls, deliveredFeedback, events, true);
1232
- const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(next), timeoutMs);
1258
+ const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(handleRespondError), timeoutMs);
1233
1259
  let cleaned = false;
1234
1260
  let responding = false;
1235
1261
  const cleanup = () => {
1236
1262
  if (cleaned) return;
1237
1263
  cleaned = true;
1238
1264
  if (timer) clearTimeout(timer);
1265
+ if (heartbeat) clearInterval(heartbeat);
1239
1266
  events.off("feedback", onFeedback);
1240
1267
  events.off("ended", onFeedback);
1241
1268
  setPollActive(key, activePolls, deliveredFeedback, events, false);
1242
1269
  };
1243
1270
  const respond = async () => {
1244
- if (responding || res.headersSent) return;
1271
+ if (responding || res.writableEnded) return;
1245
1272
  responding = true;
1246
1273
  try {
1247
1274
  const result = await store.takeFeedback(key);
1248
1275
  if (result.status === "feedback") markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
1249
- res.json(result);
1276
+ if (streamHeartbeat) {
1277
+ res.end(JSON.stringify(result));
1278
+ } else {
1279
+ res.json(result);
1280
+ }
1250
1281
  } finally {
1251
1282
  cleanup();
1252
1283
  }
1253
1284
  };
1254
1285
  const onFeedback = (changedKey) => {
1255
- if (changedKey !== key || res.headersSent) {
1286
+ if (changedKey !== key || res.writableEnded) {
1256
1287
  return;
1257
1288
  }
1258
- respond().catch(next);
1289
+ respond().catch(handleRespondError);
1259
1290
  };
1260
1291
  events.on("feedback", onFeedback);
1261
1292
  events.on("ended", onFeedback);
@@ -1320,7 +1351,7 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
1320
1351
  res.status(404).send("Session not found");
1321
1352
  return;
1322
1353
  }
1323
- watchSession(session, watchers, events);
1354
+ await watchSession(session, watchers, events, logEvent);
1324
1355
  res.type("html").send(createChromeHtml(session));
1325
1356
  } catch (error) {
1326
1357
  next(error);
@@ -1449,8 +1480,9 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
1449
1480
  res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
1450
1481
  });
1451
1482
  const httpServer = await new Promise((resolve) => {
1452
- const s = app.listen(port, "127.0.0.1", () => resolve(s));
1483
+ const s = app.listen(port, LOOPBACK_HOST, () => resolve(s));
1453
1484
  });
1485
+ publicPort = httpServer.address().port;
1454
1486
  let shuttingDown = false;
1455
1487
  function shutdown() {
1456
1488
  if (shuttingDown) return;
@@ -1498,23 +1530,55 @@ function resolveArtifactAsset(root, assetPath) {
1498
1530
  }
1499
1531
  return file;
1500
1532
  }
1501
- function watchSession(session, watchers, events) {
1533
+ async function watchSession(session, watchers, events, logEvent) {
1502
1534
  if (watchers.has(session.key)) {
1503
1535
  return;
1504
1536
  }
1505
- const root = path3.dirname(session.file);
1506
- const watcher = chokidar.watch(root, {
1507
- ignored: /(^|[/\\])(\.git|node_modules|dist|build|\.lavish-axi)([/\\]|$)/,
1508
- ignoreInitial: true,
1509
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
1510
- });
1537
+ const target = await resolveWatchTarget(session);
1538
+ if (watchers.has(session.key)) {
1539
+ return;
1540
+ }
1541
+ logEvent?.(`watch session=${session.key} scope=${target.scope} path=${target.path}`);
1542
+ const watcher = chokidar.watch(target.path, target.options);
1511
1543
  let timer = null;
1512
- watcher.on("all", () => {
1544
+ watcher.on("all", (event, file) => {
1545
+ logEvent?.(`watch event=${event} session=${session.key} file=${file ?? ""}`);
1513
1546
  clearTimeout(timer);
1514
1547
  timer = setTimeout(() => events.emit("reload", session.key), 100);
1515
1548
  });
1549
+ watcher.on("error", (error) => {
1550
+ const message = error instanceof Error ? error.message : String(error);
1551
+ logEvent?.(`watch error session=${session.key} message=${message}`);
1552
+ });
1516
1553
  watchers.set(session.key, watcher);
1517
1554
  }
1555
+ async function resolveWatchTarget(session) {
1556
+ const baseOptions = {
1557
+ ignoreInitial: true,
1558
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
1559
+ };
1560
+ try {
1561
+ const html = await readFile2(session.file, "utf8");
1562
+ if (hasLiveReloadRootOptIn(html)) {
1563
+ return {
1564
+ path: path3.dirname(session.file),
1565
+ scope: "directory",
1566
+ options: {
1567
+ ...baseOptions,
1568
+ ignored: /(^|[/\\])(\.git|node_modules|dist|build|\.lavish-axi)([/\\]|$)/
1569
+ }
1570
+ };
1571
+ }
1572
+ } catch {
1573
+ }
1574
+ return { path: session.file, scope: "file", options: baseOptions };
1575
+ }
1576
+ function hasLiveReloadRootOptIn(html) {
1577
+ if (typeof html !== "string") return false;
1578
+ const searchableHtml = html.replace(/<!--[\s\S]*?-->/g, "");
1579
+ if (/<html\b[^>]*\sdata-lavish-live-reload-root(?:[\s=>/]|$)[^>]*>/i.test(searchableHtml)) return true;
1580
+ return /<meta\b(?=[^>]*name=["']lavish-live-reload["'])(?=[^>]*content=["']root["'])[^>]*>/i.test(searchableHtml);
1581
+ }
1518
1582
  function setPollActive(key, activePolls, deliveredFeedback, events, active) {
1519
1583
  const previousPresence = computePresence(key, activePolls, deliveredFeedback);
1520
1584
  const count = activePolls.get(key) || 0;
@@ -1758,7 +1822,7 @@ function normalizePagePath(path5) {
1758
1822
  // src/cli.js
1759
1823
  var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "server", "playbook", "design"]);
1760
1824
  var DESCRIPTION = "Lavish Editor helps agents turn rich HTML artifacts into collaborative human review surfaces. Whenever you are about to give user a complex response that will be easier to understand via a rich / interactive page, consider using Lavish Editor. First generate an interactive HTML artifact according to user request, then run `lavish-axi <html-file>` so the user can visually review it, annotate elements or selected text, queue prompts, and send feedback back through `lavish-axi poll`.";
1761
- var VERSION = "0.1.10";
1825
+ var VERSION = "0.1.12";
1762
1826
  async function run(argv) {
1763
1827
  await ensureStateDir();
1764
1828
  const normalizedArgv = normalizeArgv(argv);
@@ -1913,7 +1977,10 @@ async function pollCommand(args) {
1913
1977
  }
1914
1978
  const timeoutMs = flagValue(args, "--timeout-ms");
1915
1979
  const timeoutQuery = timeoutMs ? `&timeoutMs=${encodeURIComponent(timeoutMs)}` : "";
1916
- const response = await fetchJson(`${baseUrl}/api/poll?file=${encodeURIComponent(absolute)}${timeoutQuery}`);
1980
+ const response = await fetchJson(`${baseUrl}/api/poll?file=${encodeURIComponent(absolute)}${timeoutQuery}`, {
1981
+ retries: 3,
1982
+ retryDelayMs: 500
1983
+ });
1917
1984
  return createPollOutput({ file: absolute, response });
1918
1985
  }
1919
1986
  function createPollOutput({ file, response }) {
@@ -1956,7 +2023,8 @@ async function designCommand() {
1956
2023
  }
1957
2024
  async function serverCommand(args) {
1958
2025
  const port = Number(flagValue(args, "--port") || defaultPort());
1959
- const server = await serve({ port, stateFile: stateFile(), version: VERSION });
2026
+ const debug = args.includes("--verbose") || process.env.LAVISH_AXI_DEBUG === "1";
2027
+ const server = await serve({ port, stateFile: stateFile(), version: VERSION, debug });
1960
2028
  await server.done;
1961
2029
  return "";
1962
2030
  }
@@ -1981,7 +2049,7 @@ function isHtmlPath(file) {
1981
2049
  }
1982
2050
  async function ensureServer({ forceRestart = false } = {}) {
1983
2051
  const port = defaultPort();
1984
- const baseUrl = `http://localhost:${port}`;
2052
+ const baseUrl = `http://${LOOPBACK_HOST}:${port}`;
1985
2053
  const existing = await fetchHealth(baseUrl);
1986
2054
  if (existing && !shouldRestartServer(VERSION, existing, forceRestart)) {
1987
2055
  return baseUrl;
@@ -2070,36 +2138,84 @@ function killProcessOnPort(port) {
2070
2138
  async function startServer(port) {
2071
2139
  await ensureStateDir();
2072
2140
  const entry = resolveServerEntry();
2073
- const child = spawn(process.execPath, [entry, "server", "--port", String(port)], {
2074
- detached: true,
2075
- stdio: "ignore",
2076
- env: { ...process.env, LAVISH_AXI_NO_OPEN: "1" }
2077
- });
2078
- child.unref();
2141
+ let logFd = null;
2142
+ try {
2143
+ logFd = openSync(serverLogFile(), "a");
2144
+ } catch {
2145
+ }
2146
+ try {
2147
+ const child = spawn(process.execPath, [entry, "server", "--port", String(port)], createServerSpawnOptions(logFd));
2148
+ child.unref();
2149
+ } finally {
2150
+ if (logFd !== null) closeSync(logFd);
2151
+ }
2079
2152
  }
2080
2153
  function resolveServerEntry() {
2081
2154
  const binEntry = fileURLToPath(new URL("../bin/lavish-axi.js", import.meta.url));
2082
2155
  if (existsSync(binEntry)) return binEntry;
2083
2156
  return fileURLToPath(import.meta.url);
2084
2157
  }
2085
- async function fetchJson(url) {
2086
- const response = await fetch(url);
2158
+ function createServerSpawnOptions(logFd = null) {
2159
+ const stdio = (
2160
+ /** @type {import("node:child_process").StdioOptions} */
2161
+ logFd === null ? "ignore" : ["ignore", logFd, logFd]
2162
+ );
2163
+ return {
2164
+ detached: true,
2165
+ stdio,
2166
+ env: { ...process.env, LAVISH_AXI_NO_OPEN: "1" }
2167
+ };
2168
+ }
2169
+ async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
2170
+ let response;
2171
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
2172
+ try {
2173
+ response = await fetch(url);
2174
+ break;
2175
+ } catch (error) {
2176
+ if (error instanceof AxiError) throw error;
2177
+ if (attempt >= retries) throw serverConnectionError();
2178
+ await delay(retryDelayMs);
2179
+ }
2180
+ }
2181
+ if (!response) throw serverConnectionError();
2087
2182
  if (!response.ok) {
2088
2183
  throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
2089
2184
  }
2090
- return response.json();
2185
+ try {
2186
+ return await response.json();
2187
+ } catch {
2188
+ throw pollResponseInterruptedError();
2189
+ }
2091
2190
  }
2092
2191
  async function postJson(url, body) {
2093
- const response = await fetch(url, {
2094
- method: "POST",
2095
- headers: { "content-type": "application/json" },
2096
- body: JSON.stringify(body)
2097
- });
2192
+ let response;
2193
+ try {
2194
+ response = await fetch(url, {
2195
+ method: "POST",
2196
+ headers: { "content-type": "application/json" },
2197
+ body: JSON.stringify(body)
2198
+ });
2199
+ } catch {
2200
+ throw serverConnectionError();
2201
+ }
2098
2202
  if (!response.ok) {
2099
2203
  throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
2100
2204
  }
2101
2205
  return response.json();
2102
2206
  }
2207
+ function serverConnectionError() {
2208
+ return new AxiError("Lavish Editor server connection failed", "SERVER_ERROR", [
2209
+ "Run `lavish-axi server --verbose` or inspect `~/.lavish-axi/server.log` (`LAVISH_AXI_STATE_DIR/server.log` when set) for server startup or crash diagnostics",
2210
+ "Re-run the last `lavish-axi poll <html-file>` command after the server is healthy"
2211
+ ]);
2212
+ }
2213
+ function pollResponseInterruptedError() {
2214
+ return new AxiError("Lavish Editor poll response was interrupted", "SERVER_ERROR", [
2215
+ "Run `lavish-axi server --verbose` or inspect `~/.lavish-axi/server.log` (`LAVISH_AXI_STATE_DIR/server.log` when set) for server startup or crash diagnostics",
2216
+ "Re-run the last `lavish-axi poll <html-file>` command after the server is healthy"
2217
+ ]);
2218
+ }
2103
2219
  function flagValue(args, flag) {
2104
2220
  const index = args.indexOf(flag);
2105
2221
  if (index === -1) {
@@ -2152,11 +2268,11 @@ Examples:
2152
2268
  `,
2153
2269
  design: `Usage: lavish-axi design
2154
2270
 
2155
- Show technical reference for the Tailwind CSS browser runtime v4, DaisyUI v5 components, and DaisyUI themes that Lavish auto-injects into artifacts. Do not add these libraries separately.
2271
+ Show a copy-pasteable CDN snippet for Tailwind CSS browser runtime v4 + DaisyUI v5 + themes, plus technical reference for DaisyUI components. Lavish artifacts stay portable HTML. Prefer the CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. If the user asks for another design system or plain HTML, follow that request.
2156
2272
  `,
2157
- server: `Usage: lavish-axi server [--port 4387]
2273
+ server: `Usage: lavish-axi server [--port 4387] [--verbose]
2158
2274
 
2159
- Run the local Lavish Editor server.
2275
+ Run the local Lavish Editor server. Pass --verbose (or set LAVISH_AXI_DEBUG=1) to log session and watcher events to stderr. Detached server output is appended to ~/.lavish-axi/server.log, or LAVISH_AXI_STATE_DIR/server.log when set, for startup and crash diagnostics.
2160
2276
  `
2161
2277
  };
2162
2278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "packageManager": "pnpm@11.1.1",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "type": "module",