@cryptiklemur/lattice 1.29.1 → 1.30.0
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.
|
@@ -22,6 +22,8 @@ export var PairingDialog = memo(function PairingDialog(props: PairingDialogProps
|
|
|
22
22
|
var [pairError, setPairError] = useState<string | null>(null);
|
|
23
23
|
var [copied, setCopied] = useState(false);
|
|
24
24
|
var [generating, setGenerating] = useState(false);
|
|
25
|
+
var [addresses, setAddresses] = useState<Array<{ name: string; address: string }>>([]);
|
|
26
|
+
var [selectedAddress, setSelectedAddress] = useState("");
|
|
25
27
|
var modalRef = useRef<HTMLDivElement>(null);
|
|
26
28
|
var inputRef = useRef<HTMLInputElement>(null);
|
|
27
29
|
|
|
@@ -33,14 +35,32 @@ export var PairingDialog = memo(function PairingDialog(props: PairingDialogProps
|
|
|
33
35
|
setPairError(null);
|
|
34
36
|
setCopied(false);
|
|
35
37
|
setGenerating(false);
|
|
38
|
+
setAddresses([]);
|
|
39
|
+
setSelectedAddress("");
|
|
36
40
|
setTab("generate");
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
43
|
+
|
|
44
|
+
function handleAddresses(msg: ServerMessage) {
|
|
45
|
+
if ((msg as any).type !== "mesh:addresses_result") return;
|
|
46
|
+
var data = msg as any as { addresses: Array<{ name: string; address: string }> };
|
|
47
|
+
setAddresses(data.addresses);
|
|
48
|
+
if (data.addresses.length > 0) {
|
|
49
|
+
setSelectedAddress(data.addresses[0].address);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ws.subscribe("mesh:addresses_result", handleAddresses);
|
|
54
|
+
ws.send({ type: "mesh:addresses" } as any);
|
|
55
|
+
|
|
39
56
|
function handleKeyDown(e: KeyboardEvent) {
|
|
40
57
|
if (e.key === "Escape") props.onClose();
|
|
41
58
|
}
|
|
42
59
|
document.addEventListener("keydown", handleKeyDown);
|
|
43
|
-
return function () {
|
|
60
|
+
return function () {
|
|
61
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
62
|
+
ws.unsubscribe("mesh:addresses_result", handleAddresses);
|
|
63
|
+
};
|
|
44
64
|
}, [props.isOpen]);
|
|
45
65
|
|
|
46
66
|
useEffect(function () {
|
|
@@ -77,7 +97,7 @@ export var PairingDialog = memo(function PairingDialog(props: PairingDialogProps
|
|
|
77
97
|
function handleGenerateInvite() {
|
|
78
98
|
clearInvite();
|
|
79
99
|
setGenerating(true);
|
|
80
|
-
|
|
100
|
+
ws.send({ type: "mesh:generate_invite", address: selectedAddress } as any);
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
function handlePair() {
|
|
@@ -172,9 +192,31 @@ export var PairingDialog = memo(function PairingDialog(props: PairingDialogProps
|
|
|
172
192
|
The code encodes this node's address and a one-time auth token.
|
|
173
193
|
</div>
|
|
174
194
|
|
|
195
|
+
{!mesh.inviteCode && !generating && addresses.length > 1 && (
|
|
196
|
+
<div className="mb-3">
|
|
197
|
+
<label className="text-[11px] font-mono text-base-content/35 uppercase tracking-wider mb-1.5 block">
|
|
198
|
+
Network interface
|
|
199
|
+
</label>
|
|
200
|
+
<select
|
|
201
|
+
value={selectedAddress}
|
|
202
|
+
onChange={function (e) { setSelectedAddress(e.target.value); }}
|
|
203
|
+
className="select select-bordered select-sm w-full bg-base-100 text-base-content text-[13px] font-mono"
|
|
204
|
+
>
|
|
205
|
+
{addresses.map(function (a) {
|
|
206
|
+
return (
|
|
207
|
+
<option key={a.address} value={a.address}>
|
|
208
|
+
{a.address} ({a.name})
|
|
209
|
+
</option>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
</select>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
175
216
|
{!mesh.inviteCode && !generating && (
|
|
176
217
|
<button
|
|
177
218
|
onClick={handleGenerateInvite}
|
|
219
|
+
disabled={!selectedAddress && addresses.length > 0}
|
|
178
220
|
className="btn btn-primary btn-sm"
|
|
179
221
|
>
|
|
180
222
|
Generate Invite Code
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
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,24 +4,30 @@ 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 { addPeer, removePeer, loadPeers } from "../mesh/peers";
|
|
8
|
+
import type { PeerInfo } from "@lattice/shared";
|
|
7
9
|
import { networkInterfaces } from "node:os";
|
|
8
10
|
|
|
9
11
|
function getLocalAddress(): string {
|
|
12
|
+
var all = getAllAddresses();
|
|
13
|
+
return all.length > 0 ? all[0].address : "localhost";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getAllAddresses(): Array<{ name: string; address: string }> {
|
|
10
17
|
var interfaces = networkInterfaces();
|
|
11
18
|
var keys = Object.keys(interfaces);
|
|
19
|
+
var results: Array<{ name: string; address: string }> = [];
|
|
12
20
|
for (var i = 0; i < keys.length; i++) {
|
|
13
21
|
var addrs = interfaces[keys[i]];
|
|
14
22
|
if (!addrs) continue;
|
|
15
23
|
for (var j = 0; j < addrs.length; j++) {
|
|
16
24
|
if (!addrs[j].internal && addrs[j].family === "IPv4") {
|
|
17
|
-
|
|
25
|
+
results.push({ name: keys[i], address: addrs[j].address });
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
}
|
|
21
|
-
return
|
|
29
|
+
return results;
|
|
22
30
|
}
|
|
23
|
-
import { addPeer, removePeer, loadPeers } from "../mesh/peers";
|
|
24
|
-
import type { PeerInfo } from "@lattice/shared";
|
|
25
31
|
|
|
26
32
|
export function buildNodesMessage(): NodeInfo[] {
|
|
27
33
|
var peers = loadPeers();
|
|
@@ -57,8 +63,9 @@ export function buildNodesMessage(): NodeInfo[] {
|
|
|
57
63
|
|
|
58
64
|
registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
59
65
|
if (message.type === "mesh:generate_invite") {
|
|
66
|
+
var genMsg = message as any as { type: "mesh:generate_invite"; address?: string };
|
|
60
67
|
var config = loadConfig();
|
|
61
|
-
var address = getLocalAddress();
|
|
68
|
+
var address = genMsg.address || getLocalAddress();
|
|
62
69
|
generateInviteCode(address, config.port).then(function (result) {
|
|
63
70
|
sendTo(clientId, {
|
|
64
71
|
type: "mesh:invite_code",
|
|
@@ -71,6 +78,12 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
if ((message as any).type === "mesh:addresses") {
|
|
82
|
+
var addresses = getAllAddresses();
|
|
83
|
+
sendTo(clientId, { type: "mesh:addresses_result" as any, addresses: addresses });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
if (message.type === "mesh:pair") {
|
|
75
88
|
var pairMsg = message as MeshPairMessage;
|
|
76
89
|
var parsed = parseInviteCode(pairMsg.code);
|
|
@@ -78,27 +91,38 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
78
91
|
sendTo(clientId, { type: "mesh:pair_failed", message: "Invalid invite code format" });
|
|
79
92
|
return;
|
|
80
93
|
}
|
|
81
|
-
if (!validatePairingToken(parsed.token)) {
|
|
82
|
-
sendTo(clientId, { type: "mesh:pair_failed", message: "Invite code is invalid or expired" });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
consumePairingToken(parsed.token);
|
|
86
94
|
|
|
87
95
|
var wsUrl = "ws://" + parsed.address + ":" + parsed.port + "/ws";
|
|
88
|
-
var
|
|
89
|
-
|
|
96
|
+
var pairWs = new WebSocket(wsUrl);
|
|
97
|
+
var pairTimeout = setTimeout(function () {
|
|
98
|
+
pairWs.close();
|
|
99
|
+
sendTo(clientId, { type: "mesh:pair_failed", message: "Connection timed out" });
|
|
100
|
+
}, 15000);
|
|
101
|
+
|
|
102
|
+
pairWs.addEventListener("open", function () {
|
|
90
103
|
var identity = loadOrCreateIdentity();
|
|
91
|
-
|
|
104
|
+
pairWs.send(JSON.stringify({
|
|
92
105
|
type: "mesh:hello",
|
|
93
106
|
nodeId: identity.id,
|
|
94
107
|
name: loadConfig().name,
|
|
108
|
+
token: parsed!.token,
|
|
95
109
|
projects: [],
|
|
96
110
|
}));
|
|
97
111
|
});
|
|
98
|
-
|
|
112
|
+
|
|
113
|
+
pairWs.addEventListener("message", function (event: MessageEvent) {
|
|
99
114
|
try {
|
|
100
|
-
var data = JSON.parse(event.data as string) as { type: string; nodeId?: string; name?: string };
|
|
115
|
+
var data = JSON.parse(event.data as string) as { type: string; nodeId?: string; name?: string; error?: string };
|
|
116
|
+
|
|
117
|
+
if (data.type === "mesh:hello_rejected") {
|
|
118
|
+
clearTimeout(pairTimeout);
|
|
119
|
+
pairWs.close();
|
|
120
|
+
sendTo(clientId, { type: "mesh:pair_failed", message: data.error ?? "Pairing rejected by remote node" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
if (data.type === "mesh:hello" && data.nodeId && data.name) {
|
|
125
|
+
clearTimeout(pairTimeout);
|
|
102
126
|
var peer: PeerInfo = {
|
|
103
127
|
id: data.nodeId,
|
|
104
128
|
name: data.name,
|
|
@@ -107,7 +131,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
107
131
|
pairedAt: Date.now(),
|
|
108
132
|
};
|
|
109
133
|
addPeer(peer);
|
|
110
|
-
|
|
134
|
+
pairWs.close();
|
|
111
135
|
|
|
112
136
|
var nodeInfo: NodeInfo = {
|
|
113
137
|
id: peer.id,
|
|
@@ -125,13 +149,44 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
125
149
|
console.error("[lattice] mesh:pair — invalid handshake response");
|
|
126
150
|
}
|
|
127
151
|
});
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
|
|
153
|
+
pairWs.addEventListener("error", function () {
|
|
154
|
+
clearTimeout(pairTimeout);
|
|
130
155
|
sendTo(clientId, { type: "mesh:pair_failed", message: "Failed to connect to " + parsed!.address + ":" + parsed!.port });
|
|
131
156
|
});
|
|
132
157
|
return;
|
|
133
158
|
}
|
|
134
159
|
|
|
160
|
+
if ((message as any).type === "mesh:hello") {
|
|
161
|
+
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; token?: string; projects: Array<{ slug: string; title: string }> };
|
|
162
|
+
|
|
163
|
+
if (!hello.token || !validatePairingToken(hello.token)) {
|
|
164
|
+
sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Invalid or expired invite code" });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
consumePairingToken(hello.token);
|
|
168
|
+
|
|
169
|
+
var peer: PeerInfo = {
|
|
170
|
+
id: hello.nodeId,
|
|
171
|
+
name: hello.name,
|
|
172
|
+
addresses: [],
|
|
173
|
+
publicKey: "",
|
|
174
|
+
pairedAt: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
addPeer(peer);
|
|
177
|
+
|
|
178
|
+
var identity = loadOrCreateIdentity();
|
|
179
|
+
sendTo(clientId, {
|
|
180
|
+
type: "mesh:hello" as any,
|
|
181
|
+
nodeId: identity.id,
|
|
182
|
+
name: loadConfig().name,
|
|
183
|
+
projects: [],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
135
190
|
if (message.type === "mesh:unpair") {
|
|
136
191
|
var unpairMsg = message as MeshUnpairMessage;
|
|
137
192
|
var removed = removePeer(unpairMsg.nodeId);
|