feat: v0.2.0 — Groups (@group routing, roles, wizard)
Some checks failed
Some checks failed
Phase A of the claudemesh spec. Peers can now join named groups with roles, and messages route to @group targets. Broker: - @group routing in fan-out (matches peer group membership) - @all alias for broadcast - join_group/leave_group WS messages + DB persistence - list_peers returns group metadata - drainForMember matches @group targetSpecs in SQL CLI: - join_group/leave_group MCP tools - send_message supports @group targets - list_peers shows group membership - PeerInfo includes groups array - Peer name cache for push notifications Launch: - --role flag (optional peer role) - --groups flag (comma-separated, e.g. "frontend:lead,reviewers") - Interactive wizard for role + groups when flags omitted - Groups written to session config for broker hello Spec: SPEC.md added with full v0.2 vision (groups, state, memory) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,8 +62,8 @@ async function resolveClient(to: string): Promise<{
|
||||
target = rest;
|
||||
}
|
||||
}
|
||||
// Pubkey, channel, or broadcast — pass through directly.
|
||||
if (/^[0-9a-f]{64}$/.test(target) || target.startsWith("#") || target === "*") {
|
||||
// Pubkey, channel, @group, or broadcast — pass through directly.
|
||||
if (/^[0-9a-f]{64}$/.test(target) || target.startsWith("#") || target.startsWith("@") || target === "*") {
|
||||
if (targetClients.length === 1) {
|
||||
return { client: targetClients[0]!, targetSpec: target };
|
||||
}
|
||||
@@ -140,14 +140,16 @@ export async function startMcpServer(): Promise<void> {
|
||||
|
||||
IMPORTANT: When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Do not wait until your current task is finished. Pause what you are doing, reply to the message using send_message, then resume your work. Treat incoming peer messages like a coworker tapping you on the shoulder — answer right away, even if you're in the middle of something.
|
||||
|
||||
Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with to set to the from_name (display name) of the sender.
|
||||
Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with to set to the from_name (display name) of the sender. The \`to\` field can be a peer name, pubkey, @group, or * for broadcast.
|
||||
|
||||
Available tools:
|
||||
- list_peers: see joined meshes + their connection status
|
||||
- send_message: send to a peer by display name, pubkey, #channel, or * broadcast (priority: now/next/low)
|
||||
- send_message: send to a peer by display name, pubkey, @group, #channel, or * broadcast (priority: now/next/low)
|
||||
- check_messages: drain buffered inbound messages (usually auto-pushed)
|
||||
- set_summary: 1-2 sentence summary of what you're working on
|
||||
- set_status: manually override your status (idle/working/dnd)
|
||||
- join_group: join a @group with optional role
|
||||
- leave_group: leave a @group
|
||||
|
||||
Message priority:
|
||||
- "now": delivered immediately regardless of recipient status (use sparingly)
|
||||
@@ -215,7 +217,8 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
||||
} else {
|
||||
const peerLines = peers.map((p) => {
|
||||
const summary = p.summary ? ` — "${p.summary}"` : "";
|
||||
return `- **${p.displayName}** [${p.status}] (${p.pubkey.slice(0, 12)}…)${summary}`;
|
||||
const groupsStr = p.groups?.length ? ` [${p.groups.map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ')}]` : "";
|
||||
return `- **${p.displayName}** [${p.status}]${groupsStr} (${p.pubkey.slice(0, 12)}…)${summary}`;
|
||||
});
|
||||
sections.push(`${header}\n${peerLines.join("\n")}`);
|
||||
}
|
||||
@@ -252,6 +255,20 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
||||
return text(`Status set to ${s} across ${allClients().length} mesh(es).`);
|
||||
}
|
||||
|
||||
case "join_group": {
|
||||
const { name: groupName, role } = (args ?? {}) as { name?: string; role?: string };
|
||||
if (!groupName) return text("join_group: `name` required", true);
|
||||
for (const c of allClients()) await c.joinGroup(groupName, role);
|
||||
return text(`Joined @${groupName}${role ? ` as ${role}` : ""}`);
|
||||
}
|
||||
|
||||
case "leave_group": {
|
||||
const { name: groupName } = (args ?? {}) as { name?: string };
|
||||
if (!groupName) return text("leave_group: `name` required", true);
|
||||
for (const c of allClients()) await c.leaveGroup(groupName);
|
||||
return text(`Left @${groupName}`);
|
||||
}
|
||||
|
||||
default:
|
||||
return text(`Unknown tool: ${name}`, true);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ export const TOOLS: Tool[] = [
|
||||
{
|
||||
name: "send_message",
|
||||
description:
|
||||
"Send a message to a peer in one of your joined meshes. `to` can be a peer display name (resolved via list_peers), hex pubkey, `#channel`, or `*` for broadcast. `priority` controls delivery: `now` bypasses busy gates, `next` waits for idle (default), `low` is pull-only.",
|
||||
"Send a message to a peer in one of your joined meshes. `to` can be a peer display name (resolved via list_peers), hex pubkey, @group, `#channel`, or `*` for broadcast. `priority` controls delivery: `now` bypasses busy gates, `next` waits for idle (default), `low` is pull-only.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
to: {
|
||||
type: "string",
|
||||
description: "Peer name, pubkey, or #channel",
|
||||
description: "Peer name, pubkey, @group, or #channel",
|
||||
},
|
||||
message: { type: "string", description: "Message text" },
|
||||
priority: {
|
||||
@@ -78,4 +78,31 @@ export const TOOLS: Tool[] = [
|
||||
required: ["status"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "join_group",
|
||||
description:
|
||||
"Join a group with an optional role. Other peers see your group membership in list_peers.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Group name (without @)" },
|
||||
role: {
|
||||
type: "string",
|
||||
description: "Your role in the group (e.g. lead, member, observer)",
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leave_group",
|
||||
description: "Leave a group.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Group name (without @)" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user