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",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { mkdtempSync, writeFileSync, rmSync, readdirSync, statSync, existsSync, readFileSync } from "node:fs";
|
import { mkdtempSync, writeFileSync, rmSync, readdirSync, statSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { tmpdir, hostname, homedir } from "node:os";
|
import { tmpdir, hostname, homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
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
|
// passes -y / --yes. Without it, claudemesh tools still work because
|
||||||
// `claudemesh install` pre-approves them via allowedTools in settings.json.
|
// `claudemesh install` pre-approves them via allowedTools in settings.json.
|
||||||
// This keeps permissions tight for multi-person meshes.
|
// 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 = [
|
const claudeArgs = [
|
||||||
"--dangerously-load-development-channels",
|
"--dangerously-load-development-channels",
|
||||||
"server:claudemesh",
|
"server:claudemesh",
|
||||||
|
"--session-id", claudeSessionId,
|
||||||
...(args.skipPermConfirm ? ["--dangerously-skip-permissions"] : []),
|
...(args.skipPermConfirm ? ["--dangerously-skip-permissions"] : []),
|
||||||
...(args.systemPrompt ? ["--system-prompt", args.systemPrompt] : []),
|
...(args.systemPrompt ? ["--system-prompt", args.systemPrompt] : []),
|
||||||
...filtered,
|
...filtered,
|
||||||
@@ -437,6 +445,7 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
|||||||
...process.env,
|
...process.env,
|
||||||
CLAUDEMESH_CONFIG_DIR: tmpDir,
|
CLAUDEMESH_CONFIG_DIR: tmpDir,
|
||||||
CLAUDEMESH_DISPLAY_NAME: displayName,
|
CLAUDEMESH_DISPLAY_NAME: displayName,
|
||||||
|
CLAUDEMESH_SESSION_ID: claudeSessionId,
|
||||||
MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "30000",
|
MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "30000",
|
||||||
MAX_MCP_OUTPUT_TOKENS: process.env.MAX_MCP_OUTPUT_TOKENS ?? "50000",
|
MAX_MCP_OUTPUT_TOKENS: process.env.MAX_MCP_OUTPUT_TOKENS ?? "50000",
|
||||||
...(role ? { CLAUDEMESH_ROLE: role } : {}),
|
...(role ? { CLAUDEMESH_ROLE: role } : {}),
|
||||||
|
|||||||
@@ -23,6 +23,43 @@ import {
|
|||||||
import { signHello } from "../crypto/hello-sig";
|
import { signHello } from "../crypto/hello-sig";
|
||||||
import { generateKeypair } from "../crypto/keypair";
|
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 Priority = "now" | "next" | "low";
|
||||||
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
||||||
|
|
||||||
@@ -194,7 +231,7 @@ export class BrokerClient {
|
|||||||
pubkey: this.mesh.pubkey,
|
pubkey: this.mesh.pubkey,
|
||||||
sessionPubkey: this.sessionPubkey,
|
sessionPubkey: this.sessionPubkey,
|
||||||
displayName: process.env.CLAUDEMESH_DISPLAY_NAME || this.opts.displayName || undefined,
|
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,
|
pid: process.pid,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
hostname: require("os").hostname(),
|
hostname: require("os").hostname(),
|
||||||
|
|||||||
Reference in New Issue
Block a user