@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 () { document.removeEventListener("keydown", handleKeyDown); };
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
- mesh.generateInvite();
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&apos;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.29.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
- return addrs[j].address;
25
+ results.push({ name: keys[i], address: addrs[j].address });
18
26
  }
19
27
  }
20
28
  }
21
- return "localhost";
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 ws = new WebSocket(wsUrl);
89
- ws.addEventListener("open", function () {
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
- ws.send(JSON.stringify({
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
- ws.addEventListener("message", function (event: MessageEvent) {
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
- ws.close();
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
- ws.addEventListener("error", function () {
129
- console.error("[lattice] mesh:pair — failed to connect to", wsUrl);
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);