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) <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user