@cryptiklemur/lattice 1.37.1 → 1.38.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.1",
3
+ "version": "1.38.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>",
@@ -5,7 +5,8 @@ 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, getConnectedPeerProjects } from "../mesh/connector";
8
+ import { getConnectedPeerIds, connectToPeer, reconnectPeer, getPeerConnection, disconnectPeer, getConnectedPeerProjects, registerInboundPeer } from "../mesh/connector";
9
+ import { getClientWebSocket } from "../ws/broadcast";
9
10
  import type { PeerInfo } from "@lattice/shared";
10
11
  import { networkInterfaces } from "node:os";
11
12
  import { existsSync, readFileSync } from "node:fs";
@@ -237,6 +238,12 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
237
238
  sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Public key mismatch — possible impersonation" });
238
239
  return;
239
240
  }
241
+
242
+ var inboundWs = getClientWebSocket(clientId);
243
+ if (inboundWs) {
244
+ registerInboundPeer(hello.nodeId, inboundWs as any);
245
+ }
246
+
240
247
  var identity = loadOrCreateIdentity();
241
248
  sendTo(clientId, {
242
249
  type: "mesh:hello" as any,
@@ -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,139 @@ 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("");
228
+ }
229
+
230
+ function runRestart(): void {
231
+ var pid = readPid();
232
+ if (pid !== null && isDaemonRunning(pid)) {
233
+ console.log("[lattice] Stopping daemon (PID " + pid + ")...");
234
+ try {
235
+ process.kill(pid, "SIGTERM");
236
+ } catch {}
237
+ removePid();
238
+
239
+ var waited = 0;
240
+ while (waited < 5000) {
241
+ try {
242
+ process.kill(pid, 0);
243
+ Bun.sleepSync(200);
244
+ waited += 200;
245
+ } catch {
246
+ break;
247
+ }
248
+ }
249
+ }
250
+
251
+ console.log("[lattice] Starting daemon...");
252
+ var logPath = join(getLatticeHome(), "daemon.log");
253
+
254
+ var spawnArgs = IS_COMPILED
255
+ ? [process.execPath, "daemon"]
256
+ : ["bun", import.meta.path, "daemon"];
257
+
258
+ if (portOverride) {
259
+ spawnArgs.push("--port", String(portOverride));
260
+ }
261
+
262
+ var child = Bun.spawn(spawnArgs, {
263
+ detached: true,
264
+ stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)],
265
+ });
266
+
267
+ child.unref();
268
+ writePid(child.pid);
269
+ console.log("[lattice] Daemon started (PID " + child.pid + ")");
270
+ }
271
+
272
+ async function runVersion(): Promise<void> {
273
+ var { checkForUpdate } = await import("./update-checker");
274
+ var info = await checkForUpdate(true);
275
+ console.log("[lattice] Current: v" + info.currentVersion);
276
+ if (info.latestVersion) {
277
+ if (info.updateAvailable) {
278
+ console.log("[lattice] Latest: v" + info.latestVersion + " (update available)");
279
+ console.log("[lattice] Run 'lattice update' to install");
280
+ } else {
281
+ console.log("[lattice] Latest: v" + info.latestVersion + " (up to date)");
282
+ }
283
+ }
284
+ console.log("[lattice] Mode: " + info.installMode);
285
+ }
286
+
287
+ function runLogs(): void {
288
+ var logPath = join(getLatticeHome(), "daemon.log");
289
+ if (!existsSync(logPath)) {
290
+ console.log("[lattice] No log file found at " + logPath);
291
+ process.exit(1);
292
+ }
293
+ console.log("[lattice] Tailing " + logPath + " (Ctrl+C to stop)");
294
+ var proc = Bun.spawn(["tail", "-f", "-n", "50", logPath], {
295
+ stdout: "inherit",
296
+ stderr: "inherit",
297
+ });
298
+ process.on("SIGINT", function () {
299
+ proc.kill();
300
+ process.exit(0);
301
+ });
302
+ }
303
+
304
+ function runOpen(): void {
305
+ var config = loadConfig();
306
+ var pid = readPid();
307
+ if (pid === null || !isDaemonRunning(pid)) {
308
+ console.log("[lattice] Daemon is not running. Start it with 'lattice start'");
309
+ process.exit(1);
310
+ }
311
+ var url = (config.tls ? "https" : "http") + "://localhost:" + config.port;
312
+ console.log("[lattice] Opening " + url);
313
+ openBrowser(url);
314
+ }
315
+
316
+ function runConfigInfo(): void {
317
+ var config = loadConfig();
318
+ var home = getLatticeHome();
319
+ console.log("[lattice] Home: " + home);
320
+ console.log("[lattice] Config: " + join(home, "config.json"));
321
+ console.log("[lattice] Port: " + config.port);
322
+ console.log("[lattice] Name: " + config.name);
323
+ console.log("[lattice] TLS: " + (config.tls ? "enabled" : "disabled"));
324
+ console.log("[lattice] Projects: " + config.projects.length);
325
+ for (var i = 0; i < config.projects.length; i++) {
326
+ console.log(" " + config.projects[i].slug + " → " + config.projects[i].path);
327
+ }
328
+ if (config.passphraseHash) {
329
+ console.log("[lattice] Passphrase: set");
330
+ }
331
+ if (config.costBudget) {
332
+ console.log("[lattice] Budget: $" + config.costBudget.dailyLimit + "/day (" + config.costBudget.enforcement + ")");
333
+ }
334
+ }
335
+
183
336
  function runStatus(): void {
184
337
  var pid = readPid();
185
338
  if (pid === null) {
@@ -239,6 +239,52 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
239
239
  return conn.ws;
240
240
  }
241
241
 
242
+ export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }): void {
243
+ var existing = connections.get(nodeId);
244
+ if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) {
245
+ return;
246
+ }
247
+
248
+ if (existing) {
249
+ existing.dead = true;
250
+ if (existing.retryTimer !== null) {
251
+ clearTimeout(existing.retryTimer);
252
+ }
253
+ }
254
+
255
+ circuitBreakers.delete(nodeId);
256
+
257
+ var conn: PeerConnection = {
258
+ nodeId: nodeId,
259
+ ws: ws as WebSocket,
260
+ backoffMs: 1000,
261
+ retryTimer: null,
262
+ dead: false,
263
+ projects: [],
264
+ };
265
+
266
+ connections.set(nodeId, conn);
267
+
268
+ var peers = loadPeers();
269
+ var peer = peers.find(function (p) { return p.id === nodeId; });
270
+ if (peer) {
271
+ var identity = loadOrCreateIdentity();
272
+ var config = loadConfig();
273
+ var projects = config.projects || [];
274
+ conn.projects = [];
275
+
276
+ ws.send(JSON.stringify({
277
+ type: "mesh:hello",
278
+ nodeId: identity.id,
279
+ name: config.name,
280
+ publicKey: identity.publicKey,
281
+ projects: projects.map(function (p: { slug: string; title: string }) {
282
+ return { slug: p.slug, title: p.title };
283
+ }),
284
+ }));
285
+ }
286
+ }
287
+
242
288
  export function disconnectPeer(nodeId: string): void {
243
289
  var existing = connections.get(nodeId);
244
290
  if (existing) {
@@ -40,6 +40,10 @@ export function sendTo(id: string, message: object): void {
40
40
  }
41
41
  }
42
42
 
43
+ export function getClientWebSocket(id: string): ServerWebSocket<{ id: string }> | undefined {
44
+ return clients.get(id);
45
+ }
46
+
43
47
  export function getClientCount(): number {
44
48
  return clients.size;
45
49
  }