@cryptiklemur/lattice 1.37.2 → 1.39.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.37.2",
3
+ "version": "1.39.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>",
@@ -1,4 +1,5 @@
1
1
  import type { ClientMessage, MeshPairMessage, MeshUnpairMessage, NodeInfo } from "@lattice/shared";
2
+ import { log } from "../logger";
2
3
  import { registerHandler } from "../ws/router";
3
4
  import { sendTo, broadcast } from "../ws/broadcast";
4
5
  import { loadConfig } from "../config";
@@ -122,6 +123,8 @@ export function buildNodesMessage(): NodeInfo[] {
122
123
  }
123
124
 
124
125
  registerHandler("mesh", function (clientId: string, message: ClientMessage) {
126
+ log.meshHello("mesh message: %s from %s", (message as any).type, clientId.slice(0, 8));
127
+
125
128
  if (message.type === "mesh:generate_invite") {
126
129
  var genMsg = message as any as { type: "mesh:generate_invite"; address?: string };
127
130
  var config = loadConfig();
@@ -232,14 +235,17 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
232
235
  var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; publicKey?: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
233
236
 
234
237
  var knownPeer = hello.nodeId ? getPeer(hello.nodeId) : undefined;
238
+ log.meshHello("mesh:hello from nodeId=%s name=%s known=%s", hello.nodeId?.slice(0, 8), hello.name, !!knownPeer);
235
239
 
236
240
  if (knownPeer) {
237
241
  if (knownPeer.publicKey && hello.publicKey && knownPeer.publicKey !== hello.publicKey) {
242
+ log.meshHello(" ✗ public key mismatch for %s", hello.name);
238
243
  sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Public key mismatch — possible impersonation" });
239
244
  return;
240
245
  }
241
246
 
242
247
  var inboundWs = getClientWebSocket(clientId);
248
+ log.meshHello(" registering inbound connection for %s (ws=%s)", hello.name, !!inboundWs);
243
249
  if (inboundWs) {
244
250
  registerInboundPeer(hello.nodeId, inboundWs as any);
245
251
  }
@@ -72,15 +72,35 @@ switch (command) {
72
72
  case "stop":
73
73
  runStop();
74
74
  break;
75
+ case "restart":
76
+ runRestart();
77
+ break;
75
78
  case "status":
76
79
  runStatus();
77
80
  break;
78
81
  case "update":
79
82
  await runUpdate();
80
83
  break;
84
+ case "version":
85
+ await runVersion();
86
+ break;
87
+ case "logs":
88
+ runLogs();
89
+ break;
90
+ case "open":
91
+ runOpen();
92
+ break;
93
+ case "config":
94
+ runConfigInfo();
95
+ break;
96
+ case "help":
97
+ case "--help":
98
+ case "-h":
99
+ runHelp();
100
+ break;
81
101
  default:
82
102
  console.log("[lattice] Unknown command: " + command);
83
- console.log("[lattice] Usage: lattice [start|stop|status|update|daemon]");
103
+ runHelp();
84
104
  process.exit(1);
85
105
  }
86
106
 
@@ -180,6 +200,142 @@ function runStop(): void {
180
200
  }
181
201
  }
182
202
 
203
+ function runHelp(): void {
204
+ console.log("");
205
+ console.log(" lattice — Multi-machine agentic dashboard for Claude Code");
206
+ console.log("");
207
+ console.log(" Usage: lattice [command] [options]");
208
+ console.log("");
209
+ console.log(" Commands:");
210
+ console.log(" start Start the daemon and open the UI (default)");
211
+ console.log(" stop Stop the running daemon");
212
+ console.log(" restart Stop and restart the daemon");
213
+ console.log(" status Show daemon status and connection info");
214
+ console.log(" update Check for updates and install the latest version");
215
+ console.log(" version Show current and latest version");
216
+ console.log(" logs Tail the daemon log");
217
+ console.log(" open Open the UI in the browser");
218
+ console.log(" config Show configuration paths and settings");
219
+ console.log(" help Show this help message");
220
+ console.log("");
221
+ console.log(" Options:");
222
+ console.log(" --port=N Override the server port");
223
+ console.log("");
224
+ console.log(" Environment:");
225
+ console.log(" LATTICE_HOME Data directory (default: ~/.lattice)");
226
+ console.log(" LATTICE_PORT Server port (default: 7654)");
227
+ console.log(" DEBUG Enable debug logging (e.g. DEBUG=lattice:*)");
228
+ console.log(" Scopes: server,ws,router,mesh,mesh:connect,mesh:hello,");
229
+ console.log(" mesh:proxy,broadcast,chat,session,plugins,update");
230
+ console.log("");
231
+ }
232
+
233
+ function runRestart(): void {
234
+ var pid = readPid();
235
+ if (pid !== null && isDaemonRunning(pid)) {
236
+ console.log("[lattice] Stopping daemon (PID " + pid + ")...");
237
+ try {
238
+ process.kill(pid, "SIGTERM");
239
+ } catch {}
240
+ removePid();
241
+
242
+ var waited = 0;
243
+ while (waited < 5000) {
244
+ try {
245
+ process.kill(pid, 0);
246
+ Bun.sleepSync(200);
247
+ waited += 200;
248
+ } catch {
249
+ break;
250
+ }
251
+ }
252
+ }
253
+
254
+ console.log("[lattice] Starting daemon...");
255
+ var logPath = join(getLatticeHome(), "daemon.log");
256
+
257
+ var spawnArgs = IS_COMPILED
258
+ ? [process.execPath, "daemon"]
259
+ : ["bun", import.meta.path, "daemon"];
260
+
261
+ if (portOverride) {
262
+ spawnArgs.push("--port", String(portOverride));
263
+ }
264
+
265
+ var child = Bun.spawn(spawnArgs, {
266
+ detached: true,
267
+ stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)],
268
+ });
269
+
270
+ child.unref();
271
+ writePid(child.pid);
272
+ console.log("[lattice] Daemon started (PID " + child.pid + ")");
273
+ }
274
+
275
+ async function runVersion(): Promise<void> {
276
+ var { checkForUpdate } = await import("./update-checker");
277
+ var info = await checkForUpdate(true);
278
+ console.log("[lattice] Current: v" + info.currentVersion);
279
+ if (info.latestVersion) {
280
+ if (info.updateAvailable) {
281
+ console.log("[lattice] Latest: v" + info.latestVersion + " (update available)");
282
+ console.log("[lattice] Run 'lattice update' to install");
283
+ } else {
284
+ console.log("[lattice] Latest: v" + info.latestVersion + " (up to date)");
285
+ }
286
+ }
287
+ console.log("[lattice] Mode: " + info.installMode);
288
+ }
289
+
290
+ function runLogs(): void {
291
+ var logPath = join(getLatticeHome(), "daemon.log");
292
+ if (!existsSync(logPath)) {
293
+ console.log("[lattice] No log file found at " + logPath);
294
+ process.exit(1);
295
+ }
296
+ console.log("[lattice] Tailing " + logPath + " (Ctrl+C to stop)");
297
+ var proc = Bun.spawn(["tail", "-f", "-n", "50", logPath], {
298
+ stdout: "inherit",
299
+ stderr: "inherit",
300
+ });
301
+ process.on("SIGINT", function () {
302
+ proc.kill();
303
+ process.exit(0);
304
+ });
305
+ }
306
+
307
+ function runOpen(): void {
308
+ var config = loadConfig();
309
+ var pid = readPid();
310
+ if (pid === null || !isDaemonRunning(pid)) {
311
+ console.log("[lattice] Daemon is not running. Start it with 'lattice start'");
312
+ process.exit(1);
313
+ }
314
+ var url = (config.tls ? "https" : "http") + "://localhost:" + config.port;
315
+ console.log("[lattice] Opening " + url);
316
+ openBrowser(url);
317
+ }
318
+
319
+ function runConfigInfo(): void {
320
+ var config = loadConfig();
321
+ var home = getLatticeHome();
322
+ console.log("[lattice] Home: " + home);
323
+ console.log("[lattice] Config: " + join(home, "config.json"));
324
+ console.log("[lattice] Port: " + config.port);
325
+ console.log("[lattice] Name: " + config.name);
326
+ console.log("[lattice] TLS: " + (config.tls ? "enabled" : "disabled"));
327
+ console.log("[lattice] Projects: " + config.projects.length);
328
+ for (var i = 0; i < config.projects.length; i++) {
329
+ console.log(" " + config.projects[i].slug + " → " + config.projects[i].path);
330
+ }
331
+ if (config.passphraseHash) {
332
+ console.log("[lattice] Passphrase: set");
333
+ }
334
+ if (config.costBudget) {
335
+ console.log("[lattice] Budget: $" + config.costBudget.dailyLimit + "/day (" + config.costBudget.enforcement + ")");
336
+ }
337
+ }
338
+
183
339
  function runStatus(): void {
184
340
  var pid = readPid();
185
341
  if (pid === null) {
@@ -6,7 +6,16 @@ export var log = {
6
6
  chat: createDebug("lattice:chat"),
7
7
  session: createDebug("lattice:session"),
8
8
  mesh: createDebug("lattice:mesh"),
9
+ meshConnect: createDebug("lattice:mesh:connect"),
10
+ meshHello: createDebug("lattice:mesh:hello"),
11
+ meshProxy: createDebug("lattice:mesh:proxy"),
12
+ router: createDebug("lattice:router"),
13
+ broadcast: createDebug("lattice:broadcast"),
9
14
  auth: createDebug("lattice:auth"),
10
15
  fs: createDebug("lattice:fs"),
11
16
  analytics: createDebug("lattice:analytics"),
17
+ plugins: createDebug("lattice:plugins"),
18
+ update: createDebug("lattice:update"),
19
+ terminal: createDebug("lattice:terminal"),
20
+ settings: createDebug("lattice:settings"),
12
21
  };
@@ -46,11 +46,18 @@ function reconcilePeers(): void {
46
46
  var peers = loadPeers();
47
47
  for (var i = 0; i < peers.length; i++) {
48
48
  var peer = peers[i];
49
- if (!peer.addresses || peer.addresses.length === 0) continue;
49
+ if (!peer.addresses || peer.addresses.length === 0) {
50
+ log.meshConnect("skip %s — no addresses", peer.name);
51
+ continue;
52
+ }
50
53
  var existing = connections.get(peer.id);
51
54
  if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) continue;
52
- if (existing && !existing.dead && existing.retryTimer !== null) continue;
55
+ if (existing && !existing.dead && existing.retryTimer !== null) {
56
+ log.meshConnect("skip %s — retry pending", peer.name);
57
+ continue;
58
+ }
53
59
  if (!existing || existing.dead) {
60
+ log.meshConnect("connecting to %s at %s", peer.name, peer.addresses[0]);
54
61
  connections.delete(peer.id);
55
62
  connectToPeer(peer.id, peer.addresses[0]);
56
63
  }
@@ -103,6 +110,7 @@ function openConnection(conn: PeerConnection, url: string): void {
103
110
  var circuit = circuitBreakers.get(conn.nodeId);
104
111
  if (circuit && circuit.failures >= CIRCUIT_BREAKER_THRESHOLD && !circuit.halfOpen) {
105
112
  if (Date.now() < circuit.openUntil) {
113
+ log.meshConnect("circuit breaker open for %s, retry in %dms", conn.nodeId.slice(0, 8), circuit.openUntil - Date.now());
106
114
  conn.retryTimer = setTimeout(function () {
107
115
  if (conn.dead) return;
108
116
  conn.retryTimer = null;
@@ -114,12 +122,13 @@ function openConnection(conn: PeerConnection, url: string): void {
114
122
  circuit.halfOpen = true;
115
123
  }
116
124
 
125
+ log.meshConnect("opening WebSocket to %s", url);
117
126
  var ws = new WebSocket(url);
118
127
  conn.ws = ws;
119
128
 
120
129
  var connectionTimer = setTimeout(function () {
121
130
  if (ws.readyState !== WebSocket.OPEN) {
122
- log.mesh("Connection timeout for peer: %s", conn.nodeId);
131
+ log.meshConnect("connection timeout for %s at %s", conn.nodeId.slice(0, 8), url);
123
132
  ws.close();
124
133
  }
125
134
  }, CONNECTION_TIMEOUT_MS);
@@ -242,8 +251,10 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
242
251
  export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }): void {
243
252
  var existing = connections.get(nodeId);
244
253
  if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) {
254
+ log.meshConnect("inbound peer %s already connected, skipping", nodeId.slice(0, 8));
245
255
  return;
246
256
  }
257
+ log.meshConnect("registering inbound peer %s", nodeId.slice(0, 8));
247
258
 
248
259
  if (existing) {
249
260
  existing.dead = true;
@@ -3,19 +3,22 @@ import type { ClientMessage, MeshProxyRequestMessage, MeshProxyResponseMessage,
3
3
  import { getPeerConnection } from "./connector";
4
4
  import { sendTo, broadcast, registerVirtualClient, removeVirtualClient } from "../ws/broadcast";
5
5
  import { routeMessage } from "../ws/router";
6
+ import { log } from "../logger";
6
7
 
7
8
  var pendingRequests = new Map<string, string>();
8
9
 
9
10
  export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId: string, message: ClientMessage): void {
11
+ log.meshProxy("→ proxy %s to node %s for project %s", (message as any).type, nodeId.slice(0, 8), projectSlug);
10
12
  var ws = getPeerConnection(nodeId);
11
13
  if (!ws) {
12
- console.warn("[mesh/proxy] No connection to peer: " + nodeId);
13
- sendTo(clientId, { type: "chat:error", message: "Remote node " + nodeId + " is not connected" });
14
+ log.meshProxy(" no connection to node %s", nodeId.slice(0, 8));
15
+ sendTo(clientId, { type: "chat:error", message: "Remote node is not connected" });
14
16
  return;
15
17
  }
16
18
 
17
19
  var requestId = randomUUID();
18
20
  pendingRequests.set(requestId, clientId);
21
+ log.meshProxy(" envelope requestId=%s", requestId.slice(0, 8));
19
22
 
20
23
  var envelope: MeshProxyRequestMessage = {
21
24
  type: "mesh:proxy_request",
@@ -29,8 +32,10 @@ export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId:
29
32
 
30
33
  export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMessage): void {
31
34
  var proxyClientId = "mesh-proxy:" + sourceNodeId + ":" + msg.requestId;
35
+ log.meshProxy("← proxy_request from %s: %s for %s (reqId=%s)", sourceNodeId.slice(0, 8), (msg.payload as any).type, msg.projectSlug, msg.requestId.slice(0, 8));
32
36
 
33
37
  registerVirtualClient(proxyClientId, function (response: object) {
38
+ log.meshProxy(" → proxy_response %s back to %s", (response as any).type, sourceNodeId.slice(0, 8));
34
39
  var ws = getPeerConnection(sourceNodeId);
35
40
  if (!ws) {
36
41
  console.warn("[mesh/proxy] Cannot send response, no connection to: " + sourceNodeId);
@@ -53,9 +58,10 @@ export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMe
53
58
  }
54
59
 
55
60
  export function handleProxyResponse(msg: MeshProxyResponseMessage): void {
61
+ log.meshProxy("← proxy_response %s (reqId=%s)", (msg.payload as any).type, msg.requestId.slice(0, 8));
56
62
  var clientId = pendingRequests.get(msg.requestId);
57
63
  if (!clientId) {
58
- console.warn("[mesh/proxy] No pending request for id: " + msg.requestId);
64
+ log.meshProxy(" no pending request for %s", msg.requestId.slice(0, 8));
59
65
  return;
60
66
  }
61
67
 
@@ -37,6 +37,11 @@ export function sendTo(id: string, message: object): void {
37
37
  var virtualHandler = virtualSendHandlers.get(id);
38
38
  if (virtualHandler) {
39
39
  virtualHandler(message);
40
+ return;
41
+ }
42
+ if (id.startsWith("mesh-proxy:")) {
43
+ var { log } = require("../logger");
44
+ log.broadcast(" ✗ sendTo %s but no virtual handler registered (msg=%s)", id.slice(0, 30), (message as any).type);
40
45
  }
41
46
  }
42
47
 
@@ -53,6 +53,8 @@ export function getClientRemoteNode(clientId: string): { nodeId: string; project
53
53
  export function routeMessage(clientId: string, message: ClientMessage): void {
54
54
  var prefix = message.type.split(":")[0];
55
55
 
56
+ log.router("→ %s from client %s (prefix=%s)", message.type, clientId.slice(0, 8), prefix);
57
+
56
58
  if (PROXIED_PREFIXES.has(prefix)) {
57
59
  var remote = clientRemoteNode.get(clientId);
58
60
 
@@ -60,17 +62,21 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
60
62
 
61
63
  if (msgSlug) {
62
64
  var localProject = getLocalProject(msgSlug);
65
+ log.router(" slug=%s local=%s", msgSlug, localProject);
63
66
  if (!localProject) {
64
67
  var remoteEntry = getRemoteNodeForProject(msgSlug);
65
68
  if (remoteEntry) {
69
+ log.router(" → proxying to remote node %s for project %s", remoteEntry.nodeId.slice(0, 8), msgSlug);
66
70
  setClientRemoteNode(clientId, remoteEntry.nodeId, msgSlug);
67
71
  proxyMessage(clientId, remoteEntry.nodeId, msgSlug, message);
68
72
  return;
69
73
  }
74
+ log.router(" ✗ no remote node found for slug %s", msgSlug);
70
75
  } else if (message.type === "session:activate" || message.type === "session:list_request") {
71
76
  clearClientRemoteNode(clientId);
72
77
  }
73
78
  } else if (remote) {
79
+ log.router(" → proxying via cached remote node %s", remote.nodeId.slice(0, 8));
74
80
  proxyMessage(clientId, remote.nodeId, remote.projectSlug, message);
75
81
  return;
76
82
  }
@@ -78,6 +84,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
78
84
 
79
85
  var handler = handlers.get(prefix);
80
86
  if (handler) {
87
+ log.router(" → dispatching to %s handler", prefix);
81
88
  try {
82
89
  var result = handler(clientId, message);
83
90
  if (result && typeof result.then === "function") {
@@ -94,7 +101,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
94
101
  }
95
102
  return;
96
103
  }
97
- log.ws("No handler for message type: %s", message.type);
104
+ log.router(" no handler for %s", message.type);
98
105
  sendTo(clientId, { type: "error", message: `Unknown message type: ${message.type}` });
99
106
  }
100
107