feat(cli): wss push → mcp channel injection + status hooks in install
Full parity with claude-peers:
1. Push-injection (the "tap on shoulder" UX)
- MCP server now declares experimental.claude/channel capability
- BrokerClient onPush handlers emit server.notification({
method: "notifications/claude/channel",
params: { content, meta: {from_id, from_name, mesh_slug,
mesh_id, priority, sent_at, delivered_at, kind}}
})
- Claude Code injects each push as <channel source="claudemesh">
system reminder, so the receiver session sees inbound messages
WITHOUT calling check_messages manually
- Updated MCP instructions with the "RESPOND IMMEDIATELY" framing
(adapted from claude-peers)
2. Status hooks in install (default-on, --no-hooks to opt out)
- new apps/cli/src/commands/hook.ts: reads stdin JSON (Claude Code
hook payload), extracts cwd+session_id, POSTs /hook/set-status
to every joined mesh's broker in parallel with process.ppid +
1s timeout per POST. Silent fail, fire-and-forget.
- install.ts: writes to ~/.claude/settings.json registering
`claudemesh hook idle` on Stop + `claudemesh hook working` on
UserPromptSubmit. Idempotent, preserves other hook entries.
- uninstall.ts: removes both hook entries + MCP entry; leaves
unrelated hook/MCP entries alone.
- dedupes by brokerUrl (multiple meshes on same broker → one POST)
3. CLI surface
- new subcommand: `claudemesh hook <status>` (internal, but
exposed so Claude Code can invoke it via the hook shell command)
- `install --no-hooks` for users who want bare MCP registration
- --help updated
Coexistence with claude-peers: both tools register Stop and
UserPromptSubmit hooks, each POSTs to its own broker. Claude Code
fires multiple hooks per event without conflict.
npm version 0.1.0 → 0.1.1 (patch).
Verified:
- install with hooks → 2 entries added to settings.json ✓
- install --no-hooks → "Hooks skipped" ✓
- uninstall → both MCP entry + 2 hook entries removed ✓
- `echo '{...}' | claudemesh hook idle` with no joined meshes →
silent no-op ("no joined meshes, nothing to do") ✓
- MCP initialize response includes experimental.claude/channel ✓
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.0",
|
"version": "0.1.1",
|
||||||
"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",
|
||||||
|
|||||||
123
apps/cli/src/commands/hook.ts
Normal file
123
apps/cli/src/commands/hook.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* `claudemesh hook <status>` — Claude Code hook handler.
|
||||||
|
*
|
||||||
|
* Registered as a Stop + UserPromptSubmit hook by `claudemesh install`.
|
||||||
|
* On each turn boundary, Claude Code invokes:
|
||||||
|
*
|
||||||
|
* Stop → `claudemesh hook idle`
|
||||||
|
* UserPromptSubmit → `claudemesh hook working`
|
||||||
|
*
|
||||||
|
* We read the Claude Code hook JSON payload from stdin (contains cwd +
|
||||||
|
* session_id), then POST `/hook/set-status` to EVERY joined mesh's
|
||||||
|
* broker with {cwd, pid, status, session_id}. Each broker looks up
|
||||||
|
* its local presence row by (pid, cwd) and updates status.
|
||||||
|
*
|
||||||
|
* Fire-and-forget, silent. Hooks must NEVER block Claude Code or
|
||||||
|
* surface errors to the user. Debug logging available via
|
||||||
|
* CLAUDEMESH_HOOK_DEBUG=1.
|
||||||
|
*
|
||||||
|
* Why send to every broker? A user joined to multiple meshes has
|
||||||
|
* one presence row per mesh, each on its own broker. A turn boundary
|
||||||
|
* updates the status on every broker where this session is active.
|
||||||
|
* Brokers that don't have a matching presence just queue the signal
|
||||||
|
* in pending_status (harmless, TTL-swept).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { loadConfig } from "../state/config";
|
||||||
|
|
||||||
|
const DEBUG = process.env.CLAUDEMESH_HOOK_DEBUG === "1";
|
||||||
|
|
||||||
|
function debug(msg: string): void {
|
||||||
|
if (DEBUG) console.error(`[claudemesh-hook] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WS URL → HTTP URL (same host, swap scheme). */
|
||||||
|
function wsToHttp(wsUrl: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(wsUrl);
|
||||||
|
const httpScheme = u.protocol === "wss:" ? "https:" : "http:";
|
||||||
|
return `${httpScheme}//${u.host}`;
|
||||||
|
} catch {
|
||||||
|
return wsUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdinJson(): Promise<Record<string, unknown>> {
|
||||||
|
if (process.stdin.isTTY) return {};
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
const reader = process.stdin;
|
||||||
|
try {
|
||||||
|
for await (const chunk of reader) {
|
||||||
|
chunks.push(chunk as Uint8Array);
|
||||||
|
if (chunks.reduce((n, c) => n + c.length, 0) > 256 * 1024) break;
|
||||||
|
}
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
||||||
|
if (!raw) return {};
|
||||||
|
return JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postHook(
|
||||||
|
brokerWsUrl: string,
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
const base = wsToHttp(brokerWsUrl);
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const t = setTimeout(() => controller.abort(), 1000);
|
||||||
|
await fetch(`${base}/hook/set-status`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: controller.signal,
|
||||||
|
}).finally(() => clearTimeout(t));
|
||||||
|
} catch (e) {
|
||||||
|
debug(`post failed ${base}: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runHook(args: string[]): Promise<void> {
|
||||||
|
const status = args[0];
|
||||||
|
if (!status || !["idle", "working", "dnd"].includes(status)) {
|
||||||
|
// Silent no-op — we never want a hook to surface an error.
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Claude Code's stdin payload for cwd + session_id.
|
||||||
|
const stdinTimeout = new Promise<Record<string, unknown>>((r) =>
|
||||||
|
setTimeout(() => r({}), 500),
|
||||||
|
);
|
||||||
|
const payload = await Promise.race([readStdinJson(), stdinTimeout]);
|
||||||
|
const cwd =
|
||||||
|
(typeof payload.cwd === "string" && payload.cwd) ||
|
||||||
|
process.env.CLAUDE_PROJECT_DIR ||
|
||||||
|
process.cwd();
|
||||||
|
const sessionId =
|
||||||
|
(typeof payload.session_id === "string" && payload.session_id) || "";
|
||||||
|
|
||||||
|
// Fan out to EVERY joined mesh's broker in parallel.
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = loadConfig();
|
||||||
|
} catch (e) {
|
||||||
|
debug(`config load failed: ${e instanceof Error ? e.message : e}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (config.meshes.length === 0) {
|
||||||
|
debug("no joined meshes, nothing to do");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { cwd, pid: process.ppid, status, session_id: sessionId };
|
||||||
|
debug(
|
||||||
|
`status=${status} cwd=${cwd} meshes=${config.meshes.length} session=${sessionId.slice(0, 8)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dedupe by brokerUrl — if multiple meshes share a broker, one POST
|
||||||
|
// covers them (broker resolves presence by cwd+pid regardless).
|
||||||
|
const brokerUrls = [...new Set(config.meshes.map((m) => m.brokerUrl))];
|
||||||
|
await Promise.all(brokerUrls.map((url) => postHook(url, body)));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
@@ -31,6 +31,10 @@ import { spawnSync } from "node:child_process";
|
|||||||
|
|
||||||
const MCP_NAME = "claudemesh";
|
const MCP_NAME = "claudemesh";
|
||||||
const CLAUDE_CONFIG = join(homedir(), ".claude.json");
|
const CLAUDE_CONFIG = join(homedir(), ".claude.json");
|
||||||
|
const CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
||||||
|
const HOOK_COMMAND_STOP = "claudemesh hook idle";
|
||||||
|
const HOOK_COMMAND_USER_PROMPT = "claudemesh hook working";
|
||||||
|
const HOOK_MARKER = "claudemesh hook ";
|
||||||
|
|
||||||
type McpEntry = {
|
type McpEntry = {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -38,6 +42,16 @@ type McpEntry = {
|
|||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface HookCommand {
|
||||||
|
type: "command";
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
interface HookMatcher {
|
||||||
|
matcher?: string;
|
||||||
|
hooks: HookCommand[];
|
||||||
|
}
|
||||||
|
type HooksConfig = Record<string, HookMatcher[]>;
|
||||||
|
|
||||||
function readClaudeConfig(): Record<string, unknown> {
|
function readClaudeConfig(): Record<string, unknown> {
|
||||||
if (!existsSync(CLAUDE_CONFIG)) return {};
|
if (!existsSync(CLAUDE_CONFIG)) return {};
|
||||||
const text = readFileSync(CLAUDE_CONFIG, "utf-8").trim();
|
const text = readFileSync(CLAUDE_CONFIG, "utf-8").trim();
|
||||||
@@ -116,7 +130,87 @@ function entriesEqual(a: McpEntry, b: McpEntry): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runInstall(): void {
|
function readClaudeSettings(): Record<string, unknown> {
|
||||||
|
if (!existsSync(CLAUDE_SETTINGS)) return {};
|
||||||
|
const text = readFileSync(CLAUDE_SETTINGS, "utf-8").trim();
|
||||||
|
if (!text) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(text) as Record<string, unknown>;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to parse ${CLAUDE_SETTINGS}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeClaudeSettings(obj: Record<string, unknown>): void {
|
||||||
|
mkdirSync(dirname(CLAUDE_SETTINGS), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
CLAUDE_SETTINGS,
|
||||||
|
JSON.stringify(obj, null, 2) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a Stop + UserPromptSubmit hook entry to ~/.claude/settings.json,
|
||||||
|
* idempotent on the command string. Returns counts for reporting.
|
||||||
|
*/
|
||||||
|
function installHooks(): { added: number; unchanged: number } {
|
||||||
|
const settings = readClaudeSettings();
|
||||||
|
const hooks = ((settings.hooks ??= {}) as HooksConfig) ?? {};
|
||||||
|
let added = 0;
|
||||||
|
let unchanged = 0;
|
||||||
|
|
||||||
|
const ensure = (event: string, command: string): void => {
|
||||||
|
const list = (hooks[event] ??= []);
|
||||||
|
const alreadyPresent = list.some((entry) =>
|
||||||
|
(entry.hooks ?? []).some((h) => h.command === command),
|
||||||
|
);
|
||||||
|
if (alreadyPresent) {
|
||||||
|
unchanged += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.push({ hooks: [{ type: "command", command }] });
|
||||||
|
added += 1;
|
||||||
|
};
|
||||||
|
ensure("Stop", HOOK_COMMAND_STOP);
|
||||||
|
ensure("UserPromptSubmit", HOOK_COMMAND_USER_PROMPT);
|
||||||
|
|
||||||
|
settings.hooks = hooks;
|
||||||
|
writeClaudeSettings(settings);
|
||||||
|
return { added, unchanged };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove every hook entry whose command contains "claudemesh hook "
|
||||||
|
* from ~/.claude/settings.json. Idempotent. Returns removed count.
|
||||||
|
*/
|
||||||
|
function uninstallHooks(): number {
|
||||||
|
if (!existsSync(CLAUDE_SETTINGS)) return 0;
|
||||||
|
const settings = readClaudeSettings();
|
||||||
|
const hooks = settings.hooks as HooksConfig | undefined;
|
||||||
|
if (!hooks) return 0;
|
||||||
|
let removed = 0;
|
||||||
|
for (const event of Object.keys(hooks)) {
|
||||||
|
const kept: HookMatcher[] = [];
|
||||||
|
for (const entry of hooks[event] ?? []) {
|
||||||
|
const filtered = (entry.hooks ?? []).filter(
|
||||||
|
(h) => !(h.command ?? "").includes(HOOK_MARKER),
|
||||||
|
);
|
||||||
|
removed += (entry.hooks ?? []).length - filtered.length;
|
||||||
|
if (filtered.length > 0) kept.push({ ...entry, hooks: filtered });
|
||||||
|
}
|
||||||
|
if (kept.length === 0) delete hooks[event];
|
||||||
|
else hooks[event] = kept;
|
||||||
|
}
|
||||||
|
settings.hooks = hooks;
|
||||||
|
writeClaudeSettings(settings);
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runInstall(args: string[] = []): void {
|
||||||
|
const skipHooks = args.includes("--no-hooks");
|
||||||
console.log("claudemesh install");
|
console.log("claudemesh install");
|
||||||
console.log("------------------");
|
console.log("------------------");
|
||||||
|
|
||||||
@@ -182,6 +276,31 @@ export function runInstall(): void {
|
|||||||
` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`,
|
` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Hooks — status accuracy (Stop/UserPromptSubmit → POST /hook/set-status).
|
||||||
|
if (!skipHooks) {
|
||||||
|
try {
|
||||||
|
const { added, unchanged } = installHooks();
|
||||||
|
if (added > 0) {
|
||||||
|
console.log(
|
||||||
|
`✓ Hooks registered (Stop + UserPromptSubmit) → ${added} added, ${unchanged} already present`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`✓ Hooks already registered (${unchanged} present)`);
|
||||||
|
}
|
||||||
|
console.log(dim(` config: ${CLAUDE_SETTINGS}`));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`⚠ hook registration failed: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
" (MCP is still installed — hooks just skip. Retry with --no-hooks to suppress.)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(dim("· Hooks skipped (--no-hooks)"));
|
||||||
|
}
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(yellow(bold("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear."));
|
console.log(yellow(bold("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear."));
|
||||||
console.log("");
|
console.log("");
|
||||||
@@ -193,21 +312,39 @@ export function runInstall(): void {
|
|||||||
export function runUninstall(): void {
|
export function runUninstall(): void {
|
||||||
console.log("claudemesh uninstall");
|
console.log("claudemesh uninstall");
|
||||||
console.log("--------------------");
|
console.log("--------------------");
|
||||||
if (!existsSync(CLAUDE_CONFIG)) {
|
|
||||||
console.log(`· no ${CLAUDE_CONFIG} — nothing to remove`);
|
// MCP entry
|
||||||
return;
|
if (existsSync(CLAUDE_CONFIG)) {
|
||||||
|
const cfg = readClaudeConfig();
|
||||||
|
const servers = cfg.mcpServers as
|
||||||
|
| Record<string, McpEntry>
|
||||||
|
| undefined;
|
||||||
|
if (servers && MCP_NAME in servers) {
|
||||||
|
delete servers[MCP_NAME];
|
||||||
|
cfg.mcpServers = servers;
|
||||||
|
writeClaudeConfig(cfg);
|
||||||
|
console.log(`✓ MCP server "${MCP_NAME}" removed`);
|
||||||
|
} else {
|
||||||
|
console.log(`· MCP server "${MCP_NAME}" not present`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`· no ${CLAUDE_CONFIG} — MCP entry skipped`);
|
||||||
}
|
}
|
||||||
const cfg = readClaudeConfig();
|
|
||||||
const servers = cfg.mcpServers as
|
// Hooks
|
||||||
| Record<string, McpEntry>
|
try {
|
||||||
| undefined;
|
const removed = uninstallHooks();
|
||||||
if (!servers || !(MCP_NAME in servers)) {
|
if (removed > 0) {
|
||||||
console.log(`· MCP server "${MCP_NAME}" not present — nothing to remove`);
|
console.log(`✓ Hooks removed (${removed} entries)`);
|
||||||
return;
|
} else {
|
||||||
|
console.log("· No claudemesh hooks to remove");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`⚠ hook removal failed: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
delete servers[MCP_NAME];
|
|
||||||
cfg.mcpServers = servers;
|
console.log("");
|
||||||
writeClaudeConfig(cfg);
|
console.log("Restart Claude Code to drop the MCP connection + hooks.");
|
||||||
console.log(`✓ MCP server "${MCP_NAME}" removed`);
|
|
||||||
console.log("Restart Claude Code to drop the MCP connection.");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { runJoin } from "./commands/join";
|
|||||||
import { runList } from "./commands/list";
|
import { runList } from "./commands/list";
|
||||||
import { runLeave } from "./commands/leave";
|
import { runLeave } from "./commands/leave";
|
||||||
import { runSeedTestMesh } from "./commands/seed-test-mesh";
|
import { runSeedTestMesh } from "./commands/seed-test-mesh";
|
||||||
|
import { runHook } from "./commands/hook";
|
||||||
|
|
||||||
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
||||||
|
|
||||||
@@ -21,8 +22,9 @@ Usage:
|
|||||||
claudemesh <command> [args]
|
claudemesh <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
install Register claudemesh as a Claude Code MCP server
|
install Register MCP + Stop/UserPromptSubmit status hooks
|
||||||
uninstall Remove claudemesh MCP server registration
|
(add --no-hooks for bare MCP registration)
|
||||||
|
uninstall Remove MCP server + hooks
|
||||||
join <url> Join a mesh via https://claudemesh.com/join/... URL
|
join <url> Join a mesh via https://claudemesh.com/join/... URL
|
||||||
list Show all joined meshes
|
list Show all joined meshes
|
||||||
leave <slug> Leave a joined mesh
|
leave <slug> Leave a joined mesh
|
||||||
@@ -45,11 +47,14 @@ async function main(): Promise<void> {
|
|||||||
await startMcpServer();
|
await startMcpServer();
|
||||||
return;
|
return;
|
||||||
case "install":
|
case "install":
|
||||||
runInstall();
|
runInstall(args);
|
||||||
return;
|
return;
|
||||||
case "uninstall":
|
case "uninstall":
|
||||||
runUninstall();
|
runUninstall();
|
||||||
return;
|
return;
|
||||||
|
case "hook":
|
||||||
|
await runHook(args);
|
||||||
|
return;
|
||||||
case "join":
|
case "join":
|
||||||
await runJoin(args);
|
await runJoin(args);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -82,14 +82,29 @@ export async function startMcpServer(): Promise<void> {
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.1.0" },
|
{ name: "claudemesh", version: "0.1.1" },
|
||||||
{
|
{
|
||||||
capabilities: { tools: {} },
|
capabilities: {
|
||||||
instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions.
|
experimental: { "claude/channel": {} },
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions on this machine and elsewhere.
|
||||||
|
|
||||||
Use these tools to coordinate with peers on demand. Respond promptly when you receive messages (they're like someone tapping your shoulder).
|
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.
|
||||||
|
|
||||||
Tools: send_message, list_peers, check_messages, set_summary, set_status.
|
Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with the same target (for direct messages the from_id is the sender's pubkey).
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
- list_peers: see joined meshes + their connection status
|
||||||
|
- send_message: send to a peer pubkey, 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)
|
||||||
|
|
||||||
|
Message priority:
|
||||||
|
- "now": delivered immediately regardless of recipient status (use sparingly)
|
||||||
|
- "next" (default): delivered when recipient is idle
|
||||||
|
- "low": pull-only (check_messages)
|
||||||
|
|
||||||
If you have multiple joined meshes, prefix the \`to\` argument of send_message with \`<mesh-slug>:\` to disambiguate. Otherwise claudemesh picks the single joined mesh.`,
|
If you have multiple joined meshes, prefix the \`to\` argument of send_message with \`<mesh-slug>:\` to disambiguate. Otherwise claudemesh picks the single joined mesh.`,
|
||||||
},
|
},
|
||||||
@@ -191,6 +206,39 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|
||||||
|
// Wire WSS pushes → MCP channel notifications. Each inbound push on
|
||||||
|
// any mesh's broker connection becomes a <channel source="claudemesh">
|
||||||
|
// system reminder injected into Claude Code's context.
|
||||||
|
for (const client of allClients()) {
|
||||||
|
client.onPush(async (msg) => {
|
||||||
|
const fromPubkey = msg.senderPubkey || "";
|
||||||
|
const fromName = fromPubkey
|
||||||
|
? `peer-${fromPubkey.slice(0, 8)}`
|
||||||
|
: "unknown";
|
||||||
|
const content = msg.plaintext ?? "(decryption failed)";
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content,
|
||||||
|
meta: {
|
||||||
|
from_id: fromPubkey,
|
||||||
|
from_name: fromName,
|
||||||
|
mesh_slug: client.meshSlug,
|
||||||
|
mesh_id: client.meshId,
|
||||||
|
priority: msg.priority,
|
||||||
|
sent_at: msg.createdAt,
|
||||||
|
delivered_at: msg.receivedAt,
|
||||||
|
kind: msg.kind,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* channel push is best-effort; check_messages is the fallback */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const shutdown = (): void => {
|
const shutdown = (): void => {
|
||||||
stopAll();
|
stopAll();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user