lavish-axi 0.1.73 → 0.1.74

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
@@ -206,6 +206,7 @@ pnpm link
206
206
  In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately. Escape closes the card, same as Cancel, but only while it is empty (no text, no attachment); with unsent text or an attachment present, Escape does nothing rather than risk discarding it.
207
207
  Cmd+I or Ctrl+I toggles between annotate and explore mode from either the browser chrome or the artifact iframe, including while focus is in a textarea or control.
208
208
  - **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 keeps human feedback actions available while the agent is working because the server queues them for the next poll. An agent reply concludes delivered work and returns presence to waiting.
209
+ In a Herdr-managed pane, set `LAVISH_AXI_HERDR_CHIME=1` to show a Herdr request notification and play its chime once an indefinite poll enters the waiting state. Immediate queued feedback and timed debug polls do not trigger a notification. The integration is off unless both `LAVISH_AXI_HERDR_CHIME` and Herdr's `HERDR_ENV` session marker equal `1`; leaving either unset or using any other value opts out. Notification delivery runs without blocking feedback: if Herdr is unavailable, rejects the notification, stalls, or fails, the poll continues unchanged.
209
210
  Closing the last review tab while a poll is active starts a 10-second reconnect grace period. A reload or quick reconnect keeps an active poll waiting; if every review stays disconnected, the poll returns `browser_disconnected` without ending the resumable session, and the agent asks whether to reopen or end it instead of acting uninvited.
210
211
  The no-timeout poll always writes an immediate stderr banner so it is visibly not hung; it adds the periodic stderr wait ticks only in an interactive terminal, so when stderr is piped (as under agent harnesses) the captured output carries no tick noise. Stdout always stays reserved for the final response; if the poll is interrupted or times out before feedback arrives, re-run it because feedback remains queued until delivery. Poll delivery consumes the response, so read the complete response before truncating or filtering it.
211
212
  Codex-specific guidance keeps that poll attached to the active turn instead of hiding it in a background task, because completed background tasks may not resume the agent.
package/dist/cli.mjs CHANGED
@@ -9844,7 +9844,7 @@ async function serve({
9844
9844
  const streamHeartbeat = timeoutMs === null;
9845
9845
  let heartbeat = null;
9846
9846
  if (streamHeartbeat) {
9847
- res.status(200).type("application/json");
9847
+ res.status(200).type("application/json").set("Lavish-Poll-State", "listening");
9848
9848
  res.write(" ");
9849
9849
  heartbeat = setInterval(() => {
9850
9850
  if (!res.writableEnded) res.write(" ");
@@ -10658,7 +10658,9 @@ ${body}`
10658
10658
  let retryIndex = 0;
10659
10659
  while (true) {
10660
10660
  try {
10661
- const httpServer = await listenHttp(app, boundPort, listenHost);
10661
+ const httpServer = await listenHttp(app, boundPort, listenHost, (error) => {
10662
+ writeLog(`[lavish] HTTP server error: ${error instanceof Error ? error.message : String(error)}`);
10663
+ });
10662
10664
  httpServer.on("upgrade", handleEventUpgrade);
10663
10665
  if (boundPort === 0) boundPort = httpServer.address().port;
10664
10666
  httpServers.push(httpServer);
@@ -10858,15 +10860,20 @@ ${body}`
10858
10860
  done
10859
10861
  };
10860
10862
  }
10861
- function listenHttp(app, port, host) {
10863
+ function listenHttp(app, port, host, onRuntimeError) {
10862
10864
  return new Promise((resolve, reject) => {
10863
10865
  const server = createServer(app);
10866
+ let listening = false;
10864
10867
  const onError = (error) => {
10865
- server.off("listening", onListening);
10866
- reject(error);
10868
+ if (!listening) {
10869
+ server.off("listening", onListening);
10870
+ reject(error);
10871
+ return;
10872
+ }
10873
+ onRuntimeError?.(error);
10867
10874
  };
10868
10875
  const onListening = () => {
10869
- server.off("error", onError);
10876
+ listening = true;
10870
10877
  const address = server.address();
10871
10878
  if (address && typeof address === "object" && isWildcardHost(address.address)) {
10872
10879
  server.close(() => reject(new Error(`Refusing all-interfaces listener at ${address.address}`)));
@@ -10874,7 +10881,7 @@ function listenHttp(app, port, host) {
10874
10881
  }
10875
10882
  resolve(server);
10876
10883
  };
10877
- server.once("error", onError);
10884
+ server.on("error", onError);
10878
10885
  server.once("listening", onListening);
10879
10886
  server.listen({ port, host });
10880
10887
  });
@@ -11619,14 +11626,57 @@ var AGENT_REPLY_JSON_LIMIT_BYTES = 2 * 1024 * 1024;
11619
11626
  var AGENT_REPLY_JSON_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ text: "" }));
11620
11627
  var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_JSON_ENVELOPE_BYTES;
11621
11628
  var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
11629
+ var POLL_STATE_HEADER = "lavish-poll-state";
11622
11630
  var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
11623
- var VERSION = "0.1.73";
11631
+ var VERSION = "0.1.74";
11624
11632
  function detectInvokingAgent(env = process.env) {
11625
11633
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
11626
11634
  }
11627
11635
  function shouldNarratePollWaitTicks({ isTTY }) {
11628
11636
  return Boolean(isTTY);
11629
11637
  }
11638
+ function herdrPollChimeEnabled(env = process.env) {
11639
+ return env.HERDR_ENV === "1" && env.LAVISH_AXI_HERDR_CHIME === "1";
11640
+ }
11641
+ function notifyHerdrPollReady({ env = process.env, runner = spawn } = {}) {
11642
+ if (!herdrPollChimeEnabled(env)) return false;
11643
+ try {
11644
+ const child = runner(
11645
+ "herdr",
11646
+ [
11647
+ "notification",
11648
+ "show",
11649
+ "Lavish review ready",
11650
+ "--body",
11651
+ "The artifact is open and Lavish is polling for your feedback.",
11652
+ "--sound",
11653
+ "request"
11654
+ ],
11655
+ { stdio: "ignore" }
11656
+ );
11657
+ const kill = () => {
11658
+ try {
11659
+ child.kill("SIGKILL");
11660
+ } catch {
11661
+ return;
11662
+ }
11663
+ };
11664
+ const timer = setTimeout(kill, 2e3);
11665
+ timer.unref();
11666
+ const cleanup = () => {
11667
+ clearTimeout(timer);
11668
+ process.off("exit", kill);
11669
+ };
11670
+ child.on("error", () => {
11671
+ });
11672
+ child.once("close", cleanup);
11673
+ process.once("exit", kill);
11674
+ child.unref();
11675
+ return true;
11676
+ } catch {
11677
+ return false;
11678
+ }
11679
+ }
11630
11680
  function pollExecutionGuidance({ agent = "generic" } = {}) {
11631
11681
  const sharedGuidance = POLL_WAKE_PATH_RULES.join(" ");
11632
11682
  const agentGuidance = agent === "codex" ? ` ${CODEX_POLL_WAKE_PATH_GUIDANCE}` : "";
@@ -11866,7 +11916,10 @@ ${pollInterruptedText(absolute)}
11866
11916
  try {
11867
11917
  const response = await fetchJson(`${baseUrl}/api/poll?file=${encodeURIComponent(absolute)}${timeoutQuery}`, {
11868
11918
  retries: 3,
11869
- retryDelayMs: 500
11919
+ retryDelayMs: 500,
11920
+ onResponse: (pollResponse) => {
11921
+ if (pollResponse.headers.get(POLL_STATE_HEADER) === "listening") notifyHerdrPollReady();
11922
+ }
11870
11923
  });
11871
11924
  return createPollOutput({ file: absolute, response, agent: detectInvokingAgent(process.env) });
11872
11925
  } finally {
@@ -12949,7 +13002,7 @@ function createServerSpawnOptions(logFd = null) {
12949
13002
  env: { ...process.env, LAVISH_AXI_NO_OPEN: "1" }
12950
13003
  };
12951
13004
  }
12952
- async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
13005
+ async function fetchJson(url, { retries = 0, retryDelayMs = 250, onResponse = null } = {}) {
12953
13006
  let response;
12954
13007
  for (let attempt = 0; attempt <= retries; attempt += 1) {
12955
13008
  try {
@@ -12965,6 +13018,7 @@ async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
12965
13018
  if (!response.ok) {
12966
13019
  throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
12967
13020
  }
13021
+ onResponse?.(response);
12968
13022
  try {
12969
13023
  return await response.json();
12970
13024
  } catch {
@@ -13169,7 +13223,7 @@ Open or resume a Lavish Editor review session for an HTML artifact. Use --no-ope
13169
13223
  `,
13170
13224
  poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."] [--agent-reply-file <path>]
13171
13225
 
13172
- This command long-polls indefinitely for queued user prompts. It stays silent while it waits - that is normal, never kill it. Browser-detected layout issues do NOT return this poll: they are filed passively in the user's Layout issues inbox and arrive as an ordinary tag "layout-warnings" prompt only after the user selects them and queues the fixes. Warning lifecycle: an issue stays unresolved and counted while queued, becomes recurring if a newer artifact revision still shows it, and is resolved only after a newer artifact load plus a complete diagnostic pass at the same viewport no longer detects it. A failed or incomplete pass preserves it as unverified rather than clearing it. The only response that arrives without user action is artifact_failures - a fatal failure that made the review surface itself unusable. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. ${pollExecutionGuidance({ agent })} Use --agent-reply after applying prior feedback to display a concise response in Lavish Editor before waiting again. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_HELP_POINTER} Do not combine --agent-reply with --agent-reply-file.
13226
+ This command long-polls indefinitely for queued user prompts. It stays silent while it waits - that is normal, never kill it. Browser-detected layout issues do NOT return this poll: they are filed passively in the user's Layout issues inbox and arrive as an ordinary tag "layout-warnings" prompt only after the user selects them and queues the fixes. Warning lifecycle: an issue stays unresolved and counted while queued, becomes recurring if a newer artifact revision still shows it, and is resolved only after a newer artifact load plus a complete diagnostic pass at the same viewport no longer detects it. A failed or incomplete pass preserves it as unverified rather than clearing it. The only response that arrives without user action is artifact_failures - a fatal failure that made the review surface itself unusable. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. In a Herdr-managed pane, set LAVISH_AXI_HERDR_CHIME=1 to request attention when the poll has entered its waiting state; unset it or use any other value to keep the current silent behavior. Notification failures never interrupt the poll. ${pollExecutionGuidance({ agent })} Use --agent-reply after applying prior feedback to display a concise response in Lavish Editor before waiting again. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_HELP_POINTER} Do not combine --agent-reply with --agent-reply-file.
13173
13227
 
13174
13228
  Examples:
13175
13229
  lavish-axi poll report.html --agent-reply "Renamed the payment step."
package/dist/server.mjs CHANGED
@@ -51,9 +51,18 @@ function chunkToString(chunk, encoding) {
51
51
 
52
52
  // bin/lavish-axi-server.js
53
53
  installServerStdioTimestamps();
54
+ var fatalExitScheduled = false;
55
+ function handleFatalServerError(kind, error) {
56
+ const detail = error instanceof Error ? error.stack || error.message : String(error);
57
+ console.error(`[lavish] ${kind}: ${detail}`);
58
+ if (fatalExitScheduled) return;
59
+ fatalExitScheduled = true;
60
+ process.exitCode = 1;
61
+ setImmediate(() => process.exit(1));
62
+ }
63
+ process.on("uncaughtException", (error) => handleFatalServerError("uncaught exception", error));
54
64
  try {
55
65
  await import("./cli.mjs");
56
66
  } catch (error) {
57
- console.error(error);
58
- process.exit(1);
67
+ handleFatalServerError("server startup failed", error);
59
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
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",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "lavish-axi",
4
- "version": "0.1.73",
4
+ "version": "0.1.74",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "author": {
7
7
  "name": "Kun Chen",