feat(plugin): ship Claude Code plugin + marketplace manifest
Adds a /plugin marketplace at the repo root and a `claude-mailbox` plugin under plugin/ that wires the UserPromptSubmit hook without needing the per-user `install-hook` step. The hook command (`claude-mailbox check --hook`) now reads the mailbox name from $CLAUDE_MAILBOX_NAME when --name is omitted and emits a one-line setup hint when the daemon is unreachable, so a missing daemon is loud instead of invisible. The plugin only contains the Claude Code glue — the daemon binary is still a separate prerequisite (`npm i -g @kuns/claude-mailbox` + install-autostart), and the plugin/README plus main README spell out the three-step setup.
This commit is contained in:
14
.claude-plugin/marketplace.json
Normal file
14
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "claude-mailbox",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mika Kuns"
|
||||||
|
},
|
||||||
|
"description": "Plugins for the Claude-Mailbox project.",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "claude-mailbox",
|
||||||
|
"source": "./plugin",
|
||||||
|
"description": "Auto-checks the local Claude-Mailbox daemon before every prompt and injects pending messages."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
README.md
22
README.md
@@ -104,6 +104,28 @@ claude-mailbox uninstall-service [--purge]
|
|||||||
|
|
||||||
The .NET and Node builds are wire-compatible (same port, same `X-Mailbox` header, same MCP tool names, same SQLite schema), so a `.mcp.json` configured against one works against the other.
|
The .NET and Node builds are wire-compatible (same port, same `X-Mailbox` header, same MCP tool names, same SQLite schema), so a `.mcp.json` configured against one works against the other.
|
||||||
|
|
||||||
|
## Use from Claude Code (plugin)
|
||||||
|
|
||||||
|
Easiest path for colleagues — install the marketplace once, then the plugin:
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox
|
||||||
|
/plugin install claude-mailbox@claude-mailbox
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin only wires the `UserPromptSubmit` hook. **The daemon binary is a separate prerequisite** — install it via the npm package above (or the bootstrap one-liner) and run `claude-mailbox install-autostart` so it starts on boot.
|
||||||
|
|
||||||
|
Then set a per-machine mailbox name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
setx CLAUDE_MAILBOX_NAME alice # Windows
|
||||||
|
export CLAUDE_MAILBOX_NAME=alice # macOS / Linux (in ~/.zshrc or ~/.bashrc)
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Claude Code and unread messages will appear in context before every prompt. If the daemon is not reachable, the hook emits a one-line setup hint instead of staying silent — so a missing daemon is loud, not invisible.
|
||||||
|
|
||||||
|
See [`plugin/README.md`](./plugin/README.md) for the full walkthrough.
|
||||||
|
|
||||||
## Use from a Claude session
|
## Use from a Claude session
|
||||||
|
|
||||||
Drop this into your project's `.mcp.json` (one per session, different `X-Mailbox` values):
|
Drop this into your project's `.mcp.json` (one per session, different `X-Mailbox` values):
|
||||||
|
|||||||
@@ -127,29 +127,45 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command("check")
|
.command("check")
|
||||||
.description("Pull pending messages and mark delivered.")
|
.description(
|
||||||
.requiredOption("--name <name>", "Mailbox name (also sent as X-Mailbox)")
|
"Pull pending messages and mark delivered. In --hook mode the name can come from CLAUDE_MAILBOX_NAME.",
|
||||||
|
)
|
||||||
|
.option("--name <name>", "Mailbox name (also sent as X-Mailbox). Falls back to $CLAUDE_MAILBOX_NAME.")
|
||||||
.option("--url <url>", "Daemon base URL", DEFAULT_URL)
|
.option("--url <url>", "Daemon base URL", DEFAULT_URL)
|
||||||
.option(
|
.option(
|
||||||
"--hook",
|
"--hook",
|
||||||
"Hook mode: human-readable output, silent on empty inbox or unreachable daemon (exit 0)",
|
"Hook mode: human-readable output, silent when no mailbox configured or daemon unreachable (exit 0). Emits a one-line setup hint when name is set but daemon is unreachable.",
|
||||||
)
|
)
|
||||||
.action(async (opts: { name: string; url: string; hook?: boolean }) => {
|
.action(async (opts: { name?: string; url: string; hook?: boolean }) => {
|
||||||
|
const name = (opts.name ?? process.env["CLAUDE_MAILBOX_NAME"] ?? "").trim();
|
||||||
|
if (!name) {
|
||||||
|
if (opts.hook) return;
|
||||||
|
console.error("Missing --name (or set CLAUDE_MAILBOX_NAME).");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const out = await callJson(
|
const out = await callJson(
|
||||||
"POST",
|
"POST",
|
||||||
`${opts.url}/v1/check-inbox?name=${encodeURIComponent(opts.name)}`,
|
`${opts.url}/v1/check-inbox?name=${encodeURIComponent(name)}`,
|
||||||
{ headers: { "X-Mailbox": opts.name } },
|
{ headers: { "X-Mailbox": name } },
|
||||||
);
|
);
|
||||||
if (opts.hook) {
|
if (opts.hook) {
|
||||||
const messages = (Array.isArray(out) ? out : []) as HookMessage[];
|
const messages = (Array.isArray(out) ? out : []) as HookMessage[];
|
||||||
const text = formatMessagesForHook(opts.name, messages);
|
const text = formatMessagesForHook(name, messages);
|
||||||
if (text) process.stdout.write(text);
|
if (text) process.stdout.write(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(JSON.stringify(out, null, 2));
|
console.log(JSON.stringify(out, null, 2));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (opts.hook) return;
|
if (opts.hook) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
if (/ECONNREFUSED|fetch failed|ENOTFOUND|connect/i.test(msg)) {
|
||||||
|
process.stdout.write(
|
||||||
|
`[Claude-Mailbox] Daemon not reachable at ${opts.url}. Run \`claude-mailbox install-autostart\` (one-time) or \`claude-mailbox start\`.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportClientError(err, opts.url);
|
reportClientError(err, opts.url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
63
node/tests/cli-hook.test.ts
Normal file
63
node/tests/cli-hook.test.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const cliPath = resolve(__dirname, "..", "dist", "cli.js");
|
||||||
|
|
||||||
|
function runCli(args: string[], env: Record<string, string | undefined> = {}): {
|
||||||
|
status: number;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
} {
|
||||||
|
const r = spawnSync(process.execPath, [cliPath, ...args], {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: r.status ?? -1,
|
||||||
|
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
||||||
|
stderr: typeof r.stderr === "string" ? r.stderr : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("`check --hook` CLI behavior", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
if (!existsSync(cliPath)) {
|
||||||
|
throw new Error(`CLI not built. Run \`npm run build\` first. Missing: ${cliPath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exits 0 silently when no name resolved (no --name, no env)", () => {
|
||||||
|
const r = runCli(["check", "--hook"], { CLAUDE_MAILBOX_NAME: undefined });
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(r.stdout).toBe("");
|
||||||
|
expect(r.stderr).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits daemon-not-reachable hint when name is set but daemon is down", () => {
|
||||||
|
const r = runCli(
|
||||||
|
["check", "--hook", "--url", "http://127.0.0.1:1"],
|
||||||
|
{ CLAUDE_MAILBOX_NAME: "alice" },
|
||||||
|
);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable");
|
||||||
|
expect(r.stdout).toContain("http://127.0.0.1:1");
|
||||||
|
expect(r.stdout).toContain("claude-mailbox install-autostart");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("`--name` arg wins over CLAUDE_MAILBOX_NAME env (visible via hint URL/contents)", () => {
|
||||||
|
const r = runCli(
|
||||||
|
["check", "--hook", "--name", "bob", "--url", "http://127.0.0.1:1"],
|
||||||
|
{ CLAUDE_MAILBOX_NAME: "alice" },
|
||||||
|
);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-hook mode errors out when no name resolved", () => {
|
||||||
|
const r = runCli(["check"], { CLAUDE_MAILBOX_NAME: undefined });
|
||||||
|
expect(r.status).not.toBe(0);
|
||||||
|
expect(r.stderr).toContain("CLAUDE_MAILBOX_NAME");
|
||||||
|
});
|
||||||
|
});
|
||||||
11
plugin/.claude-plugin/plugin.json
Normal file
11
plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "claude-mailbox",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Auto-checks the local Claude-Mailbox daemon before every prompt and injects pending messages into the conversation context.",
|
||||||
|
"author": {
|
||||||
|
"name": "Mika Kuns"
|
||||||
|
},
|
||||||
|
"homepage": "https://git.kuns.dev/releases/ClaudeMailbox",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": ["mailbox", "ipc", "coordination", "mcp"]
|
||||||
|
}
|
||||||
68
plugin/README.md
Normal file
68
plugin/README.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# claude-mailbox plugin
|
||||||
|
|
||||||
|
Lets Claude Code automatically pull unread messages from a local `claude-mailbox` daemon before every prompt and inject them into the conversation context.
|
||||||
|
|
||||||
|
## Three-step setup
|
||||||
|
|
||||||
|
The Claude Code plugin itself is just the glue — you still need the daemon binary on PATH and a mailbox name.
|
||||||
|
|
||||||
|
### 1. Install the daemon (one-time, per machine)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm config set @kuns:registry=https://git.kuns.dev/api/packages/releases/npm/
|
||||||
|
npm install -g @kuns/claude-mailbox
|
||||||
|
claude-mailbox install-autostart # registers per-OS autostart (no admin needed by default)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude-mailbox status # expect: Running
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install the plugin
|
||||||
|
|
||||||
|
In Claude Code:
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox
|
||||||
|
/plugin install claude-mailbox@claude-mailbox
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Choose a mailbox name
|
||||||
|
|
||||||
|
Set the env var Claude Code will inherit (any unique name — your username, machine name, whatever):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Windows (PowerShell, persistent)
|
||||||
|
setx CLAUDE_MAILBOX_NAME alice
|
||||||
|
|
||||||
|
# macOS / Linux (add to ~/.zshrc or ~/.bashrc)
|
||||||
|
export CLAUDE_MAILBOX_NAME=alice
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Claude Code so the new env var is picked up.
|
||||||
|
|
||||||
|
## What happens at runtime
|
||||||
|
|
||||||
|
Before every prompt the plugin runs `claude-mailbox check --hook`, which:
|
||||||
|
|
||||||
|
- prints unread mailbox messages in a Claude-friendly format and marks them delivered,
|
||||||
|
- stays **silent** when the inbox is empty or `CLAUDE_MAILBOX_NAME` is not set,
|
||||||
|
- emits a one-line setup hint when the daemon is unreachable (so missing step 1 is loud).
|
||||||
|
|
||||||
|
## Sending yourself a message (smoke test)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude-mailbox send --from probe --to alice --body "hello from the CLI"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start a new Claude Code session — the message should appear in context before Claude's first reply.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin uninstall claude-mailbox@claude-mailbox
|
||||||
|
npm uninstall -g @kuns/claude-mailbox
|
||||||
|
claude-mailbox uninstall-autostart # if you used it
|
||||||
|
```
|
||||||
14
plugin/hooks/hooks.json
Normal file
14
plugin/hooks/hooks.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "claude-mailbox check --hook"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user