@cryptiklemur/lattice 1.41.1 → 1.41.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.
- package/package.json +1 -1
- package/server/src/project/sdk-bridge.ts +99 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.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,7 +4,7 @@ import type { SDKMessage, SDKPartialAssistantMessage, SDKResultMessage, SDKUserM
|
|
|
4
4
|
import type { CanUseTool, PermissionMode, PermissionResult, PermissionUpdate } from "@anthropic-ai/claude-agent-sdk";
|
|
5
5
|
type MessageParam = SDKUserMessage["message"];
|
|
6
6
|
import type { Attachment } from "@lattice/shared";
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, readlinkSync } from "node:fs";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { sendTo, broadcast } from "../ws/broadcast";
|
|
@@ -227,77 +227,117 @@ export function isSessionBusy(sessionId: string): boolean {
|
|
|
227
227
|
* The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
|
|
228
228
|
* lock files — those are NOT external.
|
|
229
229
|
*/
|
|
230
|
-
function
|
|
231
|
-
var
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
var stat = readFileSync("/proc/" + current + "/stat", "utf-8");
|
|
238
|
-
// Format: pid (comm) state ppid ...
|
|
239
|
-
var match = stat.match(/^\d+\s+\([^)]*\)\s+\S+\s+(\d+)/);
|
|
240
|
-
if (!match) return false;
|
|
241
|
-
var ppid = parseInt(match[1], 10);
|
|
242
|
-
if (ppid === myPid) return true;
|
|
243
|
-
if (ppid <= 1) return false;
|
|
244
|
-
current = ppid;
|
|
245
|
-
} catch {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
230
|
+
function getProjectPathForSession(sessionId: string): string | null {
|
|
231
|
+
var config = loadConfig();
|
|
232
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
233
|
+
var hash = config.projects[i].path.replace(/\//g, "-");
|
|
234
|
+
var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
235
|
+
if (existsSync(jsonlPath)) return config.projects[i].path;
|
|
248
236
|
}
|
|
249
|
-
return
|
|
237
|
+
return null;
|
|
250
238
|
}
|
|
251
239
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
* Returns the list of truly external PIDs.
|
|
255
|
-
*/
|
|
256
|
-
function getExternalLockPids(sessionId: string): number[] {
|
|
257
|
-
var lockPath = join(homedir(), ".claude", "tasks", sessionId, ".lock");
|
|
258
|
-
if (!existsSync(lockPath)) return [];
|
|
240
|
+
function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
|
|
241
|
+
var results: Array<{ pid: number; cwd: string; cmdline: string[] }> = [];
|
|
259
242
|
try {
|
|
260
|
-
var result = Bun.spawnSync(["
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
243
|
+
var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
|
|
244
|
+
if (result.exitCode !== 0) return results;
|
|
245
|
+
var pidStrs = result.stdout.toString().trim().split("\n");
|
|
246
|
+
for (var i = 0; i < pidStrs.length; i++) {
|
|
247
|
+
var pid = parseInt(pidStrs[i], 10);
|
|
248
|
+
if (isNaN(pid) || pid === process.pid) continue;
|
|
249
|
+
try {
|
|
250
|
+
var cwd = readlinkSync("/proc/" + pid + "/cwd");
|
|
251
|
+
var cmdline = readFileSync("/proc/" + pid + "/cmdline", "utf-8").split("\0");
|
|
252
|
+
results.push({ pid, cwd, cmdline });
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
} catch {}
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveSessionName(projectPath: string, name: string): string | null {
|
|
260
|
+
var hash = projectPath.replace(/\//g, "-");
|
|
261
|
+
var dir = join(homedir(), ".claude", "projects", hash);
|
|
262
|
+
if (!existsSync(dir)) return null;
|
|
263
|
+
|
|
264
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(name)) {
|
|
265
|
+
if (existsSync(join(dir, name + ".jsonl"))) return name;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
|
|
269
|
+
for (var e = 0; e < entries.length; e++) {
|
|
270
|
+
try {
|
|
271
|
+
var result = Bun.spawnSync(["grep", "-m", "1", "custom-title", join(dir, entries[e])], { stdout: "pipe", stderr: "ignore" });
|
|
272
|
+
if (result.exitCode !== 0) continue;
|
|
273
|
+
var line = result.stdout.toString().trim();
|
|
274
|
+
if (!line) continue;
|
|
275
|
+
var parsed = JSON.parse(line);
|
|
276
|
+
if (parsed.type === "custom-title" && parsed.customTitle === name) {
|
|
277
|
+
return entries[e].replace(".jsonl", "");
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
271
280
|
}
|
|
281
|
+
return null;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
function
|
|
275
|
-
|
|
284
|
+
function findMostRecentSession(projectPath: string): string | null {
|
|
285
|
+
var hash = projectPath.replace(/\//g, "-");
|
|
286
|
+
var dir = join(homedir(), ".claude", "projects", hash);
|
|
287
|
+
if (!existsSync(dir)) return null;
|
|
288
|
+
|
|
289
|
+
var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
|
|
290
|
+
var latest: { id: string; mtime: number } | null = null;
|
|
291
|
+
for (var e = 0; e < entries.length; e++) {
|
|
292
|
+
try {
|
|
293
|
+
var s = statSync(join(dir, entries[e]));
|
|
294
|
+
if (!latest || s.mtimeMs > latest.mtime) {
|
|
295
|
+
latest = { id: entries[e].replace(".jsonl", ""), mtime: s.mtimeMs };
|
|
296
|
+
}
|
|
297
|
+
} catch {}
|
|
298
|
+
}
|
|
299
|
+
if (latest && Date.now() - latest.mtime < 60000) return latest.id;
|
|
300
|
+
return null;
|
|
276
301
|
}
|
|
277
302
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
303
|
+
function getCliSessionIdForProject(projectPath: string): string | null {
|
|
304
|
+
var cliProcesses = getClaudeCliPids();
|
|
305
|
+
for (var i = 0; i < cliProcesses.length; i++) {
|
|
306
|
+
if (cliProcesses[i].cwd !== projectPath) continue;
|
|
307
|
+
|
|
308
|
+
var cmdline = cliProcesses[i].cmdline;
|
|
309
|
+
var resumeIdx = cmdline.indexOf("--resume");
|
|
310
|
+
if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
|
|
311
|
+
var sessionName = cmdline[resumeIdx + 1];
|
|
312
|
+
return resolveSessionName(projectPath, sessionName);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return findMostRecentSession(projectPath);
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isSessionLockedByExternal(sessionId: string): boolean {
|
|
321
|
+
if (activeStreams.has(sessionId)) return false;
|
|
322
|
+
var projectPath = getProjectPathForSession(sessionId);
|
|
323
|
+
if (!projectPath) return false;
|
|
324
|
+
var cliSessionId = getCliSessionIdForProject(projectPath);
|
|
325
|
+
return cliSessionId === sessionId;
|
|
285
326
|
}
|
|
286
327
|
|
|
287
|
-
/**
|
|
288
|
-
* Gracefully stop an external Claude Code CLI process controlling a session.
|
|
289
|
-
* Sends SIGINT which triggers Claude Code's graceful shutdown.
|
|
290
|
-
* Returns true if a signal was sent.
|
|
291
|
-
*/
|
|
292
328
|
export function stopExternalSession(sessionId: string): boolean {
|
|
293
|
-
var
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
var projectPath = getProjectPathForSession(sessionId);
|
|
330
|
+
if (!projectPath) return false;
|
|
331
|
+
var cliProcesses = getClaudeCliPids();
|
|
332
|
+
for (var i = 0; i < cliProcesses.length; i++) {
|
|
333
|
+
if (cliProcesses[i].cwd === projectPath) {
|
|
334
|
+
try {
|
|
335
|
+
process.kill(cliProcesses[i].pid, "SIGINT");
|
|
336
|
+
return true;
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
300
339
|
}
|
|
340
|
+
return false;
|
|
301
341
|
}
|
|
302
342
|
|
|
303
343
|
export function getSessionStreamClientId(sessionId: string): string | undefined {
|