@cryptiklemur/lattice 1.28.0 → 1.28.2

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
@@ -23,20 +23,32 @@
23
23
  ## Quick Start
24
24
 
25
25
  ```bash
26
- npm install -g @cryptiklemur/lattice
26
+ curl -fsSL https://raw.githubusercontent.com/cryptiklemur/lattice/main/install.sh | bash
27
27
  lattice
28
28
  ```
29
29
 
30
- Opens at `http://localhost:7654`. Add projects through the UI or point it at a directory with a `CLAUDE.md` file.
30
+ Opens at `http://localhost:7654`. No runtime dependencies the binary includes everything.
31
31
 
32
32
  <details>
33
- <summary>Install with Bun</summary>
33
+ <summary>Other install methods</summary>
34
+
35
+ **Custom install directory:**
36
+
37
+ ```bash
38
+ LATTICE_INSTALL_DIR=~/.local/bin curl -fsSL https://raw.githubusercontent.com/cryptiklemur/lattice/main/install.sh | bash
39
+ ```
40
+
41
+ **Via npm** (requires [Bun](https://bun.sh)):
34
42
 
35
43
  ```bash
36
44
  bun install -g @cryptiklemur/lattice
37
45
  lattice
38
46
  ```
39
47
 
48
+ **Manual download:**
49
+
50
+ Download the binary for your platform from [GitHub Releases](https://github.com/cryptiklemur/lattice/releases), `chmod +x`, and run it.
51
+
40
52
  </details>
41
53
 
42
54
  <details>
@@ -53,6 +65,17 @@ Hot-reloads both server and client automatically.
53
65
 
54
66
  </details>
55
67
 
68
+ <details>
69
+ <summary>Updating</summary>
70
+
71
+ ```bash
72
+ lattice update
73
+ ```
74
+
75
+ The server also checks for updates automatically and shows a banner in the UI when a new version is available.
76
+
77
+ </details>
78
+
56
79
  ---
57
80
 
58
81
  ## Features
@@ -83,12 +106,17 @@ Press `?` for keyboard shortcuts, `Ctrl+K` for the command palette.
83
106
 
84
107
  ![Settings](docs/screenshots/settings.png)
85
108
 
109
+ ### Plugin Management
110
+
111
+ Install, update, and remove Claude Code plugins from the UI. Browse all plugins across registered marketplaces sorted by popularity, view details (skills, hooks, rules, author info), and enable/disable plugins per project.
112
+
86
113
  ### Infrastructure
87
114
 
88
115
  - **Mesh networking** — Connect multiple machines with automatic discovery and session proxying
89
116
  - **MCP servers** — Add, edit, and remove at global or project level
90
- - **Skill marketplace** — Search and install from [skills.sh](https://skills.sh)
117
+ - **Plugins & skills** — Browse marketplaces, install plugins, manage per-project
91
118
  - **Memory management** — View and edit Claude's project memories
119
+ - **Self-updating** — Automatic update checks with in-app banner and `lattice update` CLI
92
120
 
93
121
  ### Mobile
94
122
 
@@ -100,7 +128,7 @@ Responsive design with touch targets, swipe-to-open sidebar, and optimized layou
100
128
 
101
129
  ## Architecture
102
130
 
103
- Bun monorepo with three packages:
131
+ Bun monorepo with three packages, compiled into a standalone binary via `bun build --compile`:
104
132
 
105
133
  | Package | Stack |
106
134
  |---------|-------|
@@ -108,6 +136,8 @@ Bun monorepo with three packages:
108
136
  | `server/` | Bun WebSocket server, analytics engine, mesh networking |
109
137
  | `client/` | React 19, Vite, Tailwind, daisyUI, 23 themes |
110
138
 
139
+ The client is built by Vite, then embedded into the server binary as base64-encoded assets. The result is a single executable with zero runtime dependencies.
140
+
111
141
  Communication via typed WebSocket messages. Sessions managed through the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk). Client state via Tanstack Store + Router.
112
142
 
113
143
  ### Security
@@ -1,6 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { useFocusTrap } from "../../hooks/useFocusTrap";
3
- import { X, Copy, Check } from "lucide-react";
2
+ import { X, Copy, Check, Loader2 } from "lucide-react";
4
3
  import { useWebSocket } from "../../hooks/useWebSocket";
5
4
  import { useMesh } from "../../hooks/useMesh";
6
5
  import { clearInvite } from "../../stores/mesh";
@@ -22,9 +21,9 @@ export function PairingDialog(props: PairingDialogProps) {
22
21
  var [pairStatus, setPairStatus] = useState<PairStatus>("idle");
23
22
  var [pairError, setPairError] = useState<string | null>(null);
24
23
  var [copied, setCopied] = useState(false);
24
+ var [generating, setGenerating] = useState(false);
25
25
  var modalRef = useRef<HTMLDivElement>(null);
26
- var stableOnClose = useCallback(function () { props.onClose(); }, [props.onClose]);
27
- useFocusTrap(modalRef, stableOnClose, props.isOpen);
26
+ var inputRef = useRef<HTMLInputElement>(null);
28
27
 
29
28
  useEffect(function () {
30
29
  if (!props.isOpen) {
@@ -33,30 +32,42 @@ export function PairingDialog(props: PairingDialogProps) {
33
32
  setPairStatus("idle");
34
33
  setPairError(null);
35
34
  setCopied(false);
35
+ setGenerating(false);
36
36
  setTab("generate");
37
+ return;
38
+ }
39
+ function handleKeyDown(e: KeyboardEvent) {
40
+ if (e.key === "Escape") props.onClose();
37
41
  }
42
+ document.addEventListener("keydown", handleKeyDown);
43
+ return function () { document.removeEventListener("keydown", handleKeyDown); };
38
44
  }, [props.isOpen]);
39
45
 
40
46
  useEffect(function () {
41
- if (pairStatus !== "connecting") {
42
- return;
47
+ function handleInvite(msg: ServerMessage) {
48
+ if (msg.type === "mesh:invite_code") {
49
+ setGenerating(false);
50
+ }
43
51
  }
44
52
 
45
- function handler(msg: ServerMessage) {
53
+ function handlePaired(msg: ServerMessage) {
46
54
  if (msg.type === "mesh:paired") {
47
55
  setPairStatus("paired");
48
56
  setPairError(null);
49
57
  }
50
58
  }
51
59
 
52
- ws.subscribe("mesh:paired", handler);
60
+ ws.subscribe("mesh:invite_code", handleInvite);
61
+ ws.subscribe("mesh:paired", handlePaired);
53
62
  return function () {
54
- ws.unsubscribe("mesh:paired", handler);
63
+ ws.unsubscribe("mesh:invite_code", handleInvite);
64
+ ws.unsubscribe("mesh:paired", handlePaired);
55
65
  };
56
- }, [ws, pairStatus]);
66
+ }, []);
57
67
 
58
68
  function handleGenerateInvite() {
59
69
  clearInvite();
70
+ setGenerating(true);
60
71
  mesh.generateInvite();
61
72
  }
62
73
 
@@ -152,7 +163,7 @@ export function PairingDialog(props: PairingDialogProps) {
152
163
  The code encodes this node&apos;s address and a one-time auth token.
153
164
  </div>
154
165
 
155
- {!mesh.inviteCode && (
166
+ {!mesh.inviteCode && !generating && (
156
167
  <button
157
168
  onClick={handleGenerateInvite}
158
169
  className="btn btn-primary btn-sm"
@@ -161,6 +172,13 @@ export function PairingDialog(props: PairingDialogProps) {
161
172
  </button>
162
173
  )}
163
174
 
175
+ {generating && !mesh.inviteCode && (
176
+ <div className="flex items-center gap-2 text-[13px] text-base-content/40">
177
+ <Loader2 size={14} className="animate-spin text-primary" />
178
+ Generating invite code...
179
+ </div>
180
+ )}
181
+
164
182
  {mesh.inviteCode && (
165
183
  <div>
166
184
  <div className="flex items-center gap-2 px-3.5 py-2.5 rounded bg-base-100 border border-base-300 mb-4">
@@ -209,6 +227,7 @@ export function PairingDialog(props: PairingDialogProps) {
209
227
  </div>
210
228
 
211
229
  <input
230
+ ref={inputRef}
212
231
  type="text"
213
232
  value={pairCode}
214
233
  onChange={function (e) {
@@ -224,6 +243,7 @@ export function PairingDialog(props: PairingDialogProps) {
224
243
  }
225
244
  }}
226
245
  placeholder="LTCE-XXXX-XXXX"
246
+ autoFocus
227
247
  disabled={pairStatus === "connecting" || pairStatus === "paired"}
228
248
  className="input input-bordered w-full bg-base-100 text-base-content font-mono text-[14px] tracking-[0.06em] mb-3 focus:border-primary"
229
249
  />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.28.0",
3
+ "version": "1.28.2",
4
4
  "description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
5
5
  "license": "MIT",
6
6
  "author": "Aaron Scherer <me@aaronscherer.me>",
@@ -4,6 +4,22 @@ import { sendTo, broadcast } from "../ws/broadcast";
4
4
  import { loadConfig } from "../config";
5
5
  import { loadOrCreateIdentity } from "../identity";
6
6
  import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
7
+ import { networkInterfaces } from "node:os";
8
+
9
+ function getLocalAddress(): string {
10
+ var interfaces = networkInterfaces();
11
+ var keys = Object.keys(interfaces);
12
+ for (var i = 0; i < keys.length; i++) {
13
+ var addrs = interfaces[keys[i]];
14
+ if (!addrs) continue;
15
+ for (var j = 0; j < addrs.length; j++) {
16
+ if (!addrs[j].internal && addrs[j].family === "IPv4") {
17
+ return addrs[j].address;
18
+ }
19
+ }
20
+ }
21
+ return "localhost";
22
+ }
7
23
  import { addPeer, removePeer, loadPeers } from "../mesh/peers";
8
24
  import type { PeerInfo } from "@lattice/shared";
9
25
 
@@ -42,7 +58,8 @@ export function buildNodesMessage(): NodeInfo[] {
42
58
  registerHandler("mesh", function (clientId: string, message: ClientMessage) {
43
59
  if (message.type === "mesh:generate_invite") {
44
60
  var config = loadConfig();
45
- generateInviteCode("localhost", config.port).then(function (result) {
61
+ var address = getLocalAddress();
62
+ generateInviteCode(address, config.port).then(function (result) {
46
63
  sendTo(clientId, {
47
64
  type: "mesh:invite_code",
48
65
  code: result.code,
@@ -58,11 +75,11 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
58
75
  var pairMsg = message as MeshPairMessage;
59
76
  var parsed = parseInviteCode(pairMsg.code);
60
77
  if (!parsed) {
61
- console.warn("[lattice] mesh:pair invalid invite code");
78
+ sendTo(clientId, { type: "chat:error", message: "Invalid invite code format" });
62
79
  return;
63
80
  }
64
81
  if (!validatePairingToken(parsed.token)) {
65
- console.warn("[lattice] mesh:pair invalid or expired token");
82
+ sendTo(clientId, { type: "chat:error", message: "Invite code is invalid or expired" });
66
83
  return;
67
84
  }
68
85
  consumePairingToken(parsed.token);
@@ -110,6 +127,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
110
127
  });
111
128
  ws.addEventListener("error", function () {
112
129
  console.error("[lattice] mesh:pair — failed to connect to", wsUrl);
130
+ sendTo(clientId, { type: "chat:error", message: "Failed to connect to " + parsed!.address + ":" + parsed!.port });
113
131
  });
114
132
  return;
115
133
  }
@@ -1,6 +1,8 @@
1
- import Bonjour from "bonjour-service";
1
+ import BonjourImport from "bonjour-service";
2
2
  import type { Service, Browser } from "bonjour-service";
3
3
 
4
+ var Bonjour = (typeof BonjourImport === "function" ? BonjourImport : (BonjourImport as any).default) as typeof BonjourImport;
5
+
4
6
  export interface DiscoveredNode {
5
7
  nodeId: string;
6
8
  name: string;
@@ -60,7 +60,8 @@ export async function generateInviteCode(
60
60
 
61
61
  pendingTokens.set(token, Date.now());
62
62
 
63
- var qrDataUrl = await QRCode.toString(code, { type: "svg" });
63
+ var qrSvg = await QRCode.toString(code, { type: "svg" });
64
+ var qrDataUrl = "data:image/svg+xml;base64," + Buffer.from(qrSvg).toString("base64");
64
65
 
65
66
  return { code, token, qrDataUrl };
66
67
  }