feat(cli): use Claude Code session ID for mesh peer identity
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-08 11:38:44 +01:00
parent 9474d985ae
commit 52393429f9
3 changed files with 48 additions and 2 deletions

View File

@@ -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",

View File

@@ -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 } : {}),

View File

@@ -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/<project-hash>/ 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(),