@cryptiklemur/lattice 1.28.1 → 1.28.3

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.
@@ -1,6 +1,5 @@
1
- import { useState, useEffect, useCallback, useRef } from "react";
2
- import { useFocusTrap } from "../../hooks/useFocusTrap";
3
- import { X, Copy, Check } from "lucide-react";
1
+ import { useState, useEffect, useCallback, useRef, memo } from "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";
@@ -14,7 +13,7 @@ interface PairingDialogProps {
14
13
  onClose: () => void;
15
14
  }
16
15
 
17
- export function PairingDialog(props: PairingDialogProps) {
16
+ export var PairingDialog = memo(function PairingDialog(props: PairingDialogProps) {
18
17
  var ws = useWebSocket();
19
18
  var mesh = useMesh();
20
19
  var [tab, setTab] = useState<Tab>("generate");
@@ -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">
@@ -180,17 +198,6 @@ export function PairingDialog(props: PairingDialogProps) {
180
198
  </button>
181
199
  </div>
182
200
 
183
- {mesh.inviteQr && (
184
- <div className="flex justify-center mb-4">
185
- <img
186
- src={mesh.inviteQr}
187
- alt="QR code for invite"
188
- className="w-40 h-40 rounded border border-base-300"
189
- style={{ imageRendering: "pixelated" }}
190
- />
191
- </div>
192
- )}
193
-
194
201
  <button
195
202
  onClick={handleGenerateInvite}
196
203
  className="text-[12px] text-base-content/40 underline cursor-pointer"
@@ -209,6 +216,7 @@ export function PairingDialog(props: PairingDialogProps) {
209
216
  </div>
210
217
 
211
218
  <input
219
+ ref={inputRef}
212
220
  type="text"
213
221
  value={pairCode}
214
222
  onChange={function (e) {
@@ -224,6 +232,7 @@ export function PairingDialog(props: PairingDialogProps) {
224
232
  }
225
233
  }}
226
234
  placeholder="LTCE-XXXX-XXXX"
235
+ autoFocus
227
236
  disabled={pairStatus === "connecting" || pairStatus === "paired"}
228
237
  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
238
  />
@@ -267,4 +276,4 @@ export function PairingDialog(props: PairingDialogProps) {
267
276
  </div>
268
277
  </div>
269
278
  );
270
- }
279
+ });
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useState, useCallback, memo } from "react";
2
2
  import { Plus, CircleDot, Circle } from "lucide-react";
3
3
  import { useWebSocket } from "../../hooks/useWebSocket";
4
4
  import { useMesh } from "../../hooks/useMesh";
@@ -83,6 +83,8 @@ export function MeshStatus() {
83
83
  var { nodes } = useMesh();
84
84
  var [pairingOpen, setPairingOpen] = useState(false);
85
85
 
86
+ var handleClosePairing = useCallback(function () { setPairingOpen(false); }, []);
87
+
86
88
  function handleUnpair(nodeId: string) {
87
89
  ws.send({ type: "mesh:unpair", nodeId });
88
90
  }
@@ -138,7 +140,7 @@ export function MeshStatus() {
138
140
 
139
141
  <PairingDialog
140
142
  isOpen={pairingOpen}
141
- onClose={function () { setPairingOpen(false); }}
143
+ onClose={handleClosePairing}
142
144
  />
143
145
  </div>
144
146
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.28.1",
3
+ "version": "1.28.3",
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,7 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import QRCode from "qrcode";
3
2
 
4
3
  var BASE62_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz";
4
+ var CODE_LENGTH = 16;
5
5
 
6
6
  var PAIRING_TOKEN_TTL = 300000;
7
7
  var CLEANUP_INTERVAL = 60000;
@@ -16,7 +16,10 @@ function base62Encode(buf: Buffer): string {
16
16
  result = BASE62_CHARS[Number(n % base)] + result;
17
17
  n = n / base;
18
18
  }
19
- return result || BASE62_CHARS[0];
19
+ while (result.length < CODE_LENGTH) {
20
+ result = BASE62_CHARS[0] + result;
21
+ }
22
+ return result;
20
23
  }
21
24
 
22
25
  function base62Decode(s: string): Buffer {
@@ -36,11 +39,29 @@ function base62Decode(s: string): Buffer {
36
39
  return Buffer.from(hex, "hex");
37
40
  }
38
41
 
42
+ function packPayload(address: string, port: number, token: Buffer): Buffer {
43
+ var parts = address.split(".");
44
+ var buf = Buffer.alloc(4 + 2 + token.length);
45
+ for (var i = 0; i < 4; i++) {
46
+ buf[i] = parseInt(parts[i] || "0", 10);
47
+ }
48
+ buf.writeUInt16BE(port, 4);
49
+ token.copy(buf, 6);
50
+ return buf;
51
+ }
52
+
53
+ function unpackPayload(buf: Buffer): { address: string; port: number; token: string } | null {
54
+ if (buf.length < 8) return null;
55
+ var address = buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
56
+ var port = buf.readUInt16BE(4);
57
+ var token = buf.subarray(6).toString("hex");
58
+ return { address, port, token };
59
+ }
60
+
39
61
  function formatCode(raw: string): string {
40
- var upper = raw.toUpperCase();
41
62
  var chunks: string[] = [];
42
- for (var i = 0; i < upper.length; i += 4) {
43
- chunks.push(upper.slice(i, i + 4));
63
+ for (var i = 0; i < raw.length; i += 4) {
64
+ chunks.push(raw.slice(i, i + 4));
44
65
  }
45
66
  return "LTCE-" + chunks.join("-");
46
67
  }
@@ -53,16 +74,15 @@ export async function generateInviteCode(
53
74
  address: string,
54
75
  port: number
55
76
  ): Promise<{ code: string; token: string; qrDataUrl: string }> {
56
- var token = randomBytes(8).toString("hex");
57
- var payload = Buffer.from(address + ":" + port + ":" + token, "utf-8");
77
+ var tokenBuf = randomBytes(4);
78
+ var token = tokenBuf.toString("hex");
79
+ var payload = packPayload(address, port, tokenBuf);
58
80
  var encoded = base62Encode(payload);
59
81
  var code = formatCode(encoded);
60
82
 
61
83
  pendingTokens.set(token, Date.now());
62
84
 
63
- var qrDataUrl = await QRCode.toString(code, { type: "svg" });
64
-
65
- return { code, token, qrDataUrl };
85
+ return { code, token, qrDataUrl: "" };
66
86
  }
67
87
 
68
88
  export function parseInviteCode(
@@ -70,19 +90,8 @@ export function parseInviteCode(
70
90
  ): { address: string; port: number; token: string } | null {
71
91
  try {
72
92
  var stripped = stripCode(code);
73
- var decoded = base62Decode(stripped).toString("utf-8");
74
- var parts = decoded.split(":");
75
- if (parts.length < 3) {
76
- return null;
77
- }
78
- var token = parts[parts.length - 1];
79
- var portStr = parts[parts.length - 2];
80
- var address = parts.slice(0, parts.length - 2).join(":");
81
- var port = parseInt(portStr, 10);
82
- if (isNaN(port)) {
83
- return null;
84
- }
85
- return { address, port, token };
93
+ var decoded = base62Decode(stripped);
94
+ return unpackPayload(decoded);
86
95
  } catch {
87
96
  return null;
88
97
  }