@cryptiklemur/lattice 1.34.0 → 1.36.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.
@@ -111,7 +111,7 @@ function ProjectButton(props: ProjectButtonProps) {
111
111
 
112
112
  {hovered && (
113
113
  <div
114
- className="pointer-events-none z-[9000] bg-base-300 border border-base-content/20 rounded px-2 py-1 text-xs text-base-content whitespace-nowrap shadow-xl"
114
+ className="pointer-events-none z-[9000] bg-base-300 border border-base-content/20 rounded-lg px-2.5 py-1.5 shadow-xl"
115
115
  style={{
116
116
  position: "fixed",
117
117
  left: "calc(64px + 8px)",
@@ -119,7 +119,18 @@ function ProjectButton(props: ProjectButtonProps) {
119
119
  transform: "translateY(-50%)",
120
120
  }}
121
121
  >
122
- {props.group.title}
122
+ <div className="text-[12px] font-bold text-base-content whitespace-nowrap">{props.group.title}</div>
123
+ {props.group.nodes.map(function (n) {
124
+ return (
125
+ <div key={n.nodeId} className="flex items-center gap-1.5 mt-0.5">
126
+ <div className={"w-[6px] h-[6px] rounded-full flex-shrink-0 " + (n.online ? "bg-success" : "bg-error")} />
127
+ <span className="text-[10px] text-base-content/50 whitespace-nowrap">
128
+ {n.nodeName}
129
+ {n.path ? " \u00B7 " + n.path : ""}
130
+ </span>
131
+ </div>
132
+ );
133
+ })}
123
134
  </div>
124
135
  )}
125
136
  </div>
@@ -331,6 +331,11 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
331
331
  >
332
332
  <span className="text-[13px] font-mono font-bold text-base-content/90">
333
333
  {activeProject?.title ?? "No Project"}
334
+ {activeProject?.isRemote && (
335
+ <span className="ml-1.5 text-[10px] font-normal text-base-content/30">
336
+ on {activeProject.nodeName}
337
+ </span>
338
+ )}
334
339
  </span>
335
340
  <ChevronDown size={14} className="text-base-content/30" />
336
341
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.34.0",
3
+ "version": "1.36.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>",
@@ -8,7 +8,7 @@ import { addClient, removeClient, routeMessage } from "./ws/server";
8
8
  import { broadcast, sendTo } from "./ws/broadcast";
9
9
  import { buildNodesMessage } from "./handlers/mesh";
10
10
  import { startDiscovery } from "./mesh/discovery";
11
- import { startMeshConnections, onPeerConnected, onPeerDisconnected, onPeerMessage } from "./mesh/connector";
11
+ import { startMeshConnections, onPeerConnected, onPeerDisconnected, onPeerMessage, getAllRemoteProjects } from "./mesh/connector";
12
12
  import { handleProxyRequest, handleProxyResponse } from "./mesh/proxy";
13
13
  import { verifyPassphrase, generateSessionToken, addSession, isValidSession } from "./auth/passphrase";
14
14
  import { ensureCerts } from "./tls";
@@ -395,11 +395,13 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
395
395
  var currentConfig = loadConfig();
396
396
  var currentIdentity = loadOrCreateIdentity();
397
397
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
398
+ var localProjects = currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
399
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
400
+ });
401
+ var remoteProjects = getAllRemoteProjects(currentIdentity.id);
398
402
  broadcast({
399
403
  type: "projects:list",
400
- projects: currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
401
- return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
402
- }),
404
+ projects: localProjects.concat(remoteProjects as typeof localProjects),
403
405
  });
404
406
  var updateInfo = getCachedUpdateInfo();
405
407
  if (updateInfo && updateInfo.updateAvailable) {
@@ -5,9 +5,11 @@ import { loadConfig } from "../config";
5
5
  import { loadOrCreateIdentity } from "../identity";
6
6
  import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
7
7
  import { addPeer, removePeer, loadPeers, getPeer } from "../mesh/peers";
8
- import { getConnectedPeerIds, connectToPeer, reconnectPeer, getPeerConnection, disconnectPeer } from "../mesh/connector";
8
+ import { getConnectedPeerIds, connectToPeer, reconnectPeer, getPeerConnection, disconnectPeer, getConnectedPeerProjects } from "../mesh/connector";
9
9
  import type { PeerInfo } from "@lattice/shared";
10
10
  import { networkInterfaces } from "node:os";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { execSync } from "node:child_process";
11
13
 
12
14
  function getLocalAddress(): string {
13
15
  var all = getAllAddresses();
@@ -27,9 +29,58 @@ function getAllAddresses(): Array<{ name: string; address: string }> {
27
29
  }
28
30
  }
29
31
  }
32
+
33
+ if (isWSL()) {
34
+ var windowsAddrs = getWindowsHostAddresses();
35
+ for (var w = 0; w < windowsAddrs.length; w++) {
36
+ var exists = results.some(function (r) { return r.address === windowsAddrs[w].address; });
37
+ if (!exists) {
38
+ results.push(windowsAddrs[w]);
39
+ }
40
+ }
41
+ }
42
+
43
+ return results;
44
+ }
45
+
46
+ function isWSL(): boolean {
47
+ try {
48
+ if (existsSync("/proc/version")) {
49
+ var version = readFileSync("/proc/version", "utf-8");
50
+ return version.toLowerCase().includes("microsoft");
51
+ }
52
+ } catch {}
53
+ return false;
54
+ }
55
+
56
+ function getWindowsHostAddresses(): Array<{ name: string; address: string }> {
57
+ var results: Array<{ name: string; address: string }> = [];
58
+ try {
59
+ var output = execSync(
60
+ "powershell.exe -NoProfile -Command \"Get-NetIPAddress -AddressFamily IPv4 | ForEach-Object { \\$_.IPAddress + '|' + \\$_.InterfaceAlias }\"",
61
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
62
+ );
63
+ var lines = output.trim().split(/\r?\n/);
64
+ for (var i = 0; i < lines.length; i++) {
65
+ var parts = lines[i].trim().split("|");
66
+ var ip = parts[0];
67
+ var iface = parts[1] || "windows";
68
+ if (!ip || ip === "127.0.0.1") continue;
69
+ if (ip.startsWith("169.254.")) continue;
70
+ if (iface.includes("WSL")) continue;
71
+ results.push({ name: iface.toLowerCase().replace(/\s+/g, "-"), address: ip });
72
+ }
73
+ } catch {}
30
74
  return results;
31
75
  }
32
76
 
77
+ function getLocalProjectsList(): Array<{ slug: string; title: string }> {
78
+ var config = loadConfig();
79
+ return config.projects.map(function (p: typeof config.projects[number]) {
80
+ return { slug: p.slug, title: p.title };
81
+ });
82
+ }
83
+
33
84
  export function buildNodesMessage(): NodeInfo[] {
34
85
  var peers = loadPeers();
35
86
  var config = loadConfig();
@@ -51,6 +102,7 @@ export function buildNodesMessage(): NodeInfo[] {
51
102
  };
52
103
 
53
104
  var remotes: NodeInfo[] = peers.map(function (peer) {
105
+ var peerProjects = getConnectedPeerProjects(peer.id);
54
106
  return {
55
107
  id: peer.id,
56
108
  name: peer.name,
@@ -59,7 +111,9 @@ export function buildNodesMessage(): NodeInfo[] {
59
111
  port: 0,
60
112
  online: connectedIds.has(peer.id),
61
113
  isLocal: false,
62
- projects: [],
114
+ projects: peerProjects.map(function (p) {
115
+ return { slug: p.slug, path: "", title: p.title, nodeId: peer.id };
116
+ }),
63
117
  };
64
118
  });
65
119
 
@@ -115,7 +169,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
115
169
  token: parsed!.token,
116
170
  port: pairConfig.port,
117
171
  addresses: getAllAddresses().map(function (a) { return a.address + ":" + pairConfig.port; }),
118
- projects: [],
172
+ projects: getLocalProjectsList(),
119
173
  }));
120
174
  });
121
175
 
@@ -145,6 +199,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
145
199
 
146
200
  connectToPeer(peer.id, peerAddr);
147
201
 
202
+ var remoteProjectsList = (data as any).projects ?? [];
148
203
  var nodeInfo: NodeInfo = {
149
204
  id: peer.id,
150
205
  name: peer.name,
@@ -153,7 +208,9 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
153
208
  port: parsed!.port,
154
209
  online: true,
155
210
  isLocal: false,
156
- projects: [],
211
+ projects: remoteProjectsList.map(function (rp: { slug: string; title: string }) {
212
+ return { slug: rp.slug, path: "", title: rp.title, nodeId: peer.id };
213
+ }),
157
214
  };
158
215
  sendTo(clientId, { type: "mesh:paired", node: nodeInfo });
159
216
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
@@ -186,7 +243,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
186
243
  nodeId: identity.id,
187
244
  name: loadConfig().name,
188
245
  publicKey: identity.publicKey,
189
- projects: [],
246
+ projects: getLocalProjectsList(),
190
247
  });
191
248
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
192
249
  return;
@@ -295,6 +295,33 @@ export function onPeerMessage(callback: (nodeId: string, msg: MeshMessage) => vo
295
295
  messageCallbacks.push(callback);
296
296
  }
297
297
 
298
+ export function getConnectedPeerProjects(nodeId: string): Array<{ slug: string; title: string }> {
299
+ var conn = connections.get(nodeId);
300
+ if (!conn || conn.ws.readyState !== WebSocket.OPEN) return [];
301
+ return conn.projects;
302
+ }
303
+
304
+ export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> {
305
+ var results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> = [];
306
+ for (var [nodeId, conn] of connections) {
307
+ if (conn.ws.readyState !== WebSocket.OPEN) continue;
308
+ var peers = require("./peers") as typeof import("./peers");
309
+ var peer = peers.getPeer(nodeId);
310
+ var peerName = peer ? peer.name : nodeId;
311
+ for (var i = 0; i < conn.projects.length; i++) {
312
+ results.push({
313
+ slug: conn.projects[i].slug,
314
+ path: "",
315
+ title: conn.projects[i].title,
316
+ nodeId: nodeId,
317
+ nodeName: peerName,
318
+ isRemote: true,
319
+ });
320
+ }
321
+ }
322
+ return results;
323
+ }
324
+
298
325
  export function findNodeForProject(projectSlug: string): string | undefined {
299
326
  for (var [nodeId, conn] of connections) {
300
327
  if (conn.ws.readyState !== WebSocket.OPEN) {