@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
|
|
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.
|
|
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>",
|
package/server/src/daemon.ts
CHANGED
|
@@ -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:
|
|
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) {
|