@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
|
-
|
|
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`.
|
|
30
|
+
Opens at `http://localhost:7654`. No runtime dependencies — the binary includes everything.
|
|
31
31
|
|
|
32
32
|
<details>
|
|
33
|
-
<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
|

|
|
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
|
-
- **
|
|
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 {
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
47
|
+
function handleInvite(msg: ServerMessage) {
|
|
48
|
+
if (msg.type === "mesh:invite_code") {
|
|
49
|
+
setGenerating(false);
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function
|
|
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:
|
|
60
|
+
ws.subscribe("mesh:invite_code", handleInvite);
|
|
61
|
+
ws.subscribe("mesh:paired", handlePaired);
|
|
53
62
|
return function () {
|
|
54
|
-
ws.unsubscribe("mesh:
|
|
63
|
+
ws.unsubscribe("mesh:invite_code", handleInvite);
|
|
64
|
+
ws.unsubscribe("mesh:paired", handlePaired);
|
|
55
65
|
};
|
|
56
|
-
}, [
|
|
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'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.
|
|
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
|
-
|
|
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
|
-
|
|
78
|
+
sendTo(clientId, { type: "chat:error", message: "Invalid invite code format" });
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
64
81
|
if (!validatePairingToken(parsed.token)) {
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
}
|