From 52393429f9a2a3fb80d8d4e20f0e1301cc526217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:38:44 +0100 Subject: [PATCH] feat(cli): use Claude Code session ID for mesh peer identity claudemesh launch now generates a UUID and passes it to claude via --session-id flag + CLAUDEMESH_SESSION_ID env var. The MCP server reads this and sends it in the hello handshake. Fallback: when launched without claudemesh launch (e.g., claude --resume), detectClaudeSessionId() scans ~/.claude/projects/ for the most recent .jsonl file and extracts the session UUID from the filename. Benefits: - Broker detects reconnections (same session = restore state) - Multiple peers in same project dir get unique identities - Session identity persists across --resume Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/commands/launch.ts | 9 ++++++++ apps/cli/src/ws/client.ts | 39 ++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 721683f..dead8e3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.8.1", + "version": "0.8.2", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/launch.ts b/apps/cli/src/commands/launch.ts index 38da896..b3eb065 100644 --- a/apps/cli/src/commands/launch.ts +++ b/apps/cli/src/commands/launch.ts @@ -14,6 +14,7 @@ */ import { spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { mkdtempSync, writeFileSync, rmSync, readdirSync, statSync, existsSync, readFileSync } from "node:fs"; import { tmpdir, hostname, homedir } from "node:os"; import { join } from "node:path"; @@ -421,9 +422,16 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise< // passes -y / --yes. Without it, claudemesh tools still work because // `claudemesh install` pre-approves them via allowedTools in settings.json. // This keeps permissions tight for multi-person meshes. + // Generate a stable session ID for this launch. Used by the broker to: + // - detect reconnections (same session ID = restore state) + // - disambiguate multiple peers in the same project + // - persist identity across --resume (Claude reuses the session ID) + const claudeSessionId = randomUUID(); + const claudeArgs = [ "--dangerously-load-development-channels", "server:claudemesh", + "--session-id", claudeSessionId, ...(args.skipPermConfirm ? ["--dangerously-skip-permissions"] : []), ...(args.systemPrompt ? ["--system-prompt", args.systemPrompt] : []), ...filtered, @@ -437,6 +445,7 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise< ...process.env, CLAUDEMESH_CONFIG_DIR: tmpDir, CLAUDEMESH_DISPLAY_NAME: displayName, + CLAUDEMESH_SESSION_ID: claudeSessionId, MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "30000", MAX_MCP_OUTPUT_TOKENS: process.env.MAX_MCP_OUTPUT_TOKENS ?? "50000", ...(role ? { CLAUDEMESH_ROLE: role } : {}), diff --git a/apps/cli/src/ws/client.ts b/apps/cli/src/ws/client.ts index 46de52d..f34d588 100644 --- a/apps/cli/src/ws/client.ts +++ b/apps/cli/src/ws/client.ts @@ -23,6 +23,43 @@ import { import { signHello } from "../crypto/hello-sig"; import { generateKeypair } from "../crypto/keypair"; +/** + * Detect the Claude Code session ID from the filesystem. + * Fallback for when CLAUDEMESH_SESSION_ID env var isn't set + * (e.g., claude --resume without going through claudemesh launch). + * + * Scans ~/.claude/projects// for the most recently + * modified .jsonl file and extracts its sessionId. + */ +function detectClaudeSessionId(): string | null { + try { + const { readdirSync, statSync, readFileSync } = require("node:fs"); + const { join } = require("node:path"); + const { homedir } = require("node:os"); + const cwd = process.cwd(); + // Claude Code hashes the project path for the directory name + const projectsDir = join(homedir(), ".claude", "projects"); + // Find matching project dir — the hash includes the full path with dashes + const cwdHash = cwd.replace(/\//g, "-"); + const entries = readdirSync(projectsDir) as string[]; + const projectDir = entries.find((e: string) => e === cwdHash || e.startsWith(cwdHash)); + if (!projectDir) return null; + + const fullDir = join(projectsDir, projectDir); + const jsonls = (readdirSync(fullDir) as string[]) + .filter((f: string) => f.endsWith(".jsonl")) + .map((f: string) => ({ name: f, mtime: statSync(join(fullDir, f)).mtimeMs })) + .sort((a: any, b: any) => b.mtime - a.mtime); + + if (jsonls.length === 0) return null; + const latest = jsonls[0]!; + // Session ID is the filename without .jsonl + return latest.name.replace(".jsonl", ""); + } catch { + return null; + } +} + export type Priority = "now" | "next" | "low"; export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting"; @@ -194,7 +231,7 @@ export class BrokerClient { pubkey: this.mesh.pubkey, sessionPubkey: this.sessionPubkey, displayName: process.env.CLAUDEMESH_DISPLAY_NAME || this.opts.displayName || undefined, - sessionId: `${process.pid}-${Date.now()}`, + sessionId: process.env.CLAUDEMESH_SESSION_ID || detectClaudeSessionId() || `${process.pid}-${Date.now()}`, pid: process.pid, cwd: process.cwd(), hostname: require("os").hostname(),