lavish-axi 0.1.45 → 0.1.47

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,6 +95,32 @@ This installs a `SessionStart` hook for **Claude Code**, **Codex**, **OpenCode**
95
95
  Unlike the skill, the hook also shows your live open sessions, so a fresh agent session can resume an in-flight review.
96
96
  **Restart your agent session after running this** so the new hook takes effect.
97
97
 
98
+ ### Agent Plugin
99
+
100
+ Lavish also ships as an [Agent Plugin](https://agent-plugins.org) - the vendor-neutral packaging standard for skills and MCP servers - so clients that speak that format can load it directly.
101
+
102
+ **No marketplace is involved.** The installed npm package _is_ the plugin: `plugin.json` sits at the package root next to the `skills/` directory, so whatever `npm install` already put on disk is a complete, conformant plugin. Install the CLI, then register it:
103
+
104
+ ```sh
105
+ npm install -g lavish-axi
106
+ lavish-axi setup plugin
107
+ ```
108
+
109
+ That registers the installed package with every supported client it finds - **VS Code**, **Cursor**, and **GitHub Copilot CLI** - and reports which ones were absent. It is opt-in and idempotent, and it repairs the registered path after a reinstall or relocation. Reload each client afterward.
110
+
111
+ Each client is registered independently: one that cannot be registered is reported with what to do about it, and never blocks the others or fails the command.
112
+
113
+ To register by hand instead, point any client at the package directory (`npm root -g`/`lavish-axi`):
114
+
115
+ | Client | Register with |
116
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
117
+ | VS Code | `"chat.pluginLocations": { "<package-dir>": true }` in user settings |
118
+ | Cursor | link the package dir at `~/.cursor/plugins/local/lavish-axi` (`setup plugin` handles Windows link compatibility) |
119
+ | GitHub Copilot CLI | `copilot plugin install <package-dir>` (or `copilot plugin install kunchenguid/lavish-axi` straight from the repo) |
120
+
121
+ Codex and ChatGPT install plugins only from marketplace sources, so Codex users should use the session hook above instead.
122
+ Lavish declares no MCP server - the CLI itself is the agent interface - so a plugin install brings the same `lavish` skill, and the skill and plugin are alternatives rather than a stack.
123
+
98
124
  ### From source
99
125
 
100
126
  ```sh
@@ -207,6 +233,7 @@ pnpm link
207
233
  | `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
208
234
  | `lavish-axi design` | Show agent-facing design guidance, including optional CDN and Mermaid snippets. |
209
235
  | `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward. |
236
+ | `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. |
210
237
  | `lavish-axi server` | Run the local Lavish Editor server. |
211
238
 
212
239
  Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
@@ -8,7 +8,6 @@ const queueStorageKey = "lavish-axi:queued:" + key;
8
8
  // Review-chrome state that must survive a browser refresh. Keyed per session so one review's
9
9
  // triage can never leak into another artifact's.
10
10
  const warningSelectionStorageKey = "lavish-axi:warning-selection:" + key;
11
- const warningAckStorageKey = "lavish-axi:warning-ack:" + key;
12
11
  const internalQueueKeyField = "_lavishQueueKey";
13
12
  const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
14
13
  const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
@@ -57,7 +56,6 @@ const layoutGateOverlay = /** @type {HTMLDivElement} */ (document.getElementById
57
56
  const layoutGateTitle = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateTitle"));
58
57
  const layoutGateCopy = /** @type {HTMLParagraphElement} */ (document.getElementById("layoutGateCopy"));
59
58
  const layoutGateAction = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateAction"));
60
- const layoutIssueBanner = /** @type {HTMLDivElement} */ (document.getElementById("layoutIssueBanner"));
61
59
  const warningsWrap = /** @type {HTMLDivElement} */ (document.getElementById("warningsWrap"));
62
60
  const warningsButton = /** @type {HTMLButtonElement} */ (document.getElementById("warningsButton"));
63
61
  const warningsCount = /** @type {HTMLSpanElement} */ (document.getElementById("warningsCount"));
@@ -96,7 +94,6 @@ let layoutGateTimer;
96
94
  let layoutWarnings = Array.isArray(sessionData.initialLayoutWarnings) ? sessionData.initialLayoutWarnings : [];
97
95
  const selectedWarningIds = new Set(loadJsonState(warningSelectionStorageKey, []));
98
96
  let warningsDrawerOpen = false;
99
- let warningsAcknowledged = loadJsonState(warningAckStorageKey, false) === true;
100
97
  const snapshotRequests = [];
101
98
  let endAfterSubmit = false;
102
99
  let workingBubble = null;
@@ -482,17 +479,6 @@ function normalizeLayoutFindings(value) {
482
479
  : [];
483
480
  }
484
481
 
485
- function setLayoutIssueBanner(visible) {
486
- if (!layoutIssueBanner) return;
487
- layoutIssueBanner.hidden = !visible;
488
- }
489
-
490
- // The banner is a one-time "there is unresolved work" cue for the top-bar inbox. Once the user
491
- // has opened the drawer it stays out of the way; the badge remains the standing signal.
492
- function refreshLayoutIssueBanner() {
493
- setLayoutIssueBanner(!ended && !warningsAcknowledged && !layoutGateVisible && activeWarnings().length > 0);
494
- }
495
-
496
482
  function clearLayoutGateTimer() {
497
483
  if (layoutGateTimer) clearTimeout(layoutGateTimer);
498
484
  layoutGateTimer = undefined;
@@ -522,7 +508,6 @@ function revealLayoutGate() {
522
508
  clearLayoutGateTimer();
523
509
  layoutGateArmed = false;
524
510
  setLayoutGateActive(false);
525
- refreshLayoutIssueBanner();
526
511
  }
527
512
 
528
513
  function forceRevealLayoutGate(reason) {
@@ -536,7 +521,6 @@ function startLayoutGateCycle() {
536
521
 
537
522
  layoutGateCycle += 1;
538
523
  layoutGateArmed = true;
539
- setLayoutIssueBanner(false);
540
524
  setLayoutGateCard("checking");
541
525
  setLayoutGateActive(true);
542
526
  clearLayoutGateTimer();
@@ -553,21 +537,14 @@ function startLayoutGateCycle() {
553
537
  // pending an agent repair: findings are the user's to triage, so a completed pass always reveals
554
538
  // and hands the result to the passive inbox.
555
539
  function handleLayoutGatePass() {
556
- if (!layoutGateEnabled || layoutGateManuallyBypassed) {
557
- refreshLayoutIssueBanner();
558
- return;
559
- }
560
- if (!layoutGateArmed && !layoutGateVisible) {
561
- refreshLayoutIssueBanner();
562
- return;
563
- }
540
+ if (!layoutGateEnabled || layoutGateManuallyBypassed) return;
541
+ if (!layoutGateArmed && !layoutGateVisible) return;
564
542
  revealLayoutGate();
565
543
  }
566
544
 
567
545
  function initializeLayoutGate() {
568
546
  if (!layoutGateEnabled) {
569
547
  setLayoutGateActive(false);
570
- setLayoutIssueBanner(false);
571
548
  return;
572
549
  }
573
550
 
@@ -821,7 +798,6 @@ function renderWarnings() {
821
798
  for (const warning of active) warningsList.appendChild(createWarningRow(warning));
822
799
  }
823
800
  updateWarningSelectionState();
824
- refreshLayoutIssueBanner();
825
801
  }
826
802
 
827
803
  function updateWarningSelectionState() {
@@ -854,9 +830,6 @@ function setWarningsDrawerOpen(open) {
854
830
  warningsButton.setAttribute("aria-expanded", String(warningsDrawerOpen));
855
831
  if (warningsDrawerOpen) {
856
832
  closeMenus();
857
- warningsAcknowledged = true;
858
- saveJsonState(warningAckStorageKey, true);
859
- refreshLayoutIssueBanner();
860
833
  warningsSelectAll.focus();
861
834
  }
862
835
  }
package/dist/chrome.css CHANGED
@@ -880,7 +880,6 @@ body.lavish {
880
880
  min-width: 0;
881
881
  min-height: 0;
882
882
  background: #fff;
883
- position: relative;
884
883
  overflow: hidden;
885
884
  }
886
885
  .panel {
@@ -1194,24 +1193,6 @@ body.lavish {
1194
1193
  .ended-action {
1195
1194
  margin-top: var(--space-8);
1196
1195
  }
1197
- .layout-issue-banner {
1198
- position: absolute;
1199
- top: var(--space-8);
1200
- left: var(--space-8);
1201
- right: var(--space-8);
1202
- z-index: 20;
1203
- border: 1px solid rgba(244, 201, 93, 0.35);
1204
- border-radius: var(--radius-lg);
1205
- background: rgba(37, 35, 15, 0.92);
1206
- color: var(--brass-400);
1207
- padding: var(--space-6) var(--space-8);
1208
- font-size: var(--text-sm);
1209
- line-height: var(--lh-sm);
1210
- box-shadow: var(--shadow-tooltip);
1211
- }
1212
- .layout-issue-banner[hidden] {
1213
- display: none;
1214
- }
1215
1196
  iframe {
1216
1197
  width: 100%;
1217
1198
  height: 100%;
package/dist/cli.mjs CHANGED
@@ -7,11 +7,11 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/cli.js
9
9
  import { spawn, spawnSync } from "node:child_process";
10
- import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
11
11
  import { access, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
12
- import os2 from "node:os";
13
- import path6 from "node:path";
14
- import { fileURLToPath as fileURLToPath3 } from "node:url";
12
+ import os3 from "node:os";
13
+ import path7 from "node:path";
14
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
15
15
  import { AxiError, installSessionStartHooks, RESERVED_COMMANDS, runAxiCli } from "axi-sdk-js";
16
16
 
17
17
  // src/playbooks.js
@@ -4285,14 +4285,179 @@ function defaultPort() {
4285
4285
  return Number(process.env.LAVISH_AXI_PORT || 4387);
4286
4286
  }
4287
4287
 
4288
+ // src/plugin.js
4289
+ import { randomUUID } from "node:crypto";
4290
+ import {
4291
+ chmodSync,
4292
+ existsSync,
4293
+ lstatSync,
4294
+ mkdirSync,
4295
+ readFileSync,
4296
+ readlinkSync,
4297
+ renameSync,
4298
+ rmSync,
4299
+ statSync,
4300
+ symlinkSync,
4301
+ writeFileSync
4302
+ } from "node:fs";
4303
+ import { createRequire } from "node:module";
4304
+ import os2 from "node:os";
4305
+ import path3 from "node:path";
4306
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4307
+ var crossSpawn = createRequire(import.meta.url)("cross-spawn");
4308
+ var PLUGIN_AUTHOR = Object.freeze({ name: "Kun Chen", url: "https://github.com/kunchenguid" });
4309
+ function spawnPluginClientSync(command, args) {
4310
+ return crossSpawn.sync(command, args, { encoding: "utf8" });
4311
+ }
4312
+ function resolvePluginRoot() {
4313
+ return path3.resolve(fileURLToPath2(new URL("../", import.meta.url)));
4314
+ }
4315
+ function readPluginManifest(root) {
4316
+ try {
4317
+ return JSON.parse(readFileSync(path3.join(root, "plugin.json"), "utf8"));
4318
+ } catch {
4319
+ return null;
4320
+ }
4321
+ }
4322
+ function isStalePluginLocation(candidate, pluginName) {
4323
+ const manifest = readPluginManifest(candidate);
4324
+ if (manifest) return manifest.name === pluginName;
4325
+ return !existsSync(candidate) && path3.basename(candidate) === pluginName;
4326
+ }
4327
+ function computeVsCodePluginLocationsUpdate(settings, pluginRoot, pluginName) {
4328
+ const updated = structuredClone(settings && typeof settings === "object" ? settings : {});
4329
+ const existing = updated["chat.pluginLocations"];
4330
+ const locations = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
4331
+ let changed = false;
4332
+ for (const key of Object.keys(locations)) {
4333
+ if (key !== pluginRoot && isStalePluginLocation(key, pluginName)) {
4334
+ delete locations[key];
4335
+ changed = true;
4336
+ }
4337
+ }
4338
+ if (locations[pluginRoot] !== true) {
4339
+ locations[pluginRoot] = true;
4340
+ changed = true;
4341
+ }
4342
+ updated["chat.pluginLocations"] = locations;
4343
+ return [updated, changed];
4344
+ }
4345
+ function resolveVsCodeSettingsFile(env = process.env, homeDir = os2.homedir(), platform = process.platform) {
4346
+ const platformPath = platform === "win32" ? path3.win32 : path3.posix;
4347
+ if (platform === "win32") {
4348
+ return platformPath.join(
4349
+ env.APPDATA || platformPath.join(homeDir, "AppData", "Roaming"),
4350
+ "Code",
4351
+ "User",
4352
+ "settings.json"
4353
+ );
4354
+ }
4355
+ if (platform === "darwin") {
4356
+ return platformPath.join(homeDir, "Library", "Application Support", "Code", "User", "settings.json");
4357
+ }
4358
+ return platformPath.join(
4359
+ env.XDG_CONFIG_HOME || platformPath.join(homeDir, ".config"),
4360
+ "Code",
4361
+ "User",
4362
+ "settings.json"
4363
+ );
4364
+ }
4365
+ function resolveCursorLocalPluginsDir(homeDir = os2.homedir()) {
4366
+ return path3.join(homeDir, ".cursor", "plugins", "local");
4367
+ }
4368
+ function writeTextFileAtomically(file, content, operations = {}) {
4369
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
4370
+ const write = operations.writeFileSync || writeFileSync;
4371
+ const rename2 = operations.renameSync || renameSync;
4372
+ const remove = operations.rmSync || rmSync;
4373
+ const chmod = operations.chmodSync || chmodSync;
4374
+ const stat2 = operations.statSync || statSync;
4375
+ let mode;
4376
+ try {
4377
+ mode = stat2(file).mode & 511;
4378
+ } catch {
4379
+ }
4380
+ try {
4381
+ write(temporary, content, mode === void 0 ? "utf8" : { encoding: "utf8", mode });
4382
+ if (mode !== void 0) chmod(temporary, mode);
4383
+ rename2(temporary, file);
4384
+ } catch (error) {
4385
+ try {
4386
+ remove(temporary, { force: true });
4387
+ } catch {
4388
+ }
4389
+ throw error;
4390
+ }
4391
+ }
4392
+ function createDirectoryLink(createSymlink, pluginRoot, linkPath, platform) {
4393
+ if (platform === "win32") {
4394
+ try {
4395
+ createSymlink(pluginRoot, linkPath, "junction");
4396
+ return;
4397
+ } catch {
4398
+ }
4399
+ }
4400
+ createSymlink(pluginRoot, linkPath);
4401
+ }
4402
+ function linkCursorLocalPlugin(localPluginsDir, pluginRoot, pluginName, operations = {}) {
4403
+ const target = path3.join(localPluginsDir, pluginName);
4404
+ const platform = operations.platform || process.platform;
4405
+ const createSymlink = (linkTarget, linkPath) => createDirectoryLink(operations.symlinkSync || symlinkSync, linkTarget, linkPath, platform);
4406
+ const rename2 = operations.renameSync || renameSync;
4407
+ const remove = operations.rmSync || rmSync;
4408
+ let existing = null;
4409
+ try {
4410
+ existing = lstatSync(target);
4411
+ } catch {
4412
+ }
4413
+ if (existing && !existing.isSymbolicLink()) {
4414
+ return { status: "occupied", target };
4415
+ }
4416
+ if (existing) {
4417
+ if (path3.resolve(readlinkSync(target)) === pluginRoot) return { status: "current", target };
4418
+ mkdirSync(localPluginsDir, { recursive: true });
4419
+ const replacement = `${target}.${process.pid}.${randomUUID()}.tmp`;
4420
+ const previous = `${target}.${process.pid}.${randomUUID()}.old`;
4421
+ let movedPrevious = false;
4422
+ try {
4423
+ createSymlink(pluginRoot, replacement);
4424
+ if (platform === "win32") {
4425
+ rename2(target, previous);
4426
+ movedPrevious = true;
4427
+ }
4428
+ rename2(replacement, target);
4429
+ if (movedPrevious) remove(previous, { force: true });
4430
+ } catch (error) {
4431
+ try {
4432
+ remove(replacement, { force: true });
4433
+ if (movedPrevious) rename2(previous, target);
4434
+ } catch {
4435
+ }
4436
+ return { status: "unsupported", target, reason: linkFailureReason(error) };
4437
+ }
4438
+ return { status: "repaired", target };
4439
+ }
4440
+ mkdirSync(localPluginsDir, { recursive: true });
4441
+ try {
4442
+ createSymlink(pluginRoot, target);
4443
+ } catch (error) {
4444
+ return { status: "unsupported", target, reason: linkFailureReason(error) };
4445
+ }
4446
+ return { status: "linked", target };
4447
+ }
4448
+ function linkFailureReason(error) {
4449
+ const message = String(error instanceof Error ? error.message : error).split("\n")[0];
4450
+ return message || "link creation failed";
4451
+ }
4452
+
4288
4453
  // src/server.js
4289
4454
  import crypto4 from "node:crypto";
4290
4455
  import { EventEmitter } from "node:events";
4291
- import { existsSync } from "node:fs";
4456
+ import { existsSync as existsSync2 } from "node:fs";
4292
4457
  import { readFile as readFile4 } from "node:fs/promises";
4293
4458
  import { homedir } from "node:os";
4294
- import path5 from "node:path";
4295
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4459
+ import path6 from "node:path";
4460
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
4296
4461
  import chokidar from "chokidar";
4297
4462
  import express from "express";
4298
4463
 
@@ -4792,15 +4957,15 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
4792
4957
  return node.parentElement || document.body;
4793
4958
  }
4794
4959
  function nodePath(node, root) {
4795
- const path7 = [];
4960
+ const path8 = [];
4796
4961
  let current = node;
4797
4962
  while (current && current !== root) {
4798
4963
  const parentNode = current.parentNode;
4799
4964
  if (!parentNode) break;
4800
- path7.unshift([...parentNode.childNodes].indexOf(current));
4965
+ path8.unshift([...parentNode.childNodes].indexOf(current));
4801
4966
  current = parentNode;
4802
4967
  }
4803
- return path7;
4968
+ return path8;
4804
4969
  }
4805
4970
  function rangeBoundary(node, offset) {
4806
4971
  const el = closestElement(node);
@@ -6358,7 +6523,7 @@ function mermaidSourceHash(source) {
6358
6523
 
6359
6524
  // src/whiteboard-store.js
6360
6525
  import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile } from "node:fs/promises";
6361
- import path3 from "node:path";
6526
+ import path4 from "node:path";
6362
6527
 
6363
6528
  // src/whiteboard-core.js
6364
6529
  var EXCALIDRAW_SCENE_TARGET_TYPE = "excalidraw-scene";
@@ -6410,13 +6575,13 @@ function assertValidRef(key, index) {
6410
6575
  if (!isValidDiagramIndex(index)) throw new Error(`invalid whiteboard diagram index: ${index}`);
6411
6576
  }
6412
6577
  function whiteboardDir(stateDir2, key) {
6413
- return path3.join(stateDir2, "whiteboards", String(key));
6578
+ return path4.join(stateDir2, "whiteboards", String(key));
6414
6579
  }
6415
6580
  function workingFile(stateDir2, key, index) {
6416
- return path3.join(whiteboardDir(stateDir2, key), `${Number(index)}.json`);
6581
+ return path4.join(whiteboardDir(stateDir2, key), `${Number(index)}.json`);
6417
6582
  }
6418
6583
  function writeQueueKey(stateDir2, key, index) {
6419
- return `${path3.resolve(stateDir2)}\0${key}\0${Number(index)}`;
6584
+ return `${path4.resolve(stateDir2)}\0${key}\0${Number(index)}`;
6420
6585
  }
6421
6586
  function queueWhiteboardWrite(stateDir2, key, index, operation) {
6422
6587
  const queueKey = writeQueueKey(stateDir2, key, index);
@@ -6446,8 +6611,8 @@ function whiteboardFeedbackPaths(stateDir2, key, index) {
6446
6611
  assertValidRef(key, index);
6447
6612
  const dir = whiteboardDir(stateDir2, key);
6448
6613
  return {
6449
- scenePath: path3.join(dir, `${Number(index)}.excalidraw`),
6450
- previewPath: path3.join(dir, `${Number(index)}.png`)
6614
+ scenePath: path4.join(dir, `${Number(index)}.excalidraw`),
6615
+ previewPath: path4.join(dir, `${Number(index)}.png`)
6451
6616
  };
6452
6617
  }
6453
6618
  async function saveWhiteboard(stateDir2, key, index, { sourceHash, textMetricsVersion = 0, scene, baseline = null }) {
@@ -6536,7 +6701,7 @@ ${script}`;
6536
6701
  // src/session-store.js
6537
6702
  import crypto3 from "node:crypto";
6538
6703
  import { readFile as readFile3, realpath as realpath2, writeFile as writeFile2 } from "node:fs/promises";
6539
- import path4 from "node:path";
6704
+ import path5 from "node:path";
6540
6705
  var LAYOUT_WARNINGS_TARGET_TYPE = "layout-warnings";
6541
6706
  var MAX_ARTIFACT_FAILURES = 20;
6542
6707
  var SessionStore = class {
@@ -6995,7 +7160,7 @@ var SessionStore = class {
6995
7160
  }
6996
7161
  };
6997
7162
  async function canonicalFile(file) {
6998
- const absolute = path4.resolve(file);
7163
+ const absolute = path5.resolve(file);
6999
7164
  return realpath2(absolute);
7000
7165
  }
7001
7166
  function sessionKey(file) {
@@ -7101,9 +7266,9 @@ var WHITEBOARD_CHANNEL_TOKEN_TTL_MS = 5 * 6e4;
7101
7266
  var RELOAD_DEBOUNCE_MS = 100;
7102
7267
  var BATCH_RELOAD_DEBOUNCE_MS = 900;
7103
7268
  function defaultWhiteboardAssetsDir() {
7104
- const packaged = fileURLToPath2(new URL("./whiteboard", import.meta.url));
7105
- if (existsSync(packaged)) return packaged;
7106
- return fileURLToPath2(new URL("../dist/whiteboard", import.meta.url));
7269
+ const packaged = fileURLToPath3(new URL("./whiteboard", import.meta.url));
7270
+ if (existsSync2(packaged)) return packaged;
7271
+ return fileURLToPath3(new URL("../dist/whiteboard", import.meta.url));
7107
7272
  }
7108
7273
  function isWhiteboardWriteApiPath(pathname) {
7109
7274
  return /^\/api\/[0-9a-f]{16}\/whiteboard\/\d{1,3}(\/feedback-files)?$/.test(String(pathname || ""));
@@ -7160,7 +7325,7 @@ async function serve({
7160
7325
  `);
7161
7326
  const logEvent = verbose ? (line) => writeLog(`[lavish] ${line}`) : null;
7162
7327
  let publicPort = port;
7163
- const whiteboardStateRoot = path5.dirname(stateFile2);
7328
+ const whiteboardStateRoot = path6.dirname(stateFile2);
7164
7329
  const allowedHostnames = buildAllowedHostnames({ host, linkHost: linkHostName, allowedHosts });
7165
7330
  if (!allowsAllHosts(allowedHosts)) {
7166
7331
  app.use((req, res, next) => {
@@ -7430,7 +7595,7 @@ async function serve({
7430
7595
  return;
7431
7596
  }
7432
7597
  const source = await readFile4(session.file, "utf8");
7433
- const root = path5.dirname(session.file);
7598
+ const root = path6.dirname(session.file);
7434
7599
  const { html, warnings } = await buildSelfContainedHtml(source, {
7435
7600
  baseDir: root,
7436
7601
  confineDir: root,
@@ -7458,7 +7623,7 @@ async function serve({
7458
7623
  }
7459
7624
  const body = req.body || {};
7460
7625
  const source = await readFile4(session.file, "utf8");
7461
- const root = path5.dirname(session.file);
7626
+ const root = path6.dirname(session.file);
7462
7627
  const { html, warnings } = await buildSelfContainedHtml(source, {
7463
7628
  baseDir: root,
7464
7629
  confineDir: root,
@@ -7603,7 +7768,7 @@ async function serve({
7603
7768
  res.status(404).send("Session not found");
7604
7769
  return;
7605
7770
  }
7606
- const root = path5.dirname(session.file);
7771
+ const root = path6.dirname(session.file);
7607
7772
  const file = resolveArtifactAsset(root, assetPath);
7608
7773
  if (!file) {
7609
7774
  res.status(403).send("Forbidden");
@@ -7736,8 +7901,8 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
7736
7901
  res.status(403).send("Forbidden");
7737
7902
  return;
7738
7903
  }
7739
- if (!existsSync(file)) {
7740
- res.status(404).send(existsSync(whiteboardAssetsDir) ? "Not found" : "Whiteboard bundle missing - run `pnpm run build`");
7904
+ if (!existsSync2(file)) {
7905
+ res.status(404).send(existsSync2(whiteboardAssetsDir) ? "Not found" : "Whiteboard bundle missing - run `pnpm run build`");
7741
7906
  return;
7742
7907
  }
7743
7908
  res.setHeader("access-control-allow-origin", "*");
@@ -7945,10 +8110,10 @@ function resolveDesignAssetPath(refPath) {
7945
8110
  if (!match) return null;
7946
8111
  const asset = designAssetUrls[match[1]];
7947
8112
  if (!asset) return null;
7948
- const packaged = fileURLToPath2(asset.packaged);
7949
- if (existsSync(packaged)) return packaged;
7950
- const source = fileURLToPath2(asset.source);
7951
- return existsSync(source) ? source : null;
8113
+ const packaged = fileURLToPath3(asset.packaged);
8114
+ if (existsSync2(packaged)) return packaged;
8115
+ const source = fileURLToPath3(asset.source);
8116
+ return existsSync2(source) ? source : null;
7952
8117
  }
7953
8118
  function exportContentDisposition(file) {
7954
8119
  const filename = exportFileName(file);
@@ -8028,9 +8193,9 @@ function optionalBodyString(value) {
8028
8193
  return trimmed || void 0;
8029
8194
  }
8030
8195
  function resolveArtifactAsset(root, assetPath) {
8031
- const file = path5.resolve(root, assetPath);
8032
- const relative = path5.relative(root, file);
8033
- if (relative.startsWith("..") || path5.isAbsolute(relative)) {
8196
+ const file = path6.resolve(root, assetPath);
8197
+ const relative = path6.relative(root, file);
8198
+ if (relative.startsWith("..") || path6.isAbsolute(relative)) {
8034
8199
  return null;
8035
8200
  }
8036
8201
  return file;
@@ -8066,7 +8231,7 @@ async function resolveWatchTarget(session) {
8066
8231
  const html = await readFile4(session.file, "utf8");
8067
8232
  if (hasLiveReloadRootOptIn(html)) {
8068
8233
  return {
8069
- path: path5.dirname(session.file),
8234
+ path: path6.dirname(session.file),
8070
8235
  scope: "directory",
8071
8236
  options: {
8072
8237
  ...baseOptions,
@@ -8265,7 +8430,7 @@ ${faviconTag}
8265
8430
  </head>
8266
8431
  <body class="${bodyClass}">
8267
8432
  <div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="spacer" aria-hidden="true"></div><div class="warnings-wrap" id="warningsWrap" hidden><button class="warnings-button" id="warningsButton" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="warningsDrawer">${chromeIcons.warning}<span class="warnings-count" id="warningsCount">0</span></button><div class="menu warnings-drawer" id="warningsDrawer" role="dialog" aria-labelledby="warningsTitle" aria-describedby="warningsSummary" hidden><div class="warnings-head"><h2 class="warnings-title" id="warningsTitle">Layout issues</h2><p class="warnings-summary" id="warningsSummary"></p></div><div class="warnings-toolbar"><label class="warnings-selectall"><input type="checkbox" id="warningsSelectAll"><span>Select all</span></label><span class="warnings-selected" id="warningsSelected" role="status" aria-live="polite"></span></div><div class="warnings-list" id="warningsList"></div><div class="warnings-foot"><p class="warnings-note">Queueing sends a repair request with your next feedback. An issue is marked resolved only after a newer artifact load and a complete check at the same viewport no longer finds it.</p><button class="button" id="warningsQueueButton" type="button" disabled>Queue selected fixes</button></div></div></div><button class="annotate-switch" id="annotation" type="button" aria-pressed="true" title="${escapeHtml(modeToggleHint)}"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml(pathHead)}</span><span class="path-tail">${escapeHtml(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
8268
- <div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe><div class="layout-issue-banner" id="layoutIssueBanner" hidden>Layout issues detected. Open <strong>Layout issues</strong> in the top bar to review and queue fixes.</div></div><aside class="panel"><h2>Conversation</h2><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></div><div class="composer"><div class="presence-banner handoff-banner" id="handoffBanner" hidden><span>This review is open in another Lavish tab.</span><button class="handoff-takeover" id="handoffTakeover" type="button">Take over here</button></div><div class="presence-banner" id="presenceBanner" hidden>Your agent is not listening. If this persists, ask your agent to poll for updates from Lavish.</div><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</div><div class="actions" id="sendActions"><button class="button button-danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send &amp; End</span></button><button class="button" id="send">Send to Agent</button></div></div></aside></div>
8433
+ <div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe></div><aside class="panel"><h2>Conversation</h2><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></div><div class="composer"><div class="presence-banner handoff-banner" id="handoffBanner" hidden><span>This review is open in another Lavish tab.</span><button class="handoff-takeover" id="handoffTakeover" type="button">Take over here</button></div><div class="presence-banner" id="presenceBanner" hidden>Your agent is not listening. If this persists, ask your agent to poll for updates from Lavish.</div><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</div><div class="actions" id="sendActions"><button class="button button-danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send &amp; End</span></button><button class="button" id="send">Send to Agent</button></div></div></aside></div>
8269
8434
  <div class="share-overlay" id="shareDialog" role="dialog" aria-modal="true" aria-labelledby="shareTitleText" hidden><form class="share-card" id="shareForm"><div class="share-head"><div><div class="share-kicker">Publish to <a class="share-link" href="https://ht-ml.app" target="_blank" rel="noopener noreferrer">ht-ml.app</a></div><h2 id="shareTitleText">Publish artifact</h2></div><button class="share-close" id="shareClose" type="button" aria-label="Close publish dialog"><svg width="14" height="14" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button></div><p class="share-note">ht-ml.app is a separate, third-party hosting service, not part of Lavish. Publishing sends this artifact to its servers.</p><p class="share-copy">This uploads this artifact to ht-ml.app with local assets inlined. Without a password, the page is PUBLIC and anyone with the link can open it. With a password, the page is PRIVATE and viewers must supply the password to view.</p><p class="share-note">Do not publish secrets. The Lavish annotation SDK is not included.</p><div class="share-grid"><label>Password (optional)<input id="sharePassword" name="password" type="password" autocomplete="new-password" placeholder="Leave blank for a public page"></label></div><div class="share-status" id="shareStatus" role="status"></div><div class="share-result" id="shareResult" hidden><label>Share URL<div class="share-copy-row"><input id="shareUrl" readonly><button class="share-copy-btn" id="copyShareUrl" type="button">Copy URL</button></div></label><label>Update key (secret)<div class="share-copy-row"><input id="shareUpdateKey" readonly><button class="share-copy-btn" id="copyUpdateKey" type="button">Copy key</button></div></label><p class="share-note">Keep the update key private. ht-ml.app returns it once and it is the only way to update or delete this page later.</p></div><div class="share-actions"><button class="share-cancel" id="shareCancel" type="button">Cancel</button><button class="button" id="sharePublish" type="submit">Publish</button></div></form></div>
8270
8435
  <div class="ended-overlay layout-gate-overlay" id="layoutGateOverlay"${layoutGateHidden}><div class="ended-card"><div class="ended-title" id="layoutGateTitle">Checking layout.<br>One moment.</div><p class="ended-copy" id="layoutGateCopy">Lavish is waiting for fonts and final geometry before revealing this artifact.</p><button class="button ended-action" id="layoutGateAction" type="button">Show anyway</button></div></div>
8271
8436
  <div class="ended-overlay" id="endedOverlay" hidden><div class="ended-card"><div class="ended-title">Session ended.<br>Return to your agent to continue.</div><p class="ended-copy">${escapeHtml(session.file)}</p></div></div>
@@ -8412,9 +8577,9 @@ var HttpTelemetryClient = class {
8412
8577
  if (!trimmed) return;
8413
8578
  this.send(trimmed, eventURL(this.app, trimmed), fields);
8414
8579
  }
8415
- pageview(path7, fields = {}) {
8580
+ pageview(path8, fields = {}) {
8416
8581
  if (this.closed) return;
8417
- this.send("", normalizePagePath(path7), fields);
8582
+ this.send("", normalizePagePath(path8), fields);
8418
8583
  }
8419
8584
  async close(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
8420
8585
  this.closed = true;
@@ -8489,8 +8654,8 @@ function eventURL(app, name) {
8489
8654
  if (!name) return `app://${app}`;
8490
8655
  return `app://${app}/${name.replace(/\./g, "/")}`;
8491
8656
  }
8492
- function normalizePagePath(path7) {
8493
- const trimmed = String(path7 || "").trim();
8657
+ function normalizePagePath(path8) {
8658
+ const trimmed = String(path8 || "").trim();
8494
8659
  if (!trimmed) return "/";
8495
8660
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
8496
8661
  }
@@ -8509,7 +8674,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
8509
8674
  ]);
8510
8675
  var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback is still delivered once. After that response, polling stops, and the agent must not reopen the session uninvited.";
8511
8676
  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.";
8512
- var VERSION = "0.1.45";
8677
+ var VERSION = "0.1.47";
8513
8678
  function detectInvokingAgent(env = process.env) {
8514
8679
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
8515
8680
  }
@@ -8521,7 +8686,15 @@ function pollExecutionGuidance({ agent = "generic" } = {}) {
8521
8686
  const agentGuidance = agent === "codex" ? ` ${CODEX_POLL_WAKE_PATH_GUIDANCE}` : "";
8522
8687
  return `${sharedGuidance}${agentGuidance}`;
8523
8688
  }
8689
+ function isVersionOnlyArgv(argv) {
8690
+ return argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V");
8691
+ }
8524
8692
  async function run(argv) {
8693
+ if (isVersionOnlyArgv(argv)) {
8694
+ process.stdout.write(`${VERSION}
8695
+ `);
8696
+ return;
8697
+ }
8525
8698
  await ensureStateDir();
8526
8699
  const normalizedArgv = normalizeArgv(argv);
8527
8700
  const agent = detectInvokingAgent(process.env);
@@ -8595,7 +8768,7 @@ function telemetryCommandName(argv) {
8595
8768
  }
8596
8769
  function createHomeOutput({ bin, sessions, includeSessions = true, agent = "generic" }) {
8597
8770
  return {
8598
- bin: collapseHomeDirectory(bin, os2.homedir()),
8771
+ bin: collapseHomeDirectory(bin, os3.homedir()),
8599
8772
  description: DESCRIPTION,
8600
8773
  ...includeSessions ? {
8601
8774
  sessions: sessions.map((session) => ({
@@ -8838,8 +9011,8 @@ async function exportCommand(args) {
8838
9011
  }
8839
9012
  await assertHtmlFile(file);
8840
9013
  const absolute = await canonicalFile(file);
8841
- const root = path6.dirname(absolute);
8842
- const output = path6.resolve(flagValue(args, "--out") || path6.join(root, exportFileName(absolute)));
9014
+ const root = path7.dirname(absolute);
9015
+ const output = path7.resolve(flagValue(args, "--out") || path7.join(root, exportFileName(absolute)));
8843
9016
  const source = await readFile5(absolute, "utf8");
8844
9017
  const { html, warnings } = await buildSelfContainedHtml(source, {
8845
9018
  baseDir: root,
@@ -8885,7 +9058,7 @@ async function shareCommand(args) {
8885
9058
  const absolute = await canonicalFile(file);
8886
9059
  const password = optionalFlagString(flagValue(args, "--password"));
8887
9060
  const token = optionalFlagString(flagValue(args, "--token"));
8888
- const root = path6.dirname(absolute);
9061
+ const root = path7.dirname(absolute);
8889
9062
  const source = await readFile5(absolute, "utf8");
8890
9063
  const { html, warnings } = await buildSelfContainedHtml(source, {
8891
9064
  baseDir: root,
@@ -8964,9 +9137,13 @@ async function designCommand() {
8964
9137
  return createDesignOutput();
8965
9138
  }
8966
9139
  async function setupCommand(args) {
8967
- if (args.length !== 1 || args[0] !== "hooks") {
8968
- throw new AxiError("Unknown setup action", "VALIDATION_ERROR", ["Run `lavish-axi setup hooks`"]);
9140
+ if (args.length !== 1 || args[0] !== "hooks" && args[0] !== "plugin") {
9141
+ throw new AxiError("Unknown setup action", "VALIDATION_ERROR", [
9142
+ "Run `lavish-axi setup hooks`",
9143
+ "Run `lavish-axi setup plugin`"
9144
+ ]);
8969
9145
  }
9146
+ if (args[0] === "plugin") return setupPluginCommand();
8970
9147
  const errors = [];
8971
9148
  installSessionStartHooks({
8972
9149
  marker: "lavish-axi",
@@ -8984,14 +9161,186 @@ async function setupCommand(args) {
8984
9161
  }
8985
9162
  return {
8986
9163
  hooks: { status: "installed", integrations: "Claude Code, Codex, OpenCode, GitHub Copilot CLI" },
8987
- help: ["Restart your agent session to receive lavish-axi ambient context"]
9164
+ help: [
9165
+ "Restart your agent session to receive lavish-axi ambient context",
9166
+ "Run `lavish-axi setup plugin` to also register the Agent Plugin in VS Code, Cursor, and GitHub Copilot CLI"
9167
+ ]
8988
9168
  };
8989
9169
  }
8990
- function resolveHookHomeDir(env = process.env, fallback = os2.homedir()) {
9170
+ async function setupPluginCommand() {
9171
+ const pluginRoot = resolvePluginRoot();
9172
+ const manifest = readPluginManifest(pluginRoot);
9173
+ if (!manifest) {
9174
+ throw new AxiError("No plugin.json found in the lavish-axi package", "SERVER_ERROR", [
9175
+ `Expected a manifest at ${path7.join(pluginRoot, "plugin.json")}`,
9176
+ "Reinstall lavish-axi, or run `npm run build:plugin` when working from a source checkout"
9177
+ ]);
9178
+ }
9179
+ const clients = [
9180
+ registerVsCodePlugin(pluginRoot, manifest.name),
9181
+ registerCursorPlugin(pluginRoot, manifest.name),
9182
+ registerCopilotPlugin(pluginRoot, manifest.name)
9183
+ ];
9184
+ const help = ["Restart or reload each client so it discovers the plugin"];
9185
+ if (clients.some((client) => client.status === "absent")) {
9186
+ help.push("Absent clients are skipped; re-run `lavish-axi setup plugin` after installing one");
9187
+ }
9188
+ if (clients.some((client) => client.status === "manual")) {
9189
+ help.push(`Register the plugin root manually where noted: ${pluginRoot}`);
9190
+ }
9191
+ return { plugin: { name: manifest.name, root: collapseHome(pluginRoot) }, clients, help };
9192
+ }
9193
+ function collapseHome(target) {
9194
+ const home = resolveHookHomeDir();
9195
+ return home && target.startsWith(home) ? `~${target.slice(home.length)}` : target;
9196
+ }
9197
+ function registerVsCodePlugin(pluginRoot, pluginName) {
9198
+ const settingsFile = resolveVsCodeSettingsFile(process.env, resolveHookHomeDir());
9199
+ const settingsDir = path7.dirname(settingsFile);
9200
+ const hasSettingsFile = existsSync3(settingsFile);
9201
+ if (!hasSettingsFile && !existsSync3(settingsDir)) {
9202
+ return { client: "vscode", status: "absent", detail: "no VS Code user configuration found" };
9203
+ }
9204
+ let settings = {};
9205
+ if (hasSettingsFile) {
9206
+ try {
9207
+ settings = JSON.parse(readFileSync2(settingsFile, "utf8"));
9208
+ } catch {
9209
+ return {
9210
+ client: "vscode",
9211
+ status: "manual",
9212
+ detail: `add "chat.pluginLocations": {"${pluginRoot}": true} to ${collapseHome(settingsFile)}`
9213
+ };
9214
+ }
9215
+ }
9216
+ const [updated, changed] = computeVsCodePluginLocationsUpdate(settings, pluginRoot, pluginName);
9217
+ if (!changed) return { client: "vscode", status: "current", detail: collapseHome(settingsFile) };
9218
+ try {
9219
+ const writeTarget = hasSettingsFile ? realpathSync(settingsFile) : settingsFile;
9220
+ writeTextFileAtomically(writeTarget, `${JSON.stringify(updated, null, 2)}
9221
+ `);
9222
+ } catch (error) {
9223
+ return { client: "vscode", status: "failed", detail: String(error instanceof Error ? error.message : error) };
9224
+ }
9225
+ return { client: "vscode", status: "registered", detail: collapseHome(settingsFile) };
9226
+ }
9227
+ function registerCursorPlugin(pluginRoot, pluginName) {
9228
+ const cursorDir = path7.join(resolveHookHomeDir(), ".cursor");
9229
+ if (!existsSync3(cursorDir)) {
9230
+ return { client: "cursor", status: "absent", detail: "no ~/.cursor directory found" };
9231
+ }
9232
+ try {
9233
+ const { status, target, reason } = linkCursorLocalPlugin(
9234
+ resolveCursorLocalPluginsDir(resolveHookHomeDir()),
9235
+ pluginRoot,
9236
+ pluginName
9237
+ );
9238
+ if (status === "occupied") {
9239
+ return { client: "cursor", status: "manual", detail: `${collapseHome(target)} exists and is not a symlink` };
9240
+ }
9241
+ if (status === "unsupported") {
9242
+ return {
9243
+ client: "cursor",
9244
+ status: "manual",
9245
+ detail: `cannot link ${collapseHome(target)} (${reason}); link it to ${pluginRoot} manually, or enable Developer Mode on Windows`
9246
+ };
9247
+ }
9248
+ return {
9249
+ client: "cursor",
9250
+ status: status === "current" ? "current" : "registered",
9251
+ detail: collapseHome(target)
9252
+ };
9253
+ } catch (error) {
9254
+ return { client: "cursor", status: "failed", detail: String(error instanceof Error ? error.message : error) };
9255
+ }
9256
+ }
9257
+ function registerCopilotPlugin(pluginRoot, pluginName) {
9258
+ const listed = spawnPluginClientSync("copilot", ["plugins", "list", "--scope", "user", "--kind", "plugin", "--json"]);
9259
+ if (listed.error) {
9260
+ return { client: "copilot", status: "absent", detail: "copilot CLI not found on PATH" };
9261
+ }
9262
+ if (listed.status !== 0) {
9263
+ const detail = String(listed.stderr || listed.stdout || `exit ${listed.status}`).trim();
9264
+ return {
9265
+ client: "copilot",
9266
+ status: "manual",
9267
+ detail: `could not verify installed plugins: ${detail.split("\n")[0]}`
9268
+ };
9269
+ }
9270
+ const records = parseCopilotPluginRecords(listed.stdout);
9271
+ if (!records) {
9272
+ return { client: "copilot", status: "manual", detail: "could not parse installed plugin records" };
9273
+ }
9274
+ const existing = records.find((record) => record.name === pluginName && (!record.kind || record.kind === "plugin"));
9275
+ if (existing) {
9276
+ const source = copilotPluginSourcePath(existing) || installedCopilotPluginSourcePath(pluginName);
9277
+ if (!source) {
9278
+ return { client: "copilot", status: "manual", detail: "could not verify the installed plugin source" };
9279
+ }
9280
+ if (sameResolvedPath(source, pluginRoot)) {
9281
+ return { client: "copilot", status: "current", detail: collapseHome(pluginRoot) };
9282
+ }
9283
+ }
9284
+ const installed = spawnPluginClientSync("copilot", ["plugin", "install", pluginRoot]);
9285
+ if (installed.status !== 0) {
9286
+ const detail = String(installed.stderr || installed.stdout || `exit ${installed.status}`).trim();
9287
+ return { client: "copilot", status: "failed", detail: detail.split("\n")[0] };
9288
+ }
9289
+ return { client: "copilot", status: "registered", detail: "copilot plugin install" };
9290
+ }
9291
+ function parseCopilotPluginRecords(output) {
9292
+ try {
9293
+ const parsed = JSON.parse(String(output));
9294
+ const records = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.plugins) ? parsed.plugins : parsed?.items;
9295
+ return Array.isArray(records) && records.every((record) => record && typeof record === "object") ? records : null;
9296
+ } catch {
9297
+ return null;
9298
+ }
9299
+ }
9300
+ function installedCopilotPluginSourcePath(pluginName) {
9301
+ const configDir = process.env.COPILOT_HOME || path7.join(resolveHookHomeDir(), ".copilot");
9302
+ try {
9303
+ const config = JSON.parse(readFileSync2(path7.join(configDir, "config.json"), "utf8"));
9304
+ const record = Array.isArray(config.installedPlugins) ? config.installedPlugins.find((candidate) => candidate?.name === pluginName) : null;
9305
+ return record ? copilotPluginSourcePath(record) : null;
9306
+ } catch {
9307
+ return null;
9308
+ }
9309
+ }
9310
+ function copilotPluginSourcePath(record) {
9311
+ const candidates = [
9312
+ record.sourcePath,
9313
+ record.source_path,
9314
+ record.pluginRoot,
9315
+ record.plugin_root,
9316
+ record.path,
9317
+ record.source?.path
9318
+ ];
9319
+ for (const candidate of candidates) {
9320
+ if (typeof candidate !== "string") continue;
9321
+ if (candidate.startsWith("file:")) {
9322
+ try {
9323
+ return fileURLToPath4(candidate);
9324
+ } catch {
9325
+ continue;
9326
+ }
9327
+ }
9328
+ if (path7.isAbsolute(candidate)) return candidate;
9329
+ }
9330
+ return null;
9331
+ }
9332
+ function sameResolvedPath(left, right) {
9333
+ try {
9334
+ return realpathSync(left) === realpathSync(right);
9335
+ } catch {
9336
+ return path7.resolve(left) === path7.resolve(right);
9337
+ }
9338
+ }
9339
+ function resolveHookHomeDir(env = process.env, fallback = os3.homedir()) {
8991
9340
  return env.HOME || fallback;
8992
9341
  }
8993
9342
  function resolveCopilotHookDir(env = process.env, homeDir = resolveHookHomeDir(env)) {
8994
- return path6.join(env.COPILOT_HOME || path6.join(homeDir, ".copilot"), "hooks");
9343
+ return path7.join(env.COPILOT_HOME || path7.join(homeDir, ".copilot"), "hooks");
8995
9344
  }
8996
9345
  function createCopilotCliAmbientContextScript(command = "lavish-axi") {
8997
9346
  return [
@@ -9040,16 +9389,16 @@ function installCopilotCliSessionStartHook({
9040
9389
  timeoutSec = 10,
9041
9390
  onError = void 0
9042
9391
  } = {}) {
9043
- const target = path6.join(hookDir, "lavish-axi.json");
9392
+ const target = path7.join(hookDir, "lavish-axi.json");
9044
9393
  try {
9045
- mkdirSync(path6.dirname(target), { recursive: true });
9046
- const current = existsSync2(target) ? JSON.parse(readFileSync(target, "utf8")) : {};
9394
+ mkdirSync2(path7.dirname(target), { recursive: true });
9395
+ const current = existsSync3(target) ? JSON.parse(readFileSync2(target, "utf8")) : {};
9047
9396
  const [updated, changed] = computeCopilotCliHookUpdate(
9048
9397
  current,
9049
9398
  createCopilotCliSessionStartHook(command, timeoutSec)
9050
9399
  );
9051
9400
  if (changed) {
9052
- writeFileSync(target, `${JSON.stringify(updated, null, 2)}
9401
+ writeFileSync2(target, `${JSON.stringify(updated, null, 2)}
9053
9402
  `, "utf8");
9054
9403
  }
9055
9404
  } catch (error) {
@@ -9139,11 +9488,11 @@ function shouldRestartServer(currentVersion, healthBody, forceRestart = false) {
9139
9488
  return healthBody.version !== currentVersion;
9140
9489
  }
9141
9490
  function shouldForceRestartForLocalBuild(executablePath, sourceServerExists = localSourceServerExists()) {
9142
- const localBuildEntry = fileURLToPath3(new URL("../dist/cli.mjs", import.meta.url));
9143
- return sourceServerExists && path6.resolve(executablePath) === path6.resolve(localBuildEntry);
9491
+ const localBuildEntry = fileURLToPath4(new URL("../dist/cli.mjs", import.meta.url));
9492
+ return sourceServerExists && path7.resolve(executablePath) === path7.resolve(localBuildEntry);
9144
9493
  }
9145
9494
  function localSourceServerExists() {
9146
- return existsSync2(fileURLToPath3(new URL("../src/server.js", import.meta.url)));
9495
+ return existsSync3(fileURLToPath4(new URL("../src/server.js", import.meta.url)));
9147
9496
  }
9148
9497
  function shouldKillProcessOnPort(currentVersion, healthBody) {
9149
9498
  if (!healthBody || typeof healthBody !== "object") return false;
@@ -9229,9 +9578,9 @@ async function startServer(port) {
9229
9578
  }
9230
9579
  }
9231
9580
  function resolveServerEntry() {
9232
- const binEntry = fileURLToPath3(new URL("../bin/lavish-axi.js", import.meta.url));
9233
- if (existsSync2(binEntry)) return binEntry;
9234
- return fileURLToPath3(import.meta.url);
9581
+ const binEntry = fileURLToPath4(new URL("../bin/lavish-axi.js", import.meta.url));
9582
+ if (existsSync3(binEntry)) return binEntry;
9583
+ return fileURLToPath4(import.meta.url);
9235
9584
  }
9236
9585
  function createServerSpawnOptions(logFd = null) {
9237
9586
  const stdio = (
@@ -9353,6 +9702,7 @@ Usage:
9353
9702
  lavish-axi playbook [playbook_id]
9354
9703
  lavish-axi design
9355
9704
  lavish-axi setup hooks
9705
+ lavish-axi setup plugin
9356
9706
 
9357
9707
  ${DESIGN_SYSTEM_HINT}
9358
9708
 
@@ -9402,8 +9752,13 @@ Examples:
9402
9752
  Show a copy-pasteable CDN snippet for Tailwind CSS browser runtime v4 + DaisyUI v5 + themes, Mermaid diagram tooling, a content-to-playbook router, an optional layout safety CSS snippet, plus technical reference for DaisyUI components. ${PLAYBOOK_ROUTER_HELP} Lavish artifacts stay portable HTML. This CDN snippet is the design fallback, not the default: inspect the subject project before falling back, and paste the layout safety CSS only when useful for dense nested grid/flex layouts, badges, wide fonts, or local media. ${DESIGN_PRIORITY_RULE}
9403
9753
  `,
9404
9754
  setup: `Usage: lavish-axi setup hooks
9755
+ lavish-axi setup plugin
9756
+
9757
+ hooks: install or repair agent SessionStart hooks for lavish-axi ambient context in Claude Code, Codex, OpenCode, and GitHub Copilot CLI. Restart your agent session afterward to receive the context. This is the primary integration - it carries live session state.
9758
+
9759
+ plugin: register the installed lavish-axi package as an Agent Plugin (agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI. The installed package directory is itself the plugin root, so nothing is downloaded and no marketplace is involved. Reload each client afterward. Codex users should use \`setup hooks\` instead.
9405
9760
 
9406
- Install or repair agent SessionStart hooks for lavish-axi ambient context in Claude Code, Codex, OpenCode, and GitHub Copilot CLI. Restart your agent session afterward to receive the context.
9761
+ Both actions are explicit opt-in, idempotent, and repair a stale path after a reinstall.
9407
9762
  `,
9408
9763
  server: `Usage: lavish-axi server [--port 4387] [--verbose]
9409
9764
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
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",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "plugin.json",
12
13
  "skills/lavish",
13
14
  "lavish-editor-marketing/renders/lavish-editor-marketing.gif",
14
15
  "LICENSE",
@@ -18,7 +19,8 @@
18
19
  "scripts": {
19
20
  "build": "node scripts/build.js",
20
21
  "build:skill": "node scripts/build-skill.js",
21
- "check": "npm run build && npm run lint && npm run format:check && npm run typecheck && npm test && node scripts/build-skill.js --check",
22
+ "build:plugin": "node scripts/build-plugin.js",
23
+ "check": "npm run build && npm run lint && npm run format:check && npm run typecheck && npm test && node scripts/build-skill.js --check && node scripts/build-plugin.js --check",
22
24
  "start": "node dist/cli.mjs",
23
25
  "lint": "eslint bin src test scripts",
24
26
  "format": "prettier --write .",
@@ -32,6 +34,7 @@
32
34
  "@tailwindcss/browser": "4.2.4",
33
35
  "axi-sdk-js": "^0.1.8",
34
36
  "chokidar": "^4.0.3",
37
+ "cross-spawn": "7.0.6",
35
38
  "daisyui": "^5.5.19",
36
39
  "express": "^5.2.1",
37
40
  "open": "^10.2.0",
package/plugin.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "lavish-axi",
4
+ "version": "0.1.47",
5
+ "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
+ "author": {
7
+ "name": "Kun Chen",
8
+ "url": "https://github.com/kunchenguid"
9
+ },
10
+ "homepage": "https://github.com/kunchenguid/lavish-axi#readme",
11
+ "repository": "https://github.com/kunchenguid/lavish-axi",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "axi",
15
+ "agent",
16
+ "cli",
17
+ "html",
18
+ "review",
19
+ "collaboration"
20
+ ]
21
+ }
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: lavish
3
3
  description: Turn complex or visual agent responses into rich, reviewable HTML artifacts the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.
4
- argument-hint: <what the artifact should show>
5
- author: Kun Chen (kunchenguid)
4
+ license: MIT
6
5
  metadata:
7
- hermes:
8
- tags: [html, review, artifacts, visualization]
9
- category: productivity
6
+ author: Kun Chen (kunchenguid)
7
+ argument-hint: <what the artifact should show>
8
+ hermes-tags: html, review, artifacts, visualization
9
+ hermes-category: productivity
10
10
  ---
11
11
 
12
12
  # Lavish Editor