feat(cli): v0.5.1 — message modes (push/inbox/off)
Some checks failed
Some checks failed
--inbox: count-only notifications, no content in context --no-messages: tools only, zero prompt injection risk Default: push (real-time, current behavior) Wizard shows mode picker when no flag provided. MCP instructions tell Claude its current mode. 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.5.0",
|
"version": "0.5.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",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface LaunchArgs {
|
|||||||
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
||||||
joinLink: string | null;
|
joinLink: string | null;
|
||||||
meshSlug: string | null;
|
meshSlug: string | null;
|
||||||
|
messageMode: "push" | "inbox" | "off" | null;
|
||||||
quiet: boolean;
|
quiet: boolean;
|
||||||
skipPermConfirm: boolean;
|
skipPermConfirm: boolean;
|
||||||
claudeArgs: string[];
|
claudeArgs: string[];
|
||||||
@@ -38,6 +39,7 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
groups: null,
|
groups: null,
|
||||||
joinLink: null,
|
joinLink: null,
|
||||||
meshSlug: null,
|
meshSlug: null,
|
||||||
|
messageMode: null,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
skipPermConfirm: false,
|
skipPermConfirm: false,
|
||||||
claudeArgs: [],
|
claudeArgs: [],
|
||||||
@@ -66,6 +68,10 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
result.meshSlug = argv[++i]!;
|
result.meshSlug = argv[++i]!;
|
||||||
} else if (arg.startsWith("--mesh=")) {
|
} else if (arg.startsWith("--mesh=")) {
|
||||||
result.meshSlug = arg.slice("--mesh=".length);
|
result.meshSlug = arg.slice("--mesh=".length);
|
||||||
|
} else if (arg === "--inbox") {
|
||||||
|
result.messageMode = "inbox";
|
||||||
|
} else if (arg === "--no-messages") {
|
||||||
|
result.messageMode = "off";
|
||||||
} else if (arg === "--quiet") {
|
} else if (arg === "--quiet") {
|
||||||
result.quiet = true;
|
result.quiet = true;
|
||||||
} else if (arg === "-y" || arg === "--yes") {
|
} else if (arg === "-y" || arg === "--yes") {
|
||||||
@@ -171,7 +177,7 @@ async function confirmPermissions(): Promise<void> {
|
|||||||
|
|
||||||
// --- Banner ---
|
// --- Banner ---
|
||||||
|
|
||||||
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[]): void {
|
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[], messageMode: "push" | "inbox" | "off"): void {
|
||||||
const useColor =
|
const useColor =
|
||||||
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
@@ -183,9 +189,15 @@ function printBanner(name: string, meshSlug: string, role: string | null, groups
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const rule = "─".repeat(60);
|
const rule = "─".repeat(60);
|
||||||
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags}`));
|
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags} [${messageMode}]`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
console.log("Peer messages arrive as <channel> reminders in real-time.");
|
if (messageMode === "push") {
|
||||||
|
console.log("Peer messages arrive as <channel> reminders in real-time.");
|
||||||
|
} else if (messageMode === "inbox") {
|
||||||
|
console.log("Peer messages held in inbox. Use check_messages to read.");
|
||||||
|
} else {
|
||||||
|
console.log("Messages off. Use check_messages to poll manually.");
|
||||||
|
}
|
||||||
console.log("Peers send text only — they cannot call tools or read files.");
|
console.log("Peers send text only — they cannot call tools or read files.");
|
||||||
console.log(dim(`Config: ${getConfigPath()}`));
|
console.log(dim(`Config: ${getConfigPath()}`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
@@ -263,6 +275,8 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
let role: string | null = args.role;
|
let role: string | null = args.role;
|
||||||
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
||||||
|
|
||||||
|
let messageMode: "push" | "inbox" | "off" = args.messageMode ?? "push";
|
||||||
|
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
if (role === null) {
|
if (role === null) {
|
||||||
const answer = await askLine(" Role (optional): ");
|
const answer = await askLine(" Role (optional): ");
|
||||||
@@ -272,6 +286,18 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
const answer = await askLine(" Groups (comma-separated, optional): ");
|
const answer = await askLine(" Groups (comma-separated, optional): ");
|
||||||
if (answer) parsedGroups = parseGroupsString(answer);
|
if (answer) parsedGroups = parseGroupsString(answer);
|
||||||
}
|
}
|
||||||
|
if (args.messageMode === null) {
|
||||||
|
console.log("\n Message mode:");
|
||||||
|
console.log(" 1) Push (real-time, peers can interrupt your work)");
|
||||||
|
console.log(" 2) Inbox (held until you check, notification only)");
|
||||||
|
console.log(" 3) Off (tools only, no messages)");
|
||||||
|
console.log("");
|
||||||
|
const answer = await askLine(" Choice [1]: ");
|
||||||
|
const choice = parseInt(answer || "1", 10);
|
||||||
|
if (choice === 2) messageMode = "inbox";
|
||||||
|
else if (choice === 3) messageMode = "off";
|
||||||
|
else messageMode = "push";
|
||||||
|
}
|
||||||
if (role || parsedGroups.length) console.log("");
|
if (role || parsedGroups.length) console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +319,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
meshes: [mesh],
|
meshes: [mesh],
|
||||||
displayName,
|
displayName,
|
||||||
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
||||||
|
messageMode,
|
||||||
};
|
};
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(tmpDir, "config.json"),
|
join(tmpDir, "config.json"),
|
||||||
@@ -302,7 +329,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
|
|
||||||
// 5. Banner + permission confirmation.
|
// 5. Banner + permission confirmation.
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
printBanner(displayName, mesh.slug, role, parsedGroups);
|
printBanner(displayName, mesh.slug, role, parsedGroups, messageMode);
|
||||||
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
||||||
if (!args.skipPermConfirm) {
|
if (!args.skipPermConfirm) {
|
||||||
await confirmPermissions();
|
await confirmPermissions();
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export async function startMcpServer(): Promise<void> {
|
|||||||
|
|
||||||
const myName = config.displayName ?? "unnamed";
|
const myName = config.displayName ?? "unnamed";
|
||||||
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
|
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
|
||||||
|
const messageMode = config.messageMode ?? "push";
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.3.0" },
|
{ name: "claudemesh", version: "0.3.0" },
|
||||||
@@ -236,7 +237,13 @@ Create and claim work items. create_task to propose work, claim_task to take own
|
|||||||
- "low": pull-only via check_messages (FYI, non-blocking context)
|
- "low": pull-only via check_messages (FYI, non-blocking context)
|
||||||
|
|
||||||
## Coordination
|
## Coordination
|
||||||
Call list_peers at session start to understand who is online, their roles, and what they are working on. If you are a group lead, gather input from members before responding to external requests — do not answer alone. If you are a member, contribute to your lead when asked. Use @group messages for team-wide questions, direct messages for 1:1 coordination. Set a meaningful summary so peers know your current focus.`,
|
Call list_peers at session start to understand who is online, their roles, and what they are working on. If you are a group lead, gather input from members before responding to external requests — do not answer alone. If you are a member, contribute to your lead when asked. Use @group messages for team-wide questions, direct messages for 1:1 coordination. Set a meaningful summary so peers know your current focus.
|
||||||
|
|
||||||
|
## Message Mode
|
||||||
|
Your message mode is "${messageMode}".
|
||||||
|
- push: messages arrive in real-time as channel notifications. Respond immediately.
|
||||||
|
- inbox: messages are held. You'll see "[inbox] New message from X" notifications. Call check_messages to read them.
|
||||||
|
- off: no message notifications. Use check_messages manually to poll.`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -716,11 +723,31 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
// system reminder injected into Claude Code's context.
|
// system reminder injected into Claude Code's context.
|
||||||
for (const client of allClients()) {
|
for (const client of allClients()) {
|
||||||
client.onPush(async (msg) => {
|
client.onPush(async (msg) => {
|
||||||
|
// In "off" mode, silently skip notification — messages are still
|
||||||
|
// buffered in pushBuffer and accessible via check_messages.
|
||||||
|
if (messageMode === "off") return;
|
||||||
|
|
||||||
const fromPubkey = msg.senderPubkey || "";
|
const fromPubkey = msg.senderPubkey || "";
|
||||||
// Resolve sender's display name from the cached peer list.
|
// Resolve sender's display name from the cached peer list.
|
||||||
const fromName = fromPubkey
|
const fromName = fromPubkey
|
||||||
? await resolvePeerName(client, fromPubkey)
|
? await resolvePeerName(client, fromPubkey)
|
||||||
: "unknown";
|
: "unknown";
|
||||||
|
|
||||||
|
if (messageMode === "inbox") {
|
||||||
|
// Count-only notification, no content
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[inbox] New message from ${fromName}. Use check_messages to read.`,
|
||||||
|
meta: { kind: "inbox_notification", from_name: fromName },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push mode — full content notification
|
||||||
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
||||||
try {
|
try {
|
||||||
await server.notification({
|
await server.notification({
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Config {
|
|||||||
meshes: JoinedMesh[];
|
meshes: JoinedMesh[];
|
||||||
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
||||||
groups?: GroupEntry[];
|
groups?: GroupEntry[];
|
||||||
|
messageMode?: "push" | "inbox" | "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||||
@@ -53,7 +54,7 @@ export function loadConfig(): Config {
|
|||||||
if (!parsed || !Array.isArray(parsed.meshes)) {
|
if (!parsed || !Array.isArray(parsed.meshes)) {
|
||||||
return { version: 1, meshes: [] };
|
return { version: 1, meshes: [] };
|
||||||
}
|
}
|
||||||
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups };
|
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups, messageMode: parsed.messageMode };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user