feat(cli): v0.1.6 — name-based peer routing in send_message
resolveClient() now resolves display names via list_peers WS query. Supports exact match, partial match (unique substring), and falls back to pubkey/channel/broadcast pass-through. 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.1.5",
|
"version": "0.1.6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -33,39 +33,68 @@ function text(msg: string, isError = false) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a `to` string, pick which mesh to send from. Strategies:
|
* Given a `to` string, pick which mesh to send from. Strategies:
|
||||||
* - If `to` looks like a pubkey hex (64 chars), try every client;
|
* - If `to` looks like a pubkey hex (64 chars), use as-is.
|
||||||
* caller is expected to know which mesh the pubkey lives in.
|
* - If `to` starts with `#`, treat as channel.
|
||||||
* - If `to` starts with `#`, treat as channel on the first mesh.
|
* - If `to` is `*`, treat as broadcast.
|
||||||
* - Otherwise try to match a displayName (TODO — needs list_peers).
|
* - Otherwise resolve as a display name via list_peers.
|
||||||
*
|
*
|
||||||
* For now the MVP: if only one mesh is joined, use that. Otherwise
|
* Explicit mesh prefix `<mesh-slug>:<target>` narrows to one mesh.
|
||||||
* require the caller to prefix with `<mesh-slug>:`.
|
|
||||||
*/
|
*/
|
||||||
function resolveClient(to: string): {
|
async function resolveClient(to: string): Promise<{
|
||||||
client: BrokerClient | null;
|
client: BrokerClient | null;
|
||||||
targetSpec: string;
|
targetSpec: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
} {
|
}> {
|
||||||
const clients = allClients();
|
const clients = allClients();
|
||||||
if (clients.length === 0) {
|
if (clients.length === 0) {
|
||||||
return { client: null, targetSpec: to, error: "no meshes joined" };
|
return { client: null, targetSpec: to, error: "no meshes joined" };
|
||||||
}
|
}
|
||||||
// Explicit mesh prefix: "mesh-slug:targetspec"
|
// Explicit mesh prefix: "mesh-slug:targetspec"
|
||||||
|
let targetClients = clients;
|
||||||
|
let target = to;
|
||||||
const colonIdx = to.indexOf(":");
|
const colonIdx = to.indexOf(":");
|
||||||
if (colonIdx > 0 && colonIdx < to.length - 1) {
|
if (colonIdx > 0 && colonIdx < to.length - 1) {
|
||||||
const slug = to.slice(0, colonIdx);
|
const slug = to.slice(0, colonIdx);
|
||||||
const rest = to.slice(colonIdx + 1);
|
const rest = to.slice(colonIdx + 1);
|
||||||
const match = findClient(slug);
|
const match = findClient(slug);
|
||||||
if (match) return { client: match, targetSpec: rest };
|
if (match) {
|
||||||
|
targetClients = [match];
|
||||||
|
target = rest;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Single-mesh fast path.
|
// Pubkey, channel, or broadcast — pass through directly.
|
||||||
if (clients.length === 1) {
|
if (/^[0-9a-f]{64}$/.test(target) || target.startsWith("#") || target === "*") {
|
||||||
return { client: clients[0]!, targetSpec: to };
|
if (targetClients.length === 1) {
|
||||||
|
return { client: targetClients[0]!, targetSpec: target };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
client: null,
|
||||||
|
targetSpec: target,
|
||||||
|
error: `multiple meshes joined; prefix target with "<mesh-slug>:" (joined: ${clients.map((c) => c.meshSlug).join(", ")})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Name-based resolution: query each mesh's peer list for a matching displayName.
|
||||||
|
const nameLower = target.toLowerCase();
|
||||||
|
for (const c of targetClients) {
|
||||||
|
const peers = await c.listPeers();
|
||||||
|
const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
|
||||||
|
if (match) return { client: c, targetSpec: match.pubkey };
|
||||||
|
// Partial match: if only one peer's name contains the search string.
|
||||||
|
const partials = peers.filter((p) =>
|
||||||
|
p.displayName.toLowerCase().includes(nameLower),
|
||||||
|
);
|
||||||
|
if (partials.length === 1) {
|
||||||
|
return { client: c, targetSpec: partials[0]!.pubkey };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Single-mesh fallback: let the broker try to resolve it.
|
||||||
|
if (targetClients.length === 1) {
|
||||||
|
return { client: targetClients[0]!, targetSpec: target };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
client: null,
|
client: null,
|
||||||
targetSpec: to,
|
targetSpec: target,
|
||||||
error: `multiple meshes joined; prefix target with "<mesh-slug>:" (joined: ${clients.map((c) => c.meshSlug).join(", ")})`,
|
error: `peer "${target}" not found in any mesh (joined: ${clients.map((c) => c.meshSlug).join(", ")})`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +126,7 @@ Read the from_id, from_name, mesh_slug, and priority attributes to understand co
|
|||||||
|
|
||||||
Available tools:
|
Available tools:
|
||||||
- list_peers: see joined meshes + their connection status
|
- list_peers: see joined meshes + their connection status
|
||||||
- send_message: send to a peer pubkey, channel, or broadcast (priority: now/next/low)
|
- send_message: send to a peer by display name, pubkey, #channel, or * broadcast (priority: now/next/low)
|
||||||
- check_messages: drain buffered inbound messages (usually auto-pushed)
|
- check_messages: drain buffered inbound messages (usually auto-pushed)
|
||||||
- set_summary: 1-2 sentence summary of what you're working on
|
- set_summary: 1-2 sentence summary of what you're working on
|
||||||
- set_status: manually override your status (idle/working/dnd)
|
- set_status: manually override your status (idle/working/dnd)
|
||||||
@@ -129,7 +158,7 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
|
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
|
||||||
if (!to || !message)
|
if (!to || !message)
|
||||||
return text("send_message: `to` and `message` required", true);
|
return text("send_message: `to` and `message` required", true);
|
||||||
const { client, targetSpec, error } = resolveClient(to);
|
const { client, targetSpec, error } = await resolveClient(to);
|
||||||
if (!client)
|
if (!client)
|
||||||
return text(`send_message: ${error ?? "no client resolved"}`, true);
|
return text(`send_message: ${error ?? "no client resolved"}`, true);
|
||||||
const result = await client.send(
|
const result = await client.send(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const TOOLS: Tool[] = [
|
|||||||
{
|
{
|
||||||
name: "send_message",
|
name: "send_message",
|
||||||
description:
|
description:
|
||||||
"Send a message to a peer in one of your joined meshes. `to` is a peer display name, hex pubkey, or `#channel`. `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, `#channel`, or `*` for broadcast. `priority` controls delivery: `now` bypasses busy gates, `next` waits for idle (default), `low` is pull-only.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
Reference in New Issue
Block a user