feat(cli): scaffold @claudemesh/cli MCP client package (stubs)
The user-facing tool. Two invocation modes:
- `claudemesh mcp` → MCP server (stdio), consumed by Claude Code
- `claudemesh <subcommand>` → human CLI
Layout:
apps/cli/
├── package.json bin: { claudemesh: ./src/index.ts }
├── README.md install + usage
└── src/
├── index.ts dispatcher (mcp | install | join | list | leave | --help)
├── env.ts CLAUDEMESH_BROKER_URL, CONFIG_DIR, DEBUG
├── mcp/
│ ├── server.ts MCP stdio server with 5 tools
│ ├── tools.ts tool schemas (send_message, list_peers,
│ │ check_messages, set_summary, set_status)
│ └── types.ts
├── ws/client.ts broker connection (stub for 15b)
├── state/config.ts ~/.claudemesh/config.json (joined meshes + keys)
└── commands/
├── install.ts print `claude mcp add ...` instruction
├── join.ts parse ic://join/... (stub, Step 17)
├── list.ts show joined meshes
└── leave.ts remove mesh from local config
Tool stubs return "not connected, run `claudemesh join <invite-link>`"
errors until 15b wires the WS client.
Verified:
- `bun src/index.ts --help` → prints usage
- `bun src/index.ts install` → prints MCP add command with resolved path
- `bun src/index.ts list` → "No meshes joined yet"
- `bun src/index.ts mcp` (via stdin) → returns tools/list with all 5 tools
Deps: @modelcontextprotocol/sdk, ws, libsodium-wrappers, zod.
Lockfile regenerated in the same commit per claudemesh-3's flag —
avoids breaking Coolify's --frozen-lockfile deploys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
36
apps/cli/src/commands/install.ts
Normal file
36
apps/cli/src/commands/install.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* `claudemesh install` — print Claude Code MCP registration instructions.
|
||||
*
|
||||
* In the v1 flow, users copy-paste a `claude mcp add ...` command.
|
||||
* Later we'll auto-write the MCP entry to ~/.claude.json and hooks
|
||||
* to ~/.claude/settings.json (mirroring claude-intercom's installer).
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
export function runInstall(): void {
|
||||
// Resolve the path to this package's own index.ts so the generated
|
||||
// command points at the right binary even when installed globally.
|
||||
const here = fileURLToPath(import.meta.url);
|
||||
const entry = resolve(dirname(here), "..", "index.ts");
|
||||
|
||||
console.log("claudemesh — MCP registration");
|
||||
console.log("------------------------------");
|
||||
console.log("");
|
||||
console.log("Register the MCP server with Claude Code:");
|
||||
console.log("");
|
||||
console.log(` claude mcp add claudemesh --scope user -- bun ${entry} mcp`);
|
||||
console.log("");
|
||||
console.log("Or if installed globally:");
|
||||
console.log("");
|
||||
console.log(` claude mcp add claudemesh --scope user -- claudemesh mcp`);
|
||||
console.log("");
|
||||
console.log(
|
||||
"After registering, restart Claude Code. Then join a mesh with:",
|
||||
);
|
||||
console.log("");
|
||||
console.log(" claudemesh join <invite-link>");
|
||||
console.log("");
|
||||
console.log("(Auto-install of hooks + MCP entry will ship in a later step.)");
|
||||
}
|
||||
30
apps/cli/src/commands/join.ts
Normal file
30
apps/cli/src/commands/join.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* `claudemesh join <invite-link>` — parse a mesh invite link and
|
||||
* join the mesh.
|
||||
*
|
||||
* STUB: real invite-link parsing + keypair generation + broker
|
||||
* enrollment lands in Step 17. For now this just validates the link
|
||||
* shape and tells the user what's coming.
|
||||
*/
|
||||
|
||||
export function runJoin(args: string[]): void {
|
||||
const link = args[0];
|
||||
if (!link) {
|
||||
console.error("Usage: claudemesh join <invite-link>");
|
||||
console.error("");
|
||||
console.error("Example: claudemesh join ic://join/BASE64URL...");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!link.startsWith("ic://join/")) {
|
||||
console.error(
|
||||
`claudemesh: invalid invite link. Expected ic://join/... got "${link}"`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("claudemesh: join not yet implemented (Step 17).");
|
||||
console.log(` Invite link parsed: ${link.slice(0, 40)}...`);
|
||||
console.log(
|
||||
" Real flow will: verify sig, generate keypair, enroll member, persist to ~/.claudemesh/config.json",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
25
apps/cli/src/commands/leave.ts
Normal file
25
apps/cli/src/commands/leave.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* `claudemesh leave <slug>` — remove a mesh from local config.
|
||||
*
|
||||
* Does NOT (yet) notify the broker. In 15b+ this will send a
|
||||
* best-effort revoke request before removing the entry.
|
||||
*/
|
||||
|
||||
import { loadConfig, saveConfig } from "../state/config";
|
||||
|
||||
export function runLeave(args: string[]): void {
|
||||
const slug = args[0];
|
||||
if (!slug) {
|
||||
console.error("Usage: claudemesh leave <slug>");
|
||||
process.exit(1);
|
||||
}
|
||||
const config = loadConfig();
|
||||
const before = config.meshes.length;
|
||||
config.meshes = config.meshes.filter((m) => m.slug !== slug);
|
||||
if (config.meshes.length === before) {
|
||||
console.error(`claudemesh: no joined mesh with slug "${slug}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
saveConfig(config);
|
||||
console.log(`Left mesh "${slug}". Remaining: ${config.meshes.length}`);
|
||||
}
|
||||
28
apps/cli/src/commands/list.ts
Normal file
28
apps/cli/src/commands/list.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* `claudemesh list` — show all joined meshes + their status.
|
||||
*/
|
||||
|
||||
import { loadConfig, getConfigPath } from "../state/config";
|
||||
|
||||
export function runList(): void {
|
||||
const config = loadConfig();
|
||||
if (config.meshes.length === 0) {
|
||||
console.log("No meshes joined yet.");
|
||||
console.log("");
|
||||
console.log("Join one with: claudemesh join <invite-link>");
|
||||
console.log(`Config file: ${getConfigPath()}`);
|
||||
return;
|
||||
}
|
||||
console.log(`Joined meshes (${config.meshes.length}):`);
|
||||
console.log("");
|
||||
for (const m of config.meshes) {
|
||||
console.log(` ${m.name} (${m.slug})`);
|
||||
console.log(` mesh id: ${m.meshId}`);
|
||||
console.log(` member id: ${m.memberId}`);
|
||||
console.log(` pubkey: ${m.pubkey.slice(0, 16)}…`);
|
||||
console.log(` broker: ${m.brokerUrl}`);
|
||||
console.log(` joined: ${m.joinedAt}`);
|
||||
console.log("");
|
||||
}
|
||||
console.log(`Config: ${getConfigPath()}`);
|
||||
}
|
||||
Reference in New Issue
Block a user