Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92bb276a3e | ||
|
|
af8f8ed1f9 | ||
|
|
c8682dd700 |
@@ -307,6 +307,7 @@ export async function refreshStatusFromJsonl(
|
|||||||
export interface ConnectParams {
|
export interface ConnectParams {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
sessionPubkey?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
@@ -322,6 +323,7 @@ export async function connectPresence(
|
|||||||
.values({
|
.values({
|
||||||
memberId: params.memberId,
|
memberId: params.memberId,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
|
sessionPubkey: params.sessionPubkey ?? null,
|
||||||
displayName: params.displayName ?? null,
|
displayName: params.displayName ?? null,
|
||||||
pid: params.pid,
|
pid: params.pid,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
@@ -371,7 +373,8 @@ export async function listPeersInMesh(
|
|||||||
> {
|
> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
pubkey: memberTable.peerPubkey,
|
memberPubkey: memberTable.peerPubkey,
|
||||||
|
sessionPubkey: presence.sessionPubkey,
|
||||||
memberDisplayName: memberTable.displayName,
|
memberDisplayName: memberTable.displayName,
|
||||||
presenceDisplayName: presence.displayName,
|
presenceDisplayName: presence.displayName,
|
||||||
status: presence.status,
|
status: presence.status,
|
||||||
@@ -388,9 +391,9 @@ export async function listPeersInMesh(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.orderBy(asc(presence.connectedAt));
|
.orderBy(asc(presence.connectedAt));
|
||||||
// Prefer per-session displayName over member-level displayName.
|
// Prefer session pubkey for routing, session displayName for display.
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
pubkey: r.pubkey,
|
pubkey: r.sessionPubkey || r.memberPubkey,
|
||||||
displayName: r.presenceDisplayName || r.memberDisplayName,
|
displayName: r.presenceDisplayName || r.memberDisplayName,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
@@ -415,6 +418,7 @@ export async function setSummary(
|
|||||||
export interface QueueParams {
|
export interface QueueParams {
|
||||||
meshId: string;
|
meshId: string;
|
||||||
senderMemberId: string;
|
senderMemberId: string;
|
||||||
|
senderSessionPubkey?: string;
|
||||||
targetSpec: string;
|
targetSpec: string;
|
||||||
priority: Priority;
|
priority: Priority;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
@@ -429,6 +433,7 @@ export async function queueMessage(params: QueueParams): Promise<string> {
|
|||||||
.values({
|
.values({
|
||||||
meshId: params.meshId,
|
meshId: params.meshId,
|
||||||
senderMemberId: params.senderMemberId,
|
senderMemberId: params.senderMemberId,
|
||||||
|
senderSessionPubkey: params.senderSessionPubkey ?? null,
|
||||||
targetSpec: params.targetSpec,
|
targetSpec: params.targetSpec,
|
||||||
priority: params.priority,
|
priority: params.priority,
|
||||||
nonce: params.nonce,
|
nonce: params.nonce,
|
||||||
@@ -469,6 +474,7 @@ export async function drainForMember(
|
|||||||
_memberId: string,
|
_memberId: string,
|
||||||
memberPubkey: string,
|
memberPubkey: string,
|
||||||
status: PeerStatus,
|
status: PeerStatus,
|
||||||
|
sessionPubkey?: string,
|
||||||
): Promise<
|
): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -509,14 +515,14 @@ export async function drainForMember(
|
|||||||
WHERE mesh_id = ${meshId}
|
WHERE mesh_id = ${meshId}
|
||||||
AND delivered_at IS NULL
|
AND delivered_at IS NULL
|
||||||
AND priority::text IN (${priorityList})
|
AND priority::text IN (${priorityList})
|
||||||
AND (target_spec = ${memberPubkey} OR target_spec = '*')
|
AND (target_spec = ${memberPubkey} OR target_spec = '*'${sessionPubkey ? sql` OR target_spec = ${sessionPubkey}` : sql``})
|
||||||
ORDER BY created_at ASC, id ASC
|
ORDER BY created_at ASC, id ASC
|
||||||
FOR UPDATE SKIP LOCKED
|
FOR UPDATE SKIP LOCKED
|
||||||
)
|
)
|
||||||
AND m.id = mq.sender_member_id
|
AND m.id = mq.sender_member_id
|
||||||
RETURNING mq.id, mq.priority, mq.nonce, mq.ciphertext,
|
RETURNING mq.id, mq.priority, mq.nonce, mq.ciphertext,
|
||||||
mq.created_at, mq.sender_member_id,
|
mq.created_at, mq.sender_member_id,
|
||||||
m.peer_pubkey AS sender_pubkey
|
COALESCE(mq.sender_session_pubkey, m.peer_pubkey) AS sender_pubkey
|
||||||
)
|
)
|
||||||
SELECT * FROM claimed ORDER BY created_at ASC, id ASC
|
SELECT * FROM claimed ORDER BY created_at ASC, id ASC
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ interface PeerConn {
|
|||||||
meshId: string;
|
meshId: string;
|
||||||
memberId: string;
|
memberId: string;
|
||||||
memberPubkey: string;
|
memberPubkey: string;
|
||||||
|
sessionPubkey: string | null;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ async function maybePushQueuedMessages(presenceId: string): Promise<void> {
|
|||||||
conn.memberId,
|
conn.memberId,
|
||||||
conn.memberPubkey,
|
conn.memberPubkey,
|
||||||
status,
|
status,
|
||||||
|
conn.sessionPubkey ?? undefined,
|
||||||
);
|
);
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const push: WSPushMessage = {
|
const push: WSPushMessage = {
|
||||||
@@ -400,6 +402,7 @@ async function handleHello(
|
|||||||
const presenceId = await connectPresence({
|
const presenceId = await connectPresence({
|
||||||
memberId: member.id,
|
memberId: member.id,
|
||||||
sessionId: hello.sessionId,
|
sessionId: hello.sessionId,
|
||||||
|
sessionPubkey: hello.sessionPubkey,
|
||||||
displayName: hello.displayName,
|
displayName: hello.displayName,
|
||||||
pid: hello.pid,
|
pid: hello.pid,
|
||||||
cwd: hello.cwd,
|
cwd: hello.cwd,
|
||||||
@@ -409,6 +412,7 @@ async function handleHello(
|
|||||||
meshId: hello.meshId,
|
meshId: hello.meshId,
|
||||||
memberId: member.id,
|
memberId: member.id,
|
||||||
memberPubkey: hello.pubkey,
|
memberPubkey: hello.pubkey,
|
||||||
|
sessionPubkey: hello.sessionPubkey ?? null,
|
||||||
cwd: hello.cwd,
|
cwd: hello.cwd,
|
||||||
});
|
});
|
||||||
incMeshCount(hello.meshId);
|
incMeshCount(hello.meshId);
|
||||||
@@ -434,6 +438,7 @@ async function handleSend(
|
|||||||
const messageId = await queueMessage({
|
const messageId = await queueMessage({
|
||||||
meshId: conn.meshId,
|
meshId: conn.meshId,
|
||||||
senderMemberId: conn.memberId,
|
senderMemberId: conn.memberId,
|
||||||
|
senderSessionPubkey: conn.sessionPubkey ?? undefined,
|
||||||
targetSpec: msg.targetSpec,
|
targetSpec: msg.targetSpec,
|
||||||
priority: msg.priority,
|
priority: msg.priority,
|
||||||
nonce: msg.nonce,
|
nonce: msg.nonce,
|
||||||
@@ -450,7 +455,9 @@ async function handleSend(
|
|||||||
// Fan-out over connected peers in the same mesh.
|
// Fan-out over connected peers in the same mesh.
|
||||||
for (const [pid, peer] of connections) {
|
for (const [pid, peer] of connections) {
|
||||||
if (peer.meshId !== conn.meshId) continue;
|
if (peer.meshId !== conn.meshId) continue;
|
||||||
if (msg.targetSpec !== "*" && peer.memberPubkey !== msg.targetSpec)
|
if (msg.targetSpec !== "*"
|
||||||
|
&& peer.memberPubkey !== msg.targetSpec
|
||||||
|
&& peer.sessionPubkey !== msg.targetSpec)
|
||||||
continue;
|
continue;
|
||||||
void maybePushQueuedMessages(pid);
|
void maybePushQueuedMessages(pid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface WSHelloMessage {
|
|||||||
meshId: string;
|
meshId: string;
|
||||||
memberId: string;
|
memberId: string;
|
||||||
pubkey: string; // must match mesh.member.peerPubkey
|
pubkey: string; // must match mesh.member.peerPubkey
|
||||||
|
sessionPubkey?: string; // ephemeral per-launch pubkey for message routing
|
||||||
displayName?: string; // optional override for this session
|
displayName?: string; // optional override for this session
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.1.8",
|
"version": "0.1.11",
|
||||||
"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,7 +14,10 @@ import { parseInviteLink } from "../invite/parse";
|
|||||||
import { enrollWithBroker } from "../invite/enroll";
|
import { enrollWithBroker } from "../invite/enroll";
|
||||||
import { generateKeypair } from "../crypto/keypair";
|
import { generateKeypair } from "../crypto/keypair";
|
||||||
import { loadConfig, saveConfig, getConfigPath } from "../state/config";
|
import { loadConfig, saveConfig, getConfigPath } from "../state/config";
|
||||||
import { hostname } from "node:os";
|
import { writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { homedir, hostname } from "node:os";
|
||||||
|
import { env } from "../env";
|
||||||
|
|
||||||
export async function runJoin(args: string[]): Promise<void> {
|
export async function runJoin(args: string[]): Promise<void> {
|
||||||
const link = args[0];
|
const link = args[0];
|
||||||
@@ -78,6 +81,16 @@ export async function runJoin(args: string[]): Promise<void> {
|
|||||||
});
|
});
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
|
|
||||||
|
// 4b. Store invite token for per-session re-enrollment (launch --name).
|
||||||
|
const configDir = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||||
|
const inviteFile = join(configDir, `invite-${payload.mesh_slug}.txt`);
|
||||||
|
try {
|
||||||
|
mkdirSync(dirname(inviteFile), { recursive: true });
|
||||||
|
writeFileSync(inviteFile, link, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Non-fatal — launch will fall back to shared identity.
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Report.
|
// 5. Report.
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import { join } from "node:path";
|
|||||||
import { createInterface } from "node:readline";
|
import { createInterface } from "node:readline";
|
||||||
import { loadConfig, getConfigPath } from "../state/config";
|
import { loadConfig, getConfigPath } from "../state/config";
|
||||||
import type { Config, JoinedMesh } from "../state/config";
|
import type { Config, JoinedMesh } from "../state/config";
|
||||||
import { generateKeypair } from "../crypto/keypair";
|
|
||||||
import { enrollWithBroker } from "../invite/enroll";
|
|
||||||
import { parseInviteLink } from "../invite/parse";
|
|
||||||
|
|
||||||
// --- Arg parsing ---
|
// --- Arg parsing ---
|
||||||
|
|
||||||
@@ -174,12 +171,12 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
mesh = await pickMesh(config.meshes);
|
mesh = await pickMesh(config.meshes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Set display name. Uses existing member identity — the broker
|
// 3. Session identity. The WS client auto-generates a per-session
|
||||||
// creates a separate presence row per session (sessionId + pid)
|
// ephemeral keypair on connect (sent in hello as sessionPubkey).
|
||||||
// and stores the per-session displayName override.
|
// We just set the display name via env var.
|
||||||
const displayName = args.name ?? `${hostname()}-${process.pid}`;
|
const displayName = args.name ?? `${hostname()}-${process.pid}`;
|
||||||
|
|
||||||
// 4. Write session config to tmpdir (same mesh, same keypair).
|
// 4. Write session config to tmpdir (isolates mesh selection).
|
||||||
const tmpDir = mkdtempSync(join(tmpdir(), "claudemesh-"));
|
const tmpDir = mkdtempSync(join(tmpdir(), "claudemesh-"));
|
||||||
const sessionConfig: Config = {
|
const sessionConfig: Config = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -195,10 +192,20 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
if (!args.quiet) printBanner(displayName, mesh.slug);
|
if (!args.quiet) printBanner(displayName, mesh.slug);
|
||||||
|
|
||||||
// 6. Spawn claude with ephemeral config + dev channel + display name.
|
// 6. Spawn claude with ephemeral config + dev channel + display name.
|
||||||
|
// Strip any user-supplied --dangerously-load-development-channels
|
||||||
|
// to avoid duplicates — we always inject our own.
|
||||||
|
const filtered: string[] = [];
|
||||||
|
for (let i = 0; i < args.claudeArgs.length; i++) {
|
||||||
|
if (args.claudeArgs[i] === "--dangerously-load-development-channels") {
|
||||||
|
i++; // skip the next arg (the channel value) too
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.push(args.claudeArgs[i]!);
|
||||||
|
}
|
||||||
const claudeArgs = [
|
const claudeArgs = [
|
||||||
"--dangerously-load-development-channels",
|
"--dangerously-load-development-channels",
|
||||||
"server:claudemesh",
|
"server:claudemesh",
|
||||||
...args.claudeArgs,
|
...filtered,
|
||||||
];
|
];
|
||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
isDirectTarget,
|
isDirectTarget,
|
||||||
} from "../crypto/envelope";
|
} from "../crypto/envelope";
|
||||||
import { signHello } from "../crypto/hello-sig";
|
import { signHello } from "../crypto/hello-sig";
|
||||||
|
import { generateKeypair } from "../crypto/keypair";
|
||||||
|
|
||||||
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";
|
||||||
@@ -74,6 +75,8 @@ export class BrokerClient {
|
|||||||
private pushHandlers = new Set<PushHandler>();
|
private pushHandlers = new Set<PushHandler>();
|
||||||
private pushBuffer: InboundPush[] = [];
|
private pushBuffer: InboundPush[] = [];
|
||||||
private listPeersResolvers: Array<(peers: PeerInfo[]) => void> = [];
|
private listPeersResolvers: Array<(peers: PeerInfo[]) => void> = [];
|
||||||
|
private sessionPubkey: string | null = null;
|
||||||
|
private sessionSecretKey: string | null = null;
|
||||||
private closed = false;
|
private closed = false;
|
||||||
private reconnectAttempt = 0;
|
private reconnectAttempt = 0;
|
||||||
private helloTimer: NodeJS.Timeout | null = null;
|
private helloTimer: NodeJS.Timeout | null = null;
|
||||||
@@ -109,8 +112,13 @@ export class BrokerClient {
|
|||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const onOpen = async (): Promise<void> => {
|
const onOpen = async (): Promise<void> => {
|
||||||
this.debug("ws open → signing + sending hello");
|
this.debug("ws open → generating session keypair + signing hello");
|
||||||
try {
|
try {
|
||||||
|
// Generate per-session ephemeral keypair for message routing.
|
||||||
|
const sessionKP = await generateKeypair();
|
||||||
|
this.sessionPubkey = sessionKP.publicKey;
|
||||||
|
this.sessionSecretKey = sessionKP.secretKey;
|
||||||
|
|
||||||
const { timestamp, signature } = await signHello(
|
const { timestamp, signature } = await signHello(
|
||||||
this.mesh.meshId,
|
this.mesh.meshId,
|
||||||
this.mesh.memberId,
|
this.mesh.memberId,
|
||||||
@@ -123,6 +131,7 @@ export class BrokerClient {
|
|||||||
meshId: this.mesh.meshId,
|
meshId: this.mesh.meshId,
|
||||||
memberId: this.mesh.memberId,
|
memberId: this.mesh.memberId,
|
||||||
pubkey: this.mesh.pubkey,
|
pubkey: this.mesh.pubkey,
|
||||||
|
sessionPubkey: this.sessionPubkey,
|
||||||
displayName: process.env.CLAUDEMESH_DISPLAY_NAME || undefined,
|
displayName: process.env.CLAUDEMESH_DISPLAY_NAME || undefined,
|
||||||
sessionId: `${process.pid}-${Date.now()}`,
|
sessionId: `${process.pid}-${Date.now()}`,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
@@ -203,7 +212,7 @@ export class BrokerClient {
|
|||||||
const env = await encryptDirect(
|
const env = await encryptDirect(
|
||||||
message,
|
message,
|
||||||
targetSpec,
|
targetSpec,
|
||||||
this.mesh.secretKey,
|
this.sessionSecretKey ?? this.mesh.secretKey,
|
||||||
);
|
);
|
||||||
nonce = env.nonce;
|
nonce = env.nonce;
|
||||||
ciphertext = env.ciphertext;
|
ciphertext = env.ciphertext;
|
||||||
@@ -349,7 +358,7 @@ export class BrokerClient {
|
|||||||
plaintext = await decryptDirect(
|
plaintext = await decryptDirect(
|
||||||
{ nonce, ciphertext },
|
{ nonce, ciphertext },
|
||||||
senderPubkey,
|
senderPubkey,
|
||||||
this.mesh.secretKey,
|
this.sessionSecretKey ?? this.mesh.secretKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Legacy/broadcast path: no senderPubkey means the message
|
// Legacy/broadcast path: no senderPubkey means the message
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "mesh"."presence" ADD COLUMN "session_pubkey" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "mesh"."message_queue" ADD COLUMN "sender_session_pubkey" text;
|
||||||
@@ -192,6 +192,7 @@ export const presence = meshSchema.table("presence", {
|
|||||||
.references(() => meshMember.id, { onDelete: "cascade", onUpdate: "cascade" })
|
.references(() => meshMember.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
.notNull(),
|
.notNull(),
|
||||||
sessionId: text().notNull(),
|
sessionId: text().notNull(),
|
||||||
|
sessionPubkey: text(),
|
||||||
displayName: text(),
|
displayName: text(),
|
||||||
pid: integer().notNull(),
|
pid: integer().notNull(),
|
||||||
cwd: text().notNull(),
|
cwd: text().notNull(),
|
||||||
@@ -221,6 +222,7 @@ export const messageQueue = meshSchema.table("message_queue", {
|
|||||||
senderMemberId: text()
|
senderMemberId: text()
|
||||||
.references(() => meshMember.id, { onDelete: "cascade", onUpdate: "cascade" })
|
.references(() => meshMember.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
senderSessionPubkey: text(),
|
||||||
targetSpec: text().notNull(),
|
targetSpec: text().notNull(),
|
||||||
priority: messagePriorityEnum().notNull().default("next"),
|
priority: messagePriorityEnum().notNull().default("next"),
|
||||||
nonce: text().notNull(),
|
nonce: text().notNull(),
|
||||||
|
|||||||
Reference in New Issue
Block a user