lavish-axi 0.1.17 → 0.1.18
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 -0
- package/dist/cli.mjs +123 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ pnpm link
|
|
|
101
101
|
- **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.
|
|
102
102
|
- **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.
|
|
103
103
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
104
|
+
- **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.
|
|
105
|
+
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
104
106
|
- **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
|
|
105
107
|
|
|
106
108
|
## CLI Reference
|
|
@@ -111,6 +113,7 @@ pnpm link
|
|
|
111
113
|
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session. |
|
|
112
114
|
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session. |
|
|
113
115
|
| `lavish-axi end <html-file>` | End a session. |
|
|
116
|
+
| `lavish-axi stop` | Shut down the background server. |
|
|
114
117
|
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook. |
|
|
115
118
|
| `lavish-axi design` | Show CDN snippet + DaisyUI component reference (opt-in). |
|
|
116
119
|
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, and OpenCode; restart the agent session afterward. |
|
|
@@ -125,6 +128,7 @@ Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `input`, `
|
|
|
125
128
|
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
126
129
|
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
|
|
127
130
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it. |
|
|
131
|
+
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
|
128
132
|
| `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. |
|
|
129
133
|
|
|
130
134
|
## Development
|
package/dist/cli.mjs
CHANGED
|
@@ -1195,7 +1195,24 @@ var designAssetUrls = {
|
|
|
1195
1195
|
type: "application/javascript"
|
|
1196
1196
|
}
|
|
1197
1197
|
};
|
|
1198
|
-
|
|
1198
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
1199
|
+
function resolveIdleTimeoutMs(env = process.env) {
|
|
1200
|
+
const raw = env.LAVISH_AXI_IDLE_TIMEOUT_MS?.trim();
|
|
1201
|
+
if (raw === void 0 || raw === "") return DEFAULT_IDLE_TIMEOUT_MS;
|
|
1202
|
+
if (raw === "0" || raw.toLowerCase() === "off") return null;
|
|
1203
|
+
const value = Number(raw);
|
|
1204
|
+
if (!Number.isFinite(value) || value <= 0) return DEFAULT_IDLE_TIMEOUT_MS;
|
|
1205
|
+
return value;
|
|
1206
|
+
}
|
|
1207
|
+
async function serve({
|
|
1208
|
+
port,
|
|
1209
|
+
stateFile: stateFile2,
|
|
1210
|
+
version = "",
|
|
1211
|
+
debug = false,
|
|
1212
|
+
log = null,
|
|
1213
|
+
pollHeartbeatMs = 15e3,
|
|
1214
|
+
idleTimeoutMs = resolveIdleTimeoutMs()
|
|
1215
|
+
}) {
|
|
1199
1216
|
const app = express();
|
|
1200
1217
|
const store = new SessionStore(stateFile2);
|
|
1201
1218
|
const events = new EventEmitter();
|
|
@@ -1267,6 +1284,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1267
1284
|
heartbeat.unref?.();
|
|
1268
1285
|
}
|
|
1269
1286
|
setPollActive(key, activePolls, deliveredFeedback, events, true);
|
|
1287
|
+
refreshIdleTimer();
|
|
1270
1288
|
const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(handleRespondError), timeoutMs);
|
|
1271
1289
|
let cleaned = false;
|
|
1272
1290
|
let responding = false;
|
|
@@ -1278,6 +1296,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1278
1296
|
events.off("feedback", onFeedback);
|
|
1279
1297
|
events.off("ended", onFeedback);
|
|
1280
1298
|
setPollActive(key, activePolls, deliveredFeedback, events, false);
|
|
1299
|
+
refreshIdleTimer();
|
|
1281
1300
|
};
|
|
1282
1301
|
const respond = async () => {
|
|
1283
1302
|
if (responding || res.writableEnded) return;
|
|
@@ -1326,6 +1345,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1326
1345
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
1327
1346
|
events.emit("ended", req.params.key);
|
|
1328
1347
|
res.json({ status: "ended" });
|
|
1348
|
+
await shutdownIfNoLiveSessions();
|
|
1329
1349
|
} catch (error) {
|
|
1330
1350
|
next(error);
|
|
1331
1351
|
}
|
|
@@ -1352,6 +1372,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1352
1372
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
1353
1373
|
events.emit("ended", key);
|
|
1354
1374
|
res.json({ status: "ended" });
|
|
1375
|
+
await shutdownIfNoLiveSessions();
|
|
1355
1376
|
} catch (error) {
|
|
1356
1377
|
next(error);
|
|
1357
1378
|
}
|
|
@@ -1414,6 +1435,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1414
1435
|
connection: "keep-alive"
|
|
1415
1436
|
});
|
|
1416
1437
|
sseClients.add(res);
|
|
1438
|
+
refreshIdleTimer();
|
|
1417
1439
|
const session = await store.findByKey(req.params.key);
|
|
1418
1440
|
const sendReload = (key) => {
|
|
1419
1441
|
if (key === req.params.key) {
|
|
@@ -1454,6 +1476,7 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1454
1476
|
events.off("reload", sendReload);
|
|
1455
1477
|
events.off("agent-reply", sendAgentReply);
|
|
1456
1478
|
events.off("agent-presence", sendPresence);
|
|
1479
|
+
refreshIdleTimer();
|
|
1457
1480
|
});
|
|
1458
1481
|
} catch (error) {
|
|
1459
1482
|
next(error);
|
|
@@ -1499,6 +1522,10 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1499
1522
|
function shutdown() {
|
|
1500
1523
|
if (shuttingDown) return;
|
|
1501
1524
|
shuttingDown = true;
|
|
1525
|
+
if (idleTimer) {
|
|
1526
|
+
clearTimeout(idleTimer);
|
|
1527
|
+
idleTimer = null;
|
|
1528
|
+
}
|
|
1502
1529
|
for (const res of sseClients) {
|
|
1503
1530
|
try {
|
|
1504
1531
|
res.write("event: chrome-reload\ndata: {}\n\n");
|
|
@@ -1517,6 +1544,35 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1517
1544
|
httpServer.closeAllConnections();
|
|
1518
1545
|
}
|
|
1519
1546
|
}
|
|
1547
|
+
let idleTimer = null;
|
|
1548
|
+
function refreshIdleTimer() {
|
|
1549
|
+
if (idleTimer) {
|
|
1550
|
+
clearTimeout(idleTimer);
|
|
1551
|
+
idleTimer = null;
|
|
1552
|
+
}
|
|
1553
|
+
if (shuttingDown || idleTimeoutMs == null) return;
|
|
1554
|
+
if (sseClients.size > 0 || activePolls.size > 0) return;
|
|
1555
|
+
idleTimer = setTimeout(() => {
|
|
1556
|
+
idleTimer = null;
|
|
1557
|
+
if (!shuttingDown && sseClients.size === 0 && activePolls.size === 0) {
|
|
1558
|
+
logEvent?.(`idle for ${idleTimeoutMs}ms with no connections, shutting down`);
|
|
1559
|
+
shutdown();
|
|
1560
|
+
}
|
|
1561
|
+
}, idleTimeoutMs);
|
|
1562
|
+
idleTimer.unref?.();
|
|
1563
|
+
}
|
|
1564
|
+
async function shutdownIfNoLiveSessions() {
|
|
1565
|
+
if (sseClients.size > 0 || activePolls.size > 0) return;
|
|
1566
|
+
try {
|
|
1567
|
+
const sessions = await store.listSessions();
|
|
1568
|
+
if (sessions.every((session) => session.status === "ended")) {
|
|
1569
|
+
logEvent?.("last open session ended with no live connections, shutting down");
|
|
1570
|
+
setImmediate(shutdown);
|
|
1571
|
+
}
|
|
1572
|
+
} catch {
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
refreshIdleTimer();
|
|
1520
1576
|
return {
|
|
1521
1577
|
port: httpServer.address().port,
|
|
1522
1578
|
close: async () => {
|
|
@@ -1832,9 +1888,9 @@ function normalizePagePath(path5) {
|
|
|
1832
1888
|
}
|
|
1833
1889
|
|
|
1834
1890
|
// src/cli.js
|
|
1835
|
-
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "server", "playbook", "design", "setup"]);
|
|
1891
|
+
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup"]);
|
|
1836
1892
|
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`.";
|
|
1837
|
-
var VERSION = "0.1.
|
|
1893
|
+
var VERSION = "0.1.18";
|
|
1838
1894
|
async function run(argv) {
|
|
1839
1895
|
await ensureStateDir();
|
|
1840
1896
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -1862,6 +1918,7 @@ async function run(argv) {
|
|
|
1862
1918
|
open: openCommand,
|
|
1863
1919
|
poll: pollCommand,
|
|
1864
1920
|
end: endCommand,
|
|
1921
|
+
stop: stopCommand,
|
|
1865
1922
|
playbook: playbookCommand,
|
|
1866
1923
|
design: designCommand,
|
|
1867
1924
|
setup: setupCommand,
|
|
@@ -1927,6 +1984,7 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
|
|
|
1927
1984
|
"Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work",
|
|
1928
1985
|
"Run `lavish-axi poll <html-file>` to wait for user feedback",
|
|
1929
1986
|
"Run `lavish-axi end <html-file>` to end a session",
|
|
1987
|
+
"Run `lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)",
|
|
1930
1988
|
"Run `lavish-axi playbook <playbook_id>` for focused artifact guidance",
|
|
1931
1989
|
DESIGN_SYSTEM_HINT,
|
|
1932
1990
|
"Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop"
|
|
@@ -2028,6 +2086,35 @@ async function endCommand(args) {
|
|
|
2028
2086
|
const response = await postJson(`${baseUrl}/api/end`, { file: absolute });
|
|
2029
2087
|
return { session: { file: absolute, status: response.status || "ended" } };
|
|
2030
2088
|
}
|
|
2089
|
+
async function stopCommand(args) {
|
|
2090
|
+
const port = Number(flagValue(args, "--port") || defaultPort());
|
|
2091
|
+
const baseUrl = `http://${LOOPBACK_HOST}:${port}`;
|
|
2092
|
+
return shutdownServerOnPort(port, { baseUrl, currentVersion: VERSION });
|
|
2093
|
+
}
|
|
2094
|
+
async function shutdownServerOnPort(port, {
|
|
2095
|
+
baseUrl = `http://${LOOPBACK_HOST}:${port}`,
|
|
2096
|
+
currentVersion = VERSION,
|
|
2097
|
+
fetchHealth: healthFetcher = fetchHealth,
|
|
2098
|
+
requestShutdown: shutdownRequester = requestShutdown,
|
|
2099
|
+
waitForPortFree: portFreeWaiter = waitForPortFree,
|
|
2100
|
+
killProcessOnPort: portKiller = killProcessOnPort,
|
|
2101
|
+
processMatchesLavish = processOnPortMatchesLavish
|
|
2102
|
+
} = {}) {
|
|
2103
|
+
const health = await healthFetcher(baseUrl);
|
|
2104
|
+
if (!health) {
|
|
2105
|
+
return { server: { status: "not-running", port } };
|
|
2106
|
+
}
|
|
2107
|
+
if (!await canControlServerOnPort(port, health, processMatchesLavish)) {
|
|
2108
|
+
return { server: { status: "not-lavish", port } };
|
|
2109
|
+
}
|
|
2110
|
+
await shutdownRequester(baseUrl);
|
|
2111
|
+
let freed = await portFreeWaiter(baseUrl, 3e3);
|
|
2112
|
+
if (!freed && shouldKillProcessOnPort(currentVersion, health)) {
|
|
2113
|
+
portKiller(port);
|
|
2114
|
+
freed = await portFreeWaiter(baseUrl, 3e3);
|
|
2115
|
+
}
|
|
2116
|
+
return { server: { status: freed ? "stopped" : "stopping", port } };
|
|
2117
|
+
}
|
|
2031
2118
|
async function playbookCommand(args) {
|
|
2032
2119
|
return createPlaybookOutput(args);
|
|
2033
2120
|
}
|
|
@@ -2091,6 +2178,11 @@ async function ensureServer({ forceRestart = false } = {}) {
|
|
|
2091
2178
|
return baseUrl;
|
|
2092
2179
|
}
|
|
2093
2180
|
if (existing) {
|
|
2181
|
+
if (!await canControlServerOnPort(port, existing, processOnPortMatchesLavish)) {
|
|
2182
|
+
throw new AxiError(`Port ${port} is occupied by a non-Lavish server`, "SERVER_ERROR", [
|
|
2183
|
+
`Stop the process using port ${port}, or set LAVISH_AXI_PORT to another port`
|
|
2184
|
+
]);
|
|
2185
|
+
}
|
|
2094
2186
|
await requestShutdown(baseUrl);
|
|
2095
2187
|
const freed = await waitForPortFree(baseUrl, 2e3);
|
|
2096
2188
|
if (!freed) {
|
|
@@ -2132,6 +2224,12 @@ function shouldKillProcessOnPort(currentVersion, healthBody) {
|
|
|
2132
2224
|
if (healthBody.app !== "lavish-axi") return false;
|
|
2133
2225
|
return healthBody.version !== currentVersion;
|
|
2134
2226
|
}
|
|
2227
|
+
async function canControlServerOnPort(port, healthBody, processMatchesLavish) {
|
|
2228
|
+
if (!healthBody || typeof healthBody !== "object") return false;
|
|
2229
|
+
if (healthBody.app === "lavish-axi") return true;
|
|
2230
|
+
if (typeof healthBody.version === "string" && healthBody.version !== "") return false;
|
|
2231
|
+
return processMatchesLavish(port);
|
|
2232
|
+
}
|
|
2135
2233
|
async function fetchHealth(baseUrl) {
|
|
2136
2234
|
try {
|
|
2137
2235
|
const response = await fetch(`${baseUrl}/health`);
|
|
@@ -2171,6 +2269,23 @@ function killProcessOnPort(port) {
|
|
|
2171
2269
|
} catch {
|
|
2172
2270
|
}
|
|
2173
2271
|
}
|
|
2272
|
+
function processOnPortMatchesLavish(port) {
|
|
2273
|
+
try {
|
|
2274
|
+
const pids = spawnSync("lsof", ["-t", `-iTCP:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
|
|
2275
|
+
if (pids.status !== 0) return false;
|
|
2276
|
+
for (const line of pids.stdout.split("\n")) {
|
|
2277
|
+
const pid = Number(line.trim());
|
|
2278
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
|
|
2279
|
+
const command = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
2280
|
+
if (command.status === 0 && /lavish-axi/.test(command.stdout)) {
|
|
2281
|
+
return true;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
} catch {
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2174
2289
|
async function startServer(port) {
|
|
2175
2290
|
await ensureStateDir();
|
|
2176
2291
|
const entry = resolveServerEntry();
|
|
@@ -2272,6 +2387,7 @@ Usage:
|
|
|
2272
2387
|
lavish-axi <html-file>
|
|
2273
2388
|
lavish-axi poll <html-file> [--agent-reply "..."]
|
|
2274
2389
|
lavish-axi end <html-file>
|
|
2390
|
+
lavish-axi stop
|
|
2275
2391
|
lavish-axi playbook [playbook_id]
|
|
2276
2392
|
lavish-axi design
|
|
2277
2393
|
lavish-axi setup hooks
|
|
@@ -2293,6 +2409,10 @@ This command long-polls indefinitely for queued user prompts, then returns them
|
|
|
2293
2409
|
end: `Usage: lavish-axi end <html-file>
|
|
2294
2410
|
|
|
2295
2411
|
End a Lavish Editor session.
|
|
2412
|
+
`,
|
|
2413
|
+
stop: `Usage: lavish-axi stop [--port <port>]
|
|
2414
|
+
|
|
2415
|
+
Shut down the background Lavish Editor server. The server also stops itself when no browser or poll has been connected for a while (LAVISH_AXI_IDLE_TIMEOUT_MS, default 30m) and immediately when the last session ends with nothing connected.
|
|
2296
2416
|
`,
|
|
2297
2417
|
playbook: `Usage: lavish-axi playbook [playbook_id]
|
|
2298
2418
|
|