lavish-axi 0.1.71 → 0.1.73
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 +4 -4
- package/dist/chrome-client.js +39 -1
- package/dist/cli.mjs +205 -43
- package/dist/server.mjs +59 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -236,7 +236,7 @@ pnpm link
|
|
|
236
236
|
Lavish changes only the browser view, so saved, standalone, and exported artifacts still render plain Mermaid.
|
|
237
237
|
- **Server cleanup** - The detached server stops after the last session ends when nothing is connected, or after `LAVISH_AXI_IDLE_TIMEOUT_MS` (default 30 minutes) with no browser or poll connections.
|
|
238
238
|
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
239
|
-
- **Server upgrades** - One background server serves every session, so upgrading `lavish-axi` while reviews are open makes the next `lavish-axi <html-file>` replace that server. Only the review page for the artifact being opened reloads itself once the replacement answers - and not even that one while you have unsent annotation text open, which gets the same banner instead so the reload is yours to make. Every other open review page keeps working and shows a banner reading "Lavish was updated. This page is running the previous version.", with a Check and reload button and a Dismiss button, so no page you are reading reloads on its own. After `lavish-axi stop` those pages say Lavish was stopped and to reload after you start it again, and a restart that only picks up a local build says that rather than claiming an update.
|
|
239
|
+
- **Server upgrades** - One background server serves every session, so upgrading `lavish-axi` while reviews are open makes the next `lavish-axi <html-file>` replace that server. Only the review page for the artifact being opened reloads itself once the replacement answers - and not even that one while you have unsent annotation text open, which gets the same banner instead so the reload is yours to make. Every other open review page keeps working and shows a banner reading "Lavish was updated. This page is running the previous version.", with a Check and reload button and a Dismiss button, so no page you are reading reloads on its own. After `lavish-axi stop` those pages say Lavish was stopped and to reload after you start it again, and a restart that only picks up a local build says that rather than claiming an update. An open review page whose server simply goes away - idle self-shutdown, a crash, a machine that slept - shows that same banner once reconnecting has failed for a few seconds, rather than retrying a dead server silently while looking fine; it clears itself if the server comes back, and dismissing it keeps it dismissed until then.
|
|
240
240
|
A page still using the legacy event stream reloads once when it reconnects to the replacement server, moving itself onto the current transport without retaining an HTTP connection. If unsent annotation text is open, it shows the neutral server-restarted banner instead and waits for you to reload.
|
|
241
241
|
Every **Check and reload** control asks the server whether it is running before it navigates; while nothing answers, the page stays where it is and says so, and a check that gets no answer at all says that instead of guessing.
|
|
242
242
|
In-flight `lavish-axi poll` commands end with an interrupted-poll error and are safe to re-run; queued feedback is never lost, and annotation text you have typed but not queued yet survives the reload as described under **Live reload**.
|
|
@@ -244,7 +244,7 @@ pnpm link
|
|
|
244
244
|
- **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
|
|
245
245
|
- **Diagnostic viewports** - `LAVISH_AXI_DIAGNOSTIC_VIEWPORTS` sets which viewport classes the layout-issue inbox tracks (`mobile`, `compact`, `desktop`; comma-separated, default all). Warnings whose class leaves the set are marked obsolete with an explicit reason instead of silently reading as fixed.
|
|
246
246
|
- **Server port** - Set `LAVISH_AXI_PORT` to choose the server port; it defaults to `4387`.
|
|
247
|
-
- **Network binding** - With Tailscale running, the review server automatically listens only on loopback (`127.0.0.1`) and this machine's Tailscale IPv4 address - never on `0.0.0.0` or every interface. The generated session link uses the machine's MagicDNS name, so it is the phone-ready URL to open from another device on the same tailnet. When Tailscale is absent or down, Lavish silently falls back to loopback-only and prints no phone URL. If Tailscale is running but MagicDNS is unavailable or its address cannot be bound after brief retries, Lavish visibly warns that phone access is unavailable and remains loopback-only. Binding beyond loopback exposes an unauthenticated server that can read and serve arbitrary local files to devices that can reach it, so the tailnet should be trusted. Any explicit `LAVISH_AXI_HOST` overrides automatic Tailscale binding; wildcard values are restricted to loopback, while a non-wildcard value selects that one concrete bind address. `LAVISH_AXI_LINK_HOST` controls the link host when automatic Tailscale binding is disabled.
|
|
247
|
+
- **Network binding** - With Tailscale running, the review server automatically listens only on loopback (`127.0.0.1`) and this machine's Tailscale IPv4 address - never on `0.0.0.0` or every interface. The generated session link uses the machine's MagicDNS name, so it is the phone-ready URL to open from another device on the same tailnet. When Tailscale is absent or down, Lavish silently falls back to loopback-only and prints no phone URL. If Tailscale is running but MagicDNS is unavailable or its address cannot be bound after brief retries, Lavish visibly warns that phone access is unavailable and remains loopback-only. Binding beyond loopback exposes an unauthenticated server that can read and serve arbitrary local files to devices that can reach it, so the tailnet should be trusted. Any explicit `LAVISH_AXI_HOST` overrides automatic Tailscale binding; wildcard values are restricted to loopback, while a non-wildcard value selects that one concrete bind address. Each address is retried briefly, and if none can be bound Lavish falls back to loopback, warns that it is not reachable at the requested address, and points session links at loopback unless `LAVISH_AXI_LINK_HOST` explicitly overrides them - a locally reachable server beats no server at all. When an unavailable requested address returns, the next CLI invocation restarts the fallback server once to restore that listener. `LAVISH_AXI_LINK_HOST` controls the link host when automatic Tailscale binding is disabled.
|
|
248
248
|
- **Allowed hosts** - To defend against DNS rebinding, the server rejects (`403`) any request whose `Host` header is missing or not one it answers to: loopback names plus the concrete Tailscale IPv4 address and MagicDNS name when the Tailscale listener is successfully bound. If you configure a reverse proxy or another intentional hostname, list it in `LAVISH_AXI_ALLOWED_HOSTS` (whitespace-separated). Behind a reverse proxy, the forwarded `X-Forwarded-Host` is validated against the same list, so add the public hostname there and have the proxy send it together with `X-Forwarded-Proto`. Set `LAVISH_AXI_ALLOWED_HOSTS` to `*` to disable the check entirely, only when the server sits behind your own authentication or proxy. Mutating routes also reject a present foreign `Origin` or `Referer` (`403`); header-less CLI control requests remain allowed where supported.
|
|
249
249
|
- **Browser opening** - Set `LAVISH_AXI_NO_OPEN=1`, equivalent to `--no-open`, to create or resume a session without launching a browser window.
|
|
250
250
|
|
|
@@ -266,7 +266,7 @@ pnpm link
|
|
|
266
266
|
| `lavish-axi setup plugin` | Register the installed package as an [Agent Plugin](https://agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI; opt-in, idempotent, no marketplace involved. Reload each client afterward. |
|
|
267
267
|
| `lavish-axi server` | Run the local Lavish Editor server. |
|
|
268
268
|
|
|
269
|
-
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
|
|
269
|
+
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `explanation`, `slides`.
|
|
270
270
|
One artifact often combines several playbooks, such as a plan that includes a comparison and a diagram, so agents must match against each `use_when` trigger and open every matching playbook before writing HTML.
|
|
271
271
|
For flows, architecture, state, or sequence diagrams, open the diagram playbook for the recommended tooling and SVG guidance.
|
|
272
272
|
|
|
@@ -289,7 +289,7 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
289
289
|
| `lavish-axi poll` | `--agent-reply-file <path>` | When a longer reply is genuinely necessary, read Markdown from a file (`-` for stdin) so newlines survive quoting. Use blank-line paragraphs, `- ` / `1. ` lists, and `## ` headings so the Conversation panel renders it scannably. The Markdown subset is under Feedback controls. Cannot be combined with `--agent-reply`. |
|
|
290
290
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
|
291
291
|
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
|
292
|
-
| `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.
|
|
292
|
+
| `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 with UTC timestamps to `~/.lavish-axi/server.log` (or `LAVISH_AXI_STATE_DIR/server.log`) for startup, shutdown-cause, and crash diagnostics. |
|
|
293
293
|
|
|
294
294
|
## Development
|
|
295
295
|
|
package/dist/chrome-client.js
CHANGED
|
@@ -198,6 +198,16 @@ const layoutGateMaxHoldMs =
|
|
|
198
198
|
let chromeOutdatedReason = "";
|
|
199
199
|
let chromeOutdatedGeneration = 0;
|
|
200
200
|
let outdatedReloadInFlight = false;
|
|
201
|
+
// The live-event socket reconnects forever on a 5s cap. Silence there is indistinguishable from a
|
|
202
|
+
// healthy idle stream, so a page whose server has gone away keeps rendering its last state and
|
|
203
|
+
// tells the user nothing until they reload into a connection error. Past this many consecutive
|
|
204
|
+
// failures the banner says so, with the health-probed reload the banner already offers.
|
|
205
|
+
const LIVE_EVENT_UNREACHABLE_FAILURES = 5;
|
|
206
|
+
let liveEventFailures = 0;
|
|
207
|
+
// Only a banner this path raised may be hidden by this path: a `chrome-outdated` event means the
|
|
208
|
+
// server was replaced, which a reconnect does not disprove.
|
|
209
|
+
let unreachableBannerOwned = false;
|
|
210
|
+
let unreachableDismissed = false;
|
|
201
211
|
/** @type {{ selector: string, revision: number } | null} */
|
|
202
212
|
let unrestorableDraftMiss = null;
|
|
203
213
|
let retiredDrafts = loadRetiredDrafts();
|
|
@@ -3935,7 +3945,17 @@ endButton.onclick = () => {
|
|
|
3935
3945
|
};
|
|
3936
3946
|
handoffTakeoverButton.onclick = () => location.reload();
|
|
3937
3947
|
if (outdatedReloadButton) outdatedReloadButton.onclick = () => reloadChromeForOutdatedBanner();
|
|
3938
|
-
if (outdatedDismissButton)
|
|
3948
|
+
if (outdatedDismissButton) {
|
|
3949
|
+
outdatedDismissButton.onclick = () => {
|
|
3950
|
+
// A dismissed unreachable banner stays dismissed until the stream actually recovers; without
|
|
3951
|
+
// this the next failed reconnect puts it straight back on screen.
|
|
3952
|
+
if (unreachableBannerOwned) {
|
|
3953
|
+
unreachableBannerOwned = false;
|
|
3954
|
+
unreachableDismissed = true;
|
|
3955
|
+
}
|
|
3956
|
+
setChromeOutdated(false);
|
|
3957
|
+
};
|
|
3958
|
+
}
|
|
3939
3959
|
document.addEventListener("mousedown", (event) => {
|
|
3940
3960
|
const target = /** @type {Node} */ (event.target);
|
|
3941
3961
|
if (!moreMenu.hidden && !moreWrap.contains(target)) setMenuOpen(moreButton, moreMenu, false);
|
|
@@ -3998,6 +4018,12 @@ function connectLiveEvents() {
|
|
|
3998
4018
|
const socket = new WebSocket(protocol + "//" + location.host + "/events/" + encodeURIComponent(key));
|
|
3999
4019
|
socket.addEventListener("open", () => {
|
|
4000
4020
|
eventReconnectDelayMs = 500;
|
|
4021
|
+
liveEventFailures = 0;
|
|
4022
|
+
unreachableDismissed = false;
|
|
4023
|
+
if (unreachableBannerOwned) {
|
|
4024
|
+
unreachableBannerOwned = false;
|
|
4025
|
+
setChromeOutdated(false);
|
|
4026
|
+
}
|
|
4001
4027
|
refreshLayoutWarnings();
|
|
4002
4028
|
});
|
|
4003
4029
|
socket.addEventListener("message", (message) => {
|
|
@@ -4009,11 +4035,23 @@ function connectLiveEvents() {
|
|
|
4009
4035
|
}
|
|
4010
4036
|
});
|
|
4011
4037
|
socket.addEventListener("close", () => {
|
|
4038
|
+
liveEventFailures += 1;
|
|
4039
|
+
noteLiveEventsUnreachable();
|
|
4012
4040
|
setTimeout(connectLiveEvents, eventReconnectDelayMs);
|
|
4013
4041
|
eventReconnectDelayMs = Math.min(eventReconnectDelayMs * 2, 5000);
|
|
4014
4042
|
});
|
|
4015
4043
|
}
|
|
4016
4044
|
|
|
4045
|
+
// Raise the existing banner once the stream has been down long enough to mean it, and never
|
|
4046
|
+
// against an ended session or a banner someone else owns. Reconnecting retires it.
|
|
4047
|
+
function noteLiveEventsUnreachable() {
|
|
4048
|
+
if (liveEventFailures < LIVE_EVENT_UNREACHABLE_FAILURES) return;
|
|
4049
|
+
if (ended || unreachableDismissed || unreachableBannerOwned) return;
|
|
4050
|
+
if (outdatedBanner && !outdatedBanner.hidden) return;
|
|
4051
|
+
unreachableBannerOwned = true;
|
|
4052
|
+
setChromeOutdated(true, "");
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4017
4055
|
events.set("reload", () => {
|
|
4018
4056
|
resetFrame().then((reloaded) => {
|
|
4019
4057
|
if (reloaded) refreshWhiteboardSource();
|
package/dist/cli.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeFileSync as writeFileSync2
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { access, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
21
|
-
import
|
|
21
|
+
import os4 from "node:os";
|
|
22
22
|
import path8 from "node:path";
|
|
23
23
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
24
24
|
import { AxiError, installSessionStartHooks, RESERVED_COMMANDS, runAxiCli } from "axi-sdk-js";
|
|
@@ -249,6 +249,38 @@ var PLAYBOOKS = [
|
|
|
249
249
|
"End input paths with an obvious way for the user to send feedback back to the agent."
|
|
250
250
|
]
|
|
251
251
|
},
|
|
252
|
+
{
|
|
253
|
+
id: "explanation",
|
|
254
|
+
use_when: "Explain an existing system, PR, incident, or decision to a reader who was not there - when the goal is understanding what is and why, not choosing a direction or inspecting a plan before implementation",
|
|
255
|
+
choose: [
|
|
256
|
+
"Use this when the reader needs to understand something that already exists: a PR's mechanism, an incident's root cause, an architecture, a past decision.",
|
|
257
|
+
"Use the plan playbook when the reader must inspect and approve an approach before implementation begins; use comparison when they must choose between options.",
|
|
258
|
+
"Combine with diagram for flows or architecture, table for evidence inventories, and code when the mechanism lives in specific lines."
|
|
259
|
+
],
|
|
260
|
+
structure: [
|
|
261
|
+
"Lead with the one-sentence answer to the question the reader actually has, before any mechanism.",
|
|
262
|
+
"Then show only what changed or how it works - a flow or before/after of the relevant slice, not a diagram of the whole system.",
|
|
263
|
+
"Keep evidence (file paths, line references, links, commands) subordinate to the narrative: cited where a claim needs support, never inlined wholesale.",
|
|
264
|
+
"Make each claim its own section or annotation target so the reader can push back on exactly the part they disagree with.",
|
|
265
|
+
"End with what was deliberately left out and where to look next, not a summary that restates the piece."
|
|
266
|
+
],
|
|
267
|
+
design_rules: [
|
|
268
|
+
"Define unfamiliar terms at first use; for the reader's starting point, follow the diagram playbook's assume-nothing rule rather than restating it here.",
|
|
269
|
+
"Name the question the explanation answers at the top, so the reader knows whether it is their question.",
|
|
270
|
+
"Put prose beside figures - inline SVG for the flow or before/after, HTML for the reasoning - per the diagram playbook.",
|
|
271
|
+
"Link evidence rather than pasting logs or diffs; inlined evidence buries the narrative and goes stale.",
|
|
272
|
+
"Distinguish verified claims (cited to files, commands, or links) from inference; label uncertain reasoning as a question."
|
|
273
|
+
],
|
|
274
|
+
pitfalls: [
|
|
275
|
+
"Do not restate the PR body, diff, or ticket file-by-file; the source documents already exist and the reader can open them.",
|
|
276
|
+
"Do not bury the answer under a diagram of the entire system when the question is about one slice of it.",
|
|
277
|
+
"Do not present inferred reasoning as verified fact; cite or label it."
|
|
278
|
+
],
|
|
279
|
+
lavish_notes: [
|
|
280
|
+
"A Lavish explanation should let the reader annotate the exact claim they doubt or want expanded.",
|
|
281
|
+
"When an explanation surfaces a disagreement, queue prompts that name the claim and the evidence gap."
|
|
282
|
+
]
|
|
283
|
+
},
|
|
252
284
|
{
|
|
253
285
|
id: "slides",
|
|
254
286
|
use_when: "Create a deliberate presentation when slides are requested",
|
|
@@ -7896,6 +7928,31 @@ function serializeChatSync(session) {
|
|
|
7896
7928
|
};
|
|
7897
7929
|
}
|
|
7898
7930
|
|
|
7931
|
+
// src/local-address.js
|
|
7932
|
+
import os3 from "node:os";
|
|
7933
|
+
function isLocalAddressPresent(host) {
|
|
7934
|
+
if (typeof host !== "string" || host === "") return false;
|
|
7935
|
+
try {
|
|
7936
|
+
for (const entries of Object.values(os3.networkInterfaces() || {})) {
|
|
7937
|
+
for (const entry of entries || []) {
|
|
7938
|
+
if (entry?.address === host) return true;
|
|
7939
|
+
}
|
|
7940
|
+
}
|
|
7941
|
+
} catch {
|
|
7942
|
+
return false;
|
|
7943
|
+
}
|
|
7944
|
+
return false;
|
|
7945
|
+
}
|
|
7946
|
+
|
|
7947
|
+
// src/server-log.js
|
|
7948
|
+
function formatServerLogLine(line, now = /* @__PURE__ */ new Date()) {
|
|
7949
|
+
return `${now.toISOString()} ${line}`;
|
|
7950
|
+
}
|
|
7951
|
+
var STDIO_STAMPED = /* @__PURE__ */ Symbol.for("lavish-axi.stdio-timestamped");
|
|
7952
|
+
function serverStdioIsTimestamped() {
|
|
7953
|
+
return Boolean(globalThis[STDIO_STAMPED]);
|
|
7954
|
+
}
|
|
7955
|
+
|
|
7899
7956
|
// src/html-transform.js
|
|
7900
7957
|
function injectLavishSdk(html, key, artifactRevision, artifactLoadToken = "") {
|
|
7901
7958
|
const revisionNumber = Number(artifactRevision);
|
|
@@ -9328,8 +9385,9 @@ var designAssetUrls = {
|
|
|
9328
9385
|
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
9329
9386
|
var WHITEBOARD_CHANNEL_TOKEN_TTL_MS = 5 * 6e4;
|
|
9330
9387
|
var NETWORK_RECONCILE_CACHE_MS = 1e3;
|
|
9331
|
-
var
|
|
9388
|
+
var BIND_RETRY_DELAYS_MS = [100, 250, 500];
|
|
9332
9389
|
var WEBSOCKET_CLOSE_GRACE_MS = 250;
|
|
9390
|
+
var LIVE_EVENT_HEARTBEAT_MS = 3e4;
|
|
9333
9391
|
var BROWSER_DISCONNECT_GRACE_MS = 1e4;
|
|
9334
9392
|
var ARTIFACT_CONTENT_SECURITY_POLICY = "sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads";
|
|
9335
9393
|
var ATTACHMENT_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
@@ -9428,6 +9486,7 @@ async function serve({
|
|
|
9428
9486
|
debug = false,
|
|
9429
9487
|
log = null,
|
|
9430
9488
|
pollHeartbeatMs = 15e3,
|
|
9489
|
+
liveEventHeartbeatMs = LIVE_EVENT_HEARTBEAT_MS,
|
|
9431
9490
|
browserDisconnectGraceMs = BROWSER_DISCONNECT_GRACE_MS,
|
|
9432
9491
|
idleTimeoutMs = resolveIdleTimeoutMs(),
|
|
9433
9492
|
host = bindHost(env),
|
|
@@ -9452,6 +9511,7 @@ async function serve({
|
|
|
9452
9511
|
});
|
|
9453
9512
|
const activeTailscaleNetwork = tailscaleNetworkKey(tailscale);
|
|
9454
9513
|
let tailscalePhoneReady = false;
|
|
9514
|
+
const absentRequestedHosts = [];
|
|
9455
9515
|
let networkWarning = typeof tailscale?.warning === "string" ? tailscale.warning : "";
|
|
9456
9516
|
let resolvedLinkHost = linkHostName ?? resolveLinkHost({ env, tailscale, fallbackHost: host });
|
|
9457
9517
|
const app = express();
|
|
@@ -9466,7 +9526,7 @@ async function serve({
|
|
|
9466
9526
|
const outstandingRepairBatches = /* @__PURE__ */ new Set();
|
|
9467
9527
|
const diagnosticViewportClasses = resolveDiagnosticViewportClasses();
|
|
9468
9528
|
const verbose = debug || env.LAVISH_AXI_DEBUG === "1";
|
|
9469
|
-
const writeLog = typeof log === "function" ? log : (line) => process.stderr.write(`${line}
|
|
9529
|
+
const writeLog = typeof log === "function" ? log : (line) => process.stderr.write(`${serverStdioIsTimestamped() ? line : formatServerLogLine(line)}
|
|
9470
9530
|
`);
|
|
9471
9531
|
const logEvent = verbose ? (line) => writeLog(`[lavish] ${line}`) : null;
|
|
9472
9532
|
if (networkWarning) writeLog(`[lavish] WARNING: ${networkWarning}`);
|
|
@@ -9534,6 +9594,14 @@ async function serve({
|
|
|
9534
9594
|
client.sendEvent("agent-presence", { state: computePresence(key, activePolls, deliveredFeedback) });
|
|
9535
9595
|
if (session?.status === "ended") client.sendEvent("ended", { ended_by: session.ended_by || null });
|
|
9536
9596
|
}
|
|
9597
|
+
function requestedBindIsRecoverable() {
|
|
9598
|
+
return absentRequestedHosts.some((listenHost) => isLocalAddressPresent(listenHost));
|
|
9599
|
+
}
|
|
9600
|
+
async function reconcileNetwork() {
|
|
9601
|
+
if (requestedBindIsRecoverable()) return true;
|
|
9602
|
+
if (!(autoTailscale && typeof detect === "function")) return false;
|
|
9603
|
+
return reconcileTailscaleNetwork();
|
|
9604
|
+
}
|
|
9537
9605
|
async function reconcileTailscaleNetwork() {
|
|
9538
9606
|
if (Date.now() - networkReconcileCheckedAt < NETWORK_RECONCILE_CACHE_MS) return cachedNetworkStale;
|
|
9539
9607
|
if (networkReconcilePromise) return networkReconcilePromise;
|
|
@@ -9680,7 +9748,7 @@ async function serve({
|
|
|
9680
9748
|
res.status(503).json({ ok: false, app: "lavish-axi", version });
|
|
9681
9749
|
return;
|
|
9682
9750
|
}
|
|
9683
|
-
const networkStale = req.query.reconcile_network === "1"
|
|
9751
|
+
const networkStale = req.query.reconcile_network === "1" ? await reconcileNetwork() : false;
|
|
9684
9752
|
res.json({
|
|
9685
9753
|
ok: true,
|
|
9686
9754
|
app: "lavish-axi",
|
|
@@ -9697,7 +9765,7 @@ async function serve({
|
|
|
9697
9765
|
const reloadKey = String(req.body?.reload_key || "");
|
|
9698
9766
|
const reason = SHUTDOWN_REASONS.has(String(req.body?.reason || "")) ? String(req.body.reason) : "";
|
|
9699
9767
|
res.json({ status: "shutting-down" });
|
|
9700
|
-
setImmediate(() => shutdown(reloadKey, reason));
|
|
9768
|
+
setImmediate(() => shutdown(reloadKey, reason, "shutdown-request"));
|
|
9701
9769
|
});
|
|
9702
9770
|
app.post("/api/sessions", async (req, res, next) => {
|
|
9703
9771
|
try {
|
|
@@ -10553,6 +10621,27 @@ ${body}`
|
|
|
10553
10621
|
};
|
|
10554
10622
|
webSocket.on("error", () => {
|
|
10555
10623
|
});
|
|
10624
|
+
if (liveEventHeartbeatMs != null && liveEventHeartbeatMs > 0) {
|
|
10625
|
+
let awaitingPong = false;
|
|
10626
|
+
const heartbeat = setInterval(() => {
|
|
10627
|
+
if (awaitingPong) {
|
|
10628
|
+
logEvent?.(`event WebSocket heartbeat missed session=${key}, terminating`);
|
|
10629
|
+
webSocket.terminate();
|
|
10630
|
+
return;
|
|
10631
|
+
}
|
|
10632
|
+
awaitingPong = true;
|
|
10633
|
+
try {
|
|
10634
|
+
webSocket.ping();
|
|
10635
|
+
} catch {
|
|
10636
|
+
webSocket.terminate();
|
|
10637
|
+
}
|
|
10638
|
+
}, liveEventHeartbeatMs);
|
|
10639
|
+
heartbeat.unref?.();
|
|
10640
|
+
webSocket.on("pong", () => {
|
|
10641
|
+
awaitingPong = false;
|
|
10642
|
+
});
|
|
10643
|
+
webSocket.once("close", () => clearInterval(heartbeat));
|
|
10644
|
+
}
|
|
10556
10645
|
const cleanup = attachLiveEventClient(client, key, (remove) => webSocket.once("close", remove));
|
|
10557
10646
|
sendInitialLiveEventState(client, key, cleanup).catch((error) => {
|
|
10558
10647
|
client.close(1011, "Failed to initialize live events");
|
|
@@ -10564,8 +10653,8 @@ ${body}`
|
|
|
10564
10653
|
const httpServers = [];
|
|
10565
10654
|
const boundHosts = [];
|
|
10566
10655
|
let boundPort = port;
|
|
10567
|
-
|
|
10568
|
-
|
|
10656
|
+
let lastBindError = null;
|
|
10657
|
+
async function bindListener(listenHost) {
|
|
10569
10658
|
let retryIndex = 0;
|
|
10570
10659
|
while (true) {
|
|
10571
10660
|
try {
|
|
@@ -10574,26 +10663,63 @@ ${body}`
|
|
|
10574
10663
|
if (boundPort === 0) boundPort = httpServer.address().port;
|
|
10575
10664
|
httpServers.push(httpServer);
|
|
10576
10665
|
boundHosts.push(listenHost);
|
|
10577
|
-
|
|
10666
|
+
return null;
|
|
10578
10667
|
} catch (error) {
|
|
10579
|
-
if (
|
|
10580
|
-
|
|
10581
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelays[retryIndex]));
|
|
10668
|
+
if (retryIndex < BIND_RETRY_DELAYS_MS.length) {
|
|
10669
|
+
await new Promise((resolve) => setTimeout(resolve, BIND_RETRY_DELAYS_MS[retryIndex]));
|
|
10582
10670
|
retryIndex += 1;
|
|
10583
10671
|
continue;
|
|
10584
10672
|
}
|
|
10585
|
-
|
|
10586
|
-
networkWarning = "Tailscale binding failed; there is no phone access. Lavish remains available on loopback.";
|
|
10587
|
-
writeLog(`[lavish] WARNING: ${networkWarning} Address: ${listenHost}:${boundPort}.`);
|
|
10588
|
-
} else {
|
|
10589
|
-
logEvent?.(`failed to bind ${listenHost}:${boundPort}: ${error instanceof Error ? error.message : error}`);
|
|
10590
|
-
}
|
|
10591
|
-
break;
|
|
10673
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
10592
10674
|
}
|
|
10593
10675
|
}
|
|
10594
10676
|
}
|
|
10677
|
+
for (const listenHost of listenHosts) {
|
|
10678
|
+
const error = await bindListener(listenHost);
|
|
10679
|
+
if (!error) continue;
|
|
10680
|
+
lastBindError = error;
|
|
10681
|
+
if (isAddressAbsentBindError(error)) absentRequestedHosts.push(listenHost);
|
|
10682
|
+
if (listenHost === tailscale?.ipv4) {
|
|
10683
|
+
networkWarning = "Tailscale binding failed; there is no phone access. Lavish remains available on loopback.";
|
|
10684
|
+
writeLog(`[lavish] WARNING: ${networkWarning} Address: ${listenHost}:${boundPort}.`);
|
|
10685
|
+
} else {
|
|
10686
|
+
logEvent?.(`failed to bind ${listenHost}:${boundPort}: ${error.message}`);
|
|
10687
|
+
}
|
|
10688
|
+
}
|
|
10689
|
+
let loopbackFallback = false;
|
|
10690
|
+
if (httpServers.length === 0 && !listenHosts.includes(LOOPBACK_HOST)) {
|
|
10691
|
+
const error = await bindListener(LOOPBACK_HOST);
|
|
10692
|
+
if (error) {
|
|
10693
|
+
lastBindError = error;
|
|
10694
|
+
} else {
|
|
10695
|
+
loopbackFallback = true;
|
|
10696
|
+
networkWarning = `Could not bind ${listenHosts.join(", ")}; Lavish fell back to loopback and is not reachable at that address.`;
|
|
10697
|
+
writeLog(`[lavish] WARNING: ${networkWarning} Address: ${listenHosts[0]}:${boundPort}.`);
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10595
10700
|
if (httpServers.length === 0) {
|
|
10596
|
-
throw new Error(
|
|
10701
|
+
throw new Error(
|
|
10702
|
+
`Lavish server failed to bind any address${lastBindError ? `: ${lastBindError.message}` : ""}`,
|
|
10703
|
+
lastBindError ? { cause: lastBindError } : void 0
|
|
10704
|
+
);
|
|
10705
|
+
}
|
|
10706
|
+
if (!boundHosts.includes(LOOPBACK_HOST) && !boundHosts.includes(listenHosts[0])) {
|
|
10707
|
+
const error = new Error(
|
|
10708
|
+
`Lavish server failed to bind a control-channel address${lastBindError ? `: ${lastBindError.message}` : ""}`,
|
|
10709
|
+
lastBindError ? { cause: lastBindError } : void 0
|
|
10710
|
+
);
|
|
10711
|
+
await Promise.all(
|
|
10712
|
+
httpServers.splice(0).map(
|
|
10713
|
+
(httpServer) => new Promise((resolve) => {
|
|
10714
|
+
httpServer.close(() => resolve(void 0));
|
|
10715
|
+
})
|
|
10716
|
+
)
|
|
10717
|
+
);
|
|
10718
|
+
boundHosts.length = 0;
|
|
10719
|
+
throw error;
|
|
10720
|
+
}
|
|
10721
|
+
if (loopbackFallback) {
|
|
10722
|
+
resolvedLinkHost = linkHostName ?? resolveLinkHost({ env, tailscale: null, fallbackHost: LOOPBACK_HOST });
|
|
10597
10723
|
}
|
|
10598
10724
|
tailscalePhoneReady = Boolean(tailscale?.ipv4 && boundHosts.includes(tailscale.ipv4));
|
|
10599
10725
|
if (tailscale?.ipv4 && !tailscalePhoneReady) {
|
|
@@ -10609,9 +10735,10 @@ ${body}`
|
|
|
10609
10735
|
}
|
|
10610
10736
|
publicPort = httpServers[0].address().port;
|
|
10611
10737
|
serverReady = true;
|
|
10612
|
-
function shutdown(reloadKey = "", reason = "") {
|
|
10738
|
+
function shutdown(reloadKey = "", reason = "", cause = "requested") {
|
|
10613
10739
|
if (shuttingDown) return;
|
|
10614
10740
|
shuttingDown = true;
|
|
10741
|
+
writeLog(`[lavish] shutting down: ${cause}${reason ? ` (reason=${reason})` : ""}`);
|
|
10615
10742
|
if (idleTimer) {
|
|
10616
10743
|
clearTimeout(idleTimer);
|
|
10617
10744
|
idleTimer = null;
|
|
@@ -10663,8 +10790,7 @@ ${body}`
|
|
|
10663
10790
|
idleTimer = setTimeout(() => {
|
|
10664
10791
|
idleTimer = null;
|
|
10665
10792
|
if (!shuttingDown && liveEventClients.size === 0 && activePolls.size === 0) {
|
|
10666
|
-
|
|
10667
|
-
shutdown();
|
|
10793
|
+
shutdown("", "", `idle-timeout after ${idleTimeoutMs}ms with no connections`);
|
|
10668
10794
|
}
|
|
10669
10795
|
}, idleTimeoutMs);
|
|
10670
10796
|
idleTimer.unref?.();
|
|
@@ -10674,8 +10800,7 @@ ${body}`
|
|
|
10674
10800
|
try {
|
|
10675
10801
|
const sessions = await store.listSessions();
|
|
10676
10802
|
if (sessions.every((session) => session.status === "ended")) {
|
|
10677
|
-
|
|
10678
|
-
setImmediate(shutdown);
|
|
10803
|
+
setImmediate(() => shutdown("", "", "last open session ended with no live connections"));
|
|
10679
10804
|
}
|
|
10680
10805
|
} catch {
|
|
10681
10806
|
}
|
|
@@ -10727,7 +10852,7 @@ ${body}`
|
|
|
10727
10852
|
hosts: boundHosts,
|
|
10728
10853
|
addresses: httpServers.map((server) => server.address()),
|
|
10729
10854
|
close: async () => {
|
|
10730
|
-
shutdown();
|
|
10855
|
+
shutdown("", "", "close() called");
|
|
10731
10856
|
await done;
|
|
10732
10857
|
},
|
|
10733
10858
|
done
|
|
@@ -10762,6 +10887,9 @@ function tailscaleNetworkKey(tailscale) {
|
|
|
10762
10887
|
${tailscale.ipv4}
|
|
10763
10888
|
${tailscale.magicDnsName}`;
|
|
10764
10889
|
}
|
|
10890
|
+
function isAddressAbsentBindError(error) {
|
|
10891
|
+
return error instanceof Error && "code" in error && error.code === "EADDRNOTAVAIL";
|
|
10892
|
+
}
|
|
10765
10893
|
function wantsHtml(req) {
|
|
10766
10894
|
const accept = String(req.get("accept") || "");
|
|
10767
10895
|
return accept.toLowerCase().includes("text/html");
|
|
@@ -11492,7 +11620,7 @@ var AGENT_REPLY_JSON_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ text: "
|
|
|
11492
11620
|
var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_JSON_ENVELOPE_BYTES;
|
|
11493
11621
|
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
11494
11622
|
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.";
|
|
11495
|
-
var VERSION = "0.1.
|
|
11623
|
+
var VERSION = "0.1.73";
|
|
11496
11624
|
function detectInvokingAgent(env = process.env) {
|
|
11497
11625
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
11498
11626
|
}
|
|
@@ -11586,7 +11714,7 @@ function telemetryCommandName(argv) {
|
|
|
11586
11714
|
}
|
|
11587
11715
|
function createHomeOutput({ bin, sessions, includeSessions = true, agent = "generic" }) {
|
|
11588
11716
|
return {
|
|
11589
|
-
bin: collapseHomeDirectory(bin,
|
|
11717
|
+
bin: collapseHomeDirectory(bin, os4.homedir()),
|
|
11590
11718
|
description: DESCRIPTION,
|
|
11591
11719
|
...includeSessions ? {
|
|
11592
11720
|
sessions: sessions.map((session) => ({
|
|
@@ -12272,7 +12400,7 @@ function generatedPasswordNote(password) {
|
|
|
12272
12400
|
}
|
|
12273
12401
|
async function stopCommand(args) {
|
|
12274
12402
|
const port = Number(flagValue(args, "--port") || defaultPort());
|
|
12275
|
-
const baseUrl =
|
|
12403
|
+
const { baseUrl } = await findRunningServer(port);
|
|
12276
12404
|
return shutdownServerOnPort(port, { baseUrl, currentVersion: VERSION });
|
|
12277
12405
|
}
|
|
12278
12406
|
async function shutdownServerOnPort(port, {
|
|
@@ -12505,7 +12633,7 @@ function sameResolvedPath(left, right) {
|
|
|
12505
12633
|
return path8.resolve(left) === path8.resolve(right);
|
|
12506
12634
|
}
|
|
12507
12635
|
}
|
|
12508
|
-
function resolveHookHomeDir(env = process.env, fallback =
|
|
12636
|
+
function resolveHookHomeDir(env = process.env, fallback = os4.homedir()) {
|
|
12509
12637
|
return env.HOME || fallback;
|
|
12510
12638
|
}
|
|
12511
12639
|
function resolveCopilotHookDir(env = process.env, homeDir = resolveHookHomeDir(env)) {
|
|
@@ -12615,10 +12743,42 @@ async function assertHtmlFile(file) {
|
|
|
12615
12743
|
function isHtmlPath(file) {
|
|
12616
12744
|
return file.toLowerCase().endsWith(".html") || file.toLowerCase().endsWith(".htm");
|
|
12617
12745
|
}
|
|
12746
|
+
function serverBaseUrls(port) {
|
|
12747
|
+
const urls = [`http://${hostForUrl(clientHost())}:${port}`];
|
|
12748
|
+
const loopback = `http://${hostForUrl(LOOPBACK_HOST)}:${port}`;
|
|
12749
|
+
if (!urls.includes(loopback)) urls.push(loopback);
|
|
12750
|
+
return urls;
|
|
12751
|
+
}
|
|
12752
|
+
var HEALTH_PROBE_TIMEOUT_MS = 500;
|
|
12753
|
+
async function findRunningServer(port, { reconcileNetwork = false } = {}) {
|
|
12754
|
+
const candidates = serverBaseUrls(port);
|
|
12755
|
+
let foreign = null;
|
|
12756
|
+
for (const baseUrl of candidates) {
|
|
12757
|
+
const health = await probeHealth(baseUrl, { reconcileNetwork, timeoutMs: HEALTH_PROBE_TIMEOUT_MS });
|
|
12758
|
+
if (!health) continue;
|
|
12759
|
+
if (health.app === "lavish-axi") return { baseUrl, health };
|
|
12760
|
+
if (!foreign) foreign = { baseUrl, health };
|
|
12761
|
+
}
|
|
12762
|
+
return foreign ?? { baseUrl: candidates[0], health: null };
|
|
12763
|
+
}
|
|
12764
|
+
async function probeHealth(baseUrl, { reconcileNetwork, timeoutMs }) {
|
|
12765
|
+
const controller = new AbortController();
|
|
12766
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
12767
|
+
try {
|
|
12768
|
+
const health = await Promise.race([
|
|
12769
|
+
Promise.resolve().then(() => fetchHealth(baseUrl, { reconcileNetwork, timeoutMs, signal: controller.signal })).catch(() => null),
|
|
12770
|
+
new Promise((resolve) => {
|
|
12771
|
+
controller.signal.addEventListener("abort", () => resolve(null), { once: true });
|
|
12772
|
+
})
|
|
12773
|
+
]);
|
|
12774
|
+
return health && typeof health === "object" ? health : null;
|
|
12775
|
+
} finally {
|
|
12776
|
+
clearTimeout(timer);
|
|
12777
|
+
}
|
|
12778
|
+
}
|
|
12618
12779
|
async function ensureServer({ forceRestart = false, reloadKey = "" } = {}) {
|
|
12619
12780
|
const port = defaultPort();
|
|
12620
|
-
const baseUrl =
|
|
12621
|
-
const existing = await fetchHealth(baseUrl, { reconcileNetwork: true });
|
|
12781
|
+
const { baseUrl, health: existing } = await findRunningServer(port, { reconcileNetwork: true });
|
|
12622
12782
|
if (existing && !shouldRestartServer(VERSION, existing, forceRestart)) {
|
|
12623
12783
|
return baseUrl;
|
|
12624
12784
|
}
|
|
@@ -12638,15 +12798,16 @@ async function ensureServer({ forceRestart = false, reloadKey = "" } = {}) {
|
|
|
12638
12798
|
}
|
|
12639
12799
|
}
|
|
12640
12800
|
await startServer(port);
|
|
12641
|
-
|
|
12801
|
+
const replacedForNetwork = Boolean(existing) && existing.app === "lavish-axi" && existing.network_stale === true && !forceRestart && typeof existing.version === "string" && existing.version === VERSION;
|
|
12802
|
+
let networkRestarted = replacedForNetwork;
|
|
12642
12803
|
let deadline = Date.now() + 5e3;
|
|
12643
12804
|
while (Date.now() < deadline) {
|
|
12644
|
-
const health = await
|
|
12645
|
-
if (health && !shouldRestartServer(VERSION, health)) return
|
|
12805
|
+
const { baseUrl: liveUrl, health } = await findRunningServer(port, { reconcileNetwork: true });
|
|
12806
|
+
if (health && !shouldRestartServer(VERSION, health)) return liveUrl;
|
|
12646
12807
|
if (health?.network_stale === true && health.app === "lavish-axi") {
|
|
12647
|
-
if (networkRestarted) return
|
|
12648
|
-
await requestShutdown(
|
|
12649
|
-
if (!await waitForPortFree(
|
|
12808
|
+
if (networkRestarted) return liveUrl;
|
|
12809
|
+
await requestShutdown(liveUrl, { reloadKey, reason: "" });
|
|
12810
|
+
if (!await waitForPortFree(liveUrl, 3e3)) break;
|
|
12650
12811
|
await startServer(port);
|
|
12651
12812
|
networkRestarted = true;
|
|
12652
12813
|
deadline = Date.now() + 5e3;
|
|
@@ -12692,10 +12853,11 @@ async function canControlServerOnPort(port, healthBody, processMatchesLavish) {
|
|
|
12692
12853
|
if (typeof healthBody.version === "string" && healthBody.version !== "") return false;
|
|
12693
12854
|
return processMatchesLavish(port);
|
|
12694
12855
|
}
|
|
12695
|
-
async function fetchHealth(baseUrl, { reconcileNetwork = false } = {}) {
|
|
12856
|
+
async function fetchHealth(baseUrl, { reconcileNetwork = false, timeoutMs, signal } = {}) {
|
|
12696
12857
|
try {
|
|
12697
12858
|
const suffix = reconcileNetwork ? "?reconcile_network=1" : "";
|
|
12698
|
-
const
|
|
12859
|
+
const abortSignal = signal ?? (timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0);
|
|
12860
|
+
const response = await fetch(`${baseUrl}/health${suffix}`, abortSignal ? { signal: abortSignal } : {});
|
|
12699
12861
|
if (!response.ok) return null;
|
|
12700
12862
|
return await response.json();
|
|
12701
12863
|
} catch {
|
|
@@ -12772,9 +12934,9 @@ async function startServer(port) {
|
|
|
12772
12934
|
}
|
|
12773
12935
|
}
|
|
12774
12936
|
function resolveServerEntry() {
|
|
12775
|
-
const
|
|
12776
|
-
if (existsSync3(
|
|
12777
|
-
return fileURLToPath4(import.meta.url);
|
|
12937
|
+
const sourceEntry = fileURLToPath4(new URL("../bin/lavish-axi-server.js", import.meta.url));
|
|
12938
|
+
if (existsSync3(sourceEntry)) return sourceEntry;
|
|
12939
|
+
return fileURLToPath4(new URL("./server.mjs", import.meta.url));
|
|
12778
12940
|
}
|
|
12779
12941
|
function createServerSpawnOptions(logFd = null) {
|
|
12780
12942
|
const stdio = (
|
|
@@ -13045,7 +13207,7 @@ Shut down the background Lavish Editor server. The server also stops itself when
|
|
|
13045
13207
|
`,
|
|
13046
13208
|
playbook: `Usage: lavish-axi playbook [playbook_id]
|
|
13047
13209
|
|
|
13048
|
-
List focused artifact guidance playbooks, or show one playbook by ID. Known IDs: diagram, table, comparison, plan, code, input, slides.
|
|
13210
|
+
List focused artifact guidance playbooks, or show one playbook by ID. Known IDs: diagram, table, comparison, plan, code, input, explanation, slides.
|
|
13049
13211
|
|
|
13050
13212
|
${PLAYBOOK_ROUTER_HELP}
|
|
13051
13213
|
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server-log.js
|
|
4
|
+
function formatServerLogLine(line, now = /* @__PURE__ */ new Date()) {
|
|
5
|
+
return `${now.toISOString()} ${line}`;
|
|
6
|
+
}
|
|
7
|
+
var STDIO_STAMPED = /* @__PURE__ */ Symbol.for("lavish-axi.stdio-timestamped");
|
|
8
|
+
function serverStdioIsTimestamped() {
|
|
9
|
+
return Boolean(globalThis[STDIO_STAMPED]);
|
|
10
|
+
}
|
|
11
|
+
function createTimestampedWrite(write, now = () => /* @__PURE__ */ new Date()) {
|
|
12
|
+
let atLineStart = true;
|
|
13
|
+
return function timestampedWrite(chunk, encoding, callback) {
|
|
14
|
+
let enc = encoding;
|
|
15
|
+
let cb = callback;
|
|
16
|
+
if (typeof encoding === "function") {
|
|
17
|
+
cb = encoding;
|
|
18
|
+
enc = void 0;
|
|
19
|
+
}
|
|
20
|
+
const str = chunkToString(chunk, enc);
|
|
21
|
+
let out = "";
|
|
22
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
23
|
+
if (atLineStart) {
|
|
24
|
+
out += formatServerLogLine("", now());
|
|
25
|
+
atLineStart = false;
|
|
26
|
+
}
|
|
27
|
+
const ch = str[i];
|
|
28
|
+
out += ch;
|
|
29
|
+
if (ch === "\n") atLineStart = true;
|
|
30
|
+
}
|
|
31
|
+
return write(out, enc, cb);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function installServerStdioTimestamps() {
|
|
35
|
+
if (serverStdioIsTimestamped()) return;
|
|
36
|
+
globalThis[STDIO_STAMPED] = true;
|
|
37
|
+
process.stdout.write = createTimestampedWrite(process.stdout.write.bind(process.stdout));
|
|
38
|
+
process.stderr.write = createTimestampedWrite(process.stderr.write.bind(process.stderr));
|
|
39
|
+
}
|
|
40
|
+
function chunkToString(chunk, encoding) {
|
|
41
|
+
if (typeof chunk === "string") return chunk;
|
|
42
|
+
if (Buffer.isBuffer(chunk)) {
|
|
43
|
+
const bufferEncoding = (
|
|
44
|
+
/** @type {BufferEncoding} */
|
|
45
|
+
typeof encoding === "string" ? encoding : "utf8"
|
|
46
|
+
);
|
|
47
|
+
return chunk.toString(bufferEncoding);
|
|
48
|
+
}
|
|
49
|
+
return String(chunk);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// bin/lavish-axi-server.js
|
|
53
|
+
installServerStdioTimestamps();
|
|
54
|
+
try {
|
|
55
|
+
await import("./cli.mjs");
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
package/package.json
CHANGED
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.
|
|
4
|
+
"version": "0.1.73",
|
|
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",
|