@rayhanadev/opencode-plugin-mailbox 0.0.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.oxfmtrc.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "printWidth": 80,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "semi": true,
7
+ "singleQuote": false,
8
+ "trailingComma": "all",
9
+ "arrowParens": "always",
10
+ "experimentalSortImports": {
11
+ "order": "asc",
12
+ "ignoreCase": true,
13
+ "newlinesBetween": true,
14
+ "internalPattern": ["@/**", "~/**"]
15
+ },
16
+ "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/*.min.js"]
17
+ }
package/.oxlintrc.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["typescript", "import"],
4
+ "env": {
5
+ "browser": true,
6
+ "node": true
7
+ },
8
+ "rules": {
9
+ "typescript/no-explicit-any": "error",
10
+ "typescript/consistent-type-imports": "error",
11
+ "import/no-cycle": "error"
12
+ },
13
+ "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/*.min.js"]
14
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,105 @@
1
+ Default to using Bun instead of Node.js.
2
+
3
+ - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
4
+ - Use `bun test` instead of `jest` or `vitest`
5
+ - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
6
+ - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
7
+ - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
8
+ - Use `bunx <package> <command>` instead of `npx <package> <command>`
9
+ - Bun automatically loads .env, so don't use dotenv.
10
+
11
+ ## APIs
12
+
13
+ - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
14
+ - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
15
+ - `Bun.redis` for Redis. Don't use `ioredis`.
16
+ - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
17
+ - `WebSocket` is built-in. Don't use `ws`.
18
+ - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
19
+ - Bun.$`ls` instead of execa.
20
+
21
+ ## Testing
22
+
23
+ Use `bun test` to run tests.
24
+
25
+ ```ts#index.test.ts
26
+ import { test, expect } from "bun:test";
27
+
28
+ test("hello world", () => {
29
+ expect(1).toBe(1);
30
+ });
31
+ ```
32
+
33
+ ## Frontend
34
+
35
+ Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
36
+
37
+ Server:
38
+
39
+ ```ts#index.ts
40
+ import index from "./index.html"
41
+
42
+ Bun.serve({
43
+ routes: {
44
+ "/": index,
45
+ "/api/users/:id": {
46
+ GET: (req) => {
47
+ return new Response(JSON.stringify({ id: req.params.id }));
48
+ },
49
+ },
50
+ },
51
+ // optional websocket support
52
+ websocket: {
53
+ open: (ws) => {
54
+ ws.send("Hello, world!");
55
+ },
56
+ message: (ws, message) => {
57
+ ws.send(message);
58
+ },
59
+ close: (ws) => {
60
+ // handle close
61
+ }
62
+ },
63
+ development: {
64
+ hmr: true,
65
+ console: true,
66
+ }
67
+ })
68
+ ```
69
+
70
+ HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
71
+
72
+ ```html#index.html
73
+ <html>
74
+ <body>
75
+ <h1>Hello, world!</h1>
76
+ <script type="module" src="./frontend.tsx"></script>
77
+ </body>
78
+ </html>
79
+ ```
80
+
81
+ With the following `frontend.tsx`:
82
+
83
+ ```tsx#frontend.tsx
84
+ import React from "react";
85
+ import { createRoot } from "react-dom/client";
86
+
87
+ // import .css files directly and it works
88
+ import './index.css';
89
+
90
+ const root = createRoot(document.body);
91
+
92
+ export default function Frontend() {
93
+ return <h1>Hello, world!</h1>;
94
+ }
95
+
96
+ root.render(<Frontend />);
97
+ ```
98
+
99
+ Then, run index.ts
100
+
101
+ ```sh
102
+ bun --hot ./index.ts
103
+ ```
104
+
105
+ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rayhan Noufal Arayilakath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # opencode-plugin-mailbox
2
+
3
+ Inter-agent communication plugin for [OpenCode](https://github.com/opencode-ai/opencode). Enables real-time messaging between running agent sessions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun install
9
+ ```
10
+
11
+ Add to your OpenCode config (`~/.config/opencode/config.json`):
12
+
13
+ ```json
14
+ {
15
+ "plugins": [
16
+ "@rayhanadev/opencode-plugin-mailbox"
17
+ ]
18
+ }
19
+ ```
20
+
21
+ ## Tools
22
+
23
+ ### `mailbox_register`
24
+
25
+ Register your session to enable messaging. Required before sending or receiving.
26
+
27
+ ```
28
+ mailbox_register description="Working on API integration"
29
+ ```
30
+
31
+ ### `mailbox_list`
32
+
33
+ List all registered sessions available for communication.
34
+
35
+ ```
36
+ mailbox_list
37
+ ```
38
+
39
+ Output:
40
+
41
+ ```
42
+ | Session ID | Directory | Description | Last Seen |
43
+ | ------------ | ---------------- | ------------------------ | --------- |
44
+ | ses_a1b2c3d4 | /path/to/project | Working on API integration | just now |
45
+ | ses_x7y8z9w0 | /path/to/project | (you) | - |
46
+ ```
47
+
48
+ ### `mailbox_send`
49
+
50
+ Send a message to another session. Delivered in real-time.
51
+
52
+ ```
53
+ mailbox_send to="ses_a1b" message="Found the authentication docs you needed"
54
+ ```
55
+
56
+ Session IDs support partial matching (minimum 6 characters).
57
+
58
+ ### `mailbox_reply`
59
+
60
+ Reply to the last session that messaged you.
61
+
62
+ ```
63
+ mailbox_reply message="Thanks, implementing now!"
64
+ ```
65
+
66
+ ### `mailbox_check`
67
+
68
+ Check your registration status and who last messaged you.
69
+
70
+ ```
71
+ mailbox_check
72
+ ```
73
+
74
+ ## How It Works
75
+
76
+ 1. Each session registers with `mailbox_register`
77
+ 2. Sessions discover each other via `mailbox_list`
78
+ 3. Messages are delivered in real-time using OpenCode's `session.prompt()` API
79
+ 4. Recipients see messages injected directly into their chat
80
+ 5. Sessions are automatically cleaned up when they end
81
+
82
+ ## Message Format
83
+
84
+ When you receive a message, it appears as:
85
+
86
+ ```
87
+ 📬 MAILBOX
88
+ From: ses_a1b2c3d4 | 2:34:15 PM
89
+ ────────────────────────────────
90
+
91
+ Here's the API documentation you requested...
92
+
93
+ ────────────────────────────────
94
+ Use mailbox_reply to respond
95
+ ```
96
+
97
+ ## Registry
98
+
99
+ Session data is stored at `~/.opencode/plugins/mailbox/registry.json`. This enables:
100
+
101
+ - Discovery across all OpenCode projects (global scope)
102
+ - Persistence across plugin restarts
103
+ - Reply context tracking
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ bun run typecheck
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@rayhanadev/opencode-plugin-mailbox",
3
+ "version": "0.0.1-beta.1",
4
+ "description": "Inter-agent communication plugin for OpenCode - enables real-time messaging between running agent sessions",
5
+ "homepage": "https://github.com/rayhanadev/opencode-plugin-mailbox#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/rayhanadev/opencode-plugin-mailbox/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/rayhanadev/opencode-plugin-mailbox.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Rayhan Noufal Arayilakath <me@rayhanadev.com>",
15
+ "type": "module",
16
+ "main": "src/index.ts",
17
+ "scripts": {
18
+ "lint": "oxlint --type-aware",
19
+ "format": "oxfmt",
20
+ "typecheck": "tsc --noEmit",
21
+ "build": "bun build src/index.ts --outdir dist --target node"
22
+ },
23
+ "dependencies": {
24
+ "@opencode-ai/plugin": "^1.0.221"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "latest",
28
+ "oxfmt": "^0.21.0",
29
+ "oxlint": "^1.36.0",
30
+ "oxlint-tsgolint": "^0.10.0"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^5"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ }
39
+ }
package/src/format.ts ADDED
@@ -0,0 +1,59 @@
1
+ export function formatIncomingMessage(
2
+ fromSessionId: string,
3
+ content: string,
4
+ ): string {
5
+ const time = new Date().toLocaleTimeString();
6
+ return `📬 **MAILBOX**
7
+ From: ${fromSessionId} | ${time}
8
+ ────────────────────────────────
9
+
10
+ ${content}
11
+
12
+ ────────────────────────────────
13
+ Use mailbox_reply to respond`;
14
+ }
15
+
16
+ export function formatSessionTable(
17
+ sessions: Array<{
18
+ sessionId: string;
19
+ projectID: string;
20
+ directory: string;
21
+ description?: string;
22
+ lastSeen: number;
23
+ }>,
24
+ currentSessionId: string,
25
+ ): string {
26
+ if (sessions.length === 0) {
27
+ return "No registered sessions. Use mailbox_register to register.";
28
+ }
29
+
30
+ const header = "| Session ID | Project | Directory | Description | Status |";
31
+ const separator =
32
+ "| ---------- | ------- | --------- | ----------- | ------ |";
33
+
34
+ const rows = sessions.map((s) => {
35
+ const isYou = s.sessionId === currentSessionId;
36
+ const truncatedId = s.sessionId.slice(0, 12) + "...";
37
+ const truncatedProject = truncate(s.projectID, 15);
38
+ const truncatedDir = truncate(s.directory.split("/").pop() || "", 15);
39
+ const description = s.description || "-";
40
+ const status = isYou ? "(you)" : formatLastSeen(s.lastSeen);
41
+
42
+ return `| ${truncatedId} | ${truncatedProject} | ${truncatedDir} | ${description} | ${status} |`;
43
+ });
44
+
45
+ return [header, separator, ...rows].join("\n");
46
+ }
47
+
48
+ function truncate(str: string, maxLen: number): string {
49
+ if (str.length <= maxLen) return str;
50
+ return str.slice(0, maxLen - 3) + "...";
51
+ }
52
+
53
+ function formatLastSeen(timestamp: number): string {
54
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
55
+ if (seconds < 60) return "active";
56
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
57
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
58
+ return `${Math.floor(seconds / 86400)}d ago`;
59
+ }
@@ -0,0 +1,19 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ import { unregisterSession, updateLastSeen } from "../registry";
4
+
5
+ export function createEventHook(_ctx: PluginInput) {
6
+ return async ({
7
+ event,
8
+ }: {
9
+ event: { type: string; properties?: Record<string, unknown> };
10
+ }) => {
11
+ if (event.type === "session.deleted" && event.properties?.sessionID) {
12
+ await unregisterSession(event.properties.sessionID as string);
13
+ }
14
+
15
+ if (event.type === "session.idle" && event.properties?.sessionID) {
16
+ await updateLastSeen(event.properties.sessionID as string);
17
+ }
18
+ };
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+
3
+ import { createEventHook } from "./hooks/events";
4
+ import { createCheckTool } from "./tools/check";
5
+ import { createListTool } from "./tools/list";
6
+ import { createRegisterTool } from "./tools/register";
7
+ import { createReplyTool } from "./tools/reply";
8
+ import { createSendTool } from "./tools/send";
9
+
10
+ export const MailboxPlugin: Plugin = async (ctx) => {
11
+ return {
12
+ tool: {
13
+ mailbox_register: createRegisterTool(ctx),
14
+ mailbox_list: createListTool(),
15
+ mailbox_send: createSendTool(ctx),
16
+ mailbox_reply: createReplyTool(ctx),
17
+ mailbox_check: createCheckTool(),
18
+ },
19
+ event: createEventHook(ctx),
20
+ };
21
+ };
22
+
23
+ export default MailboxPlugin;
@@ -0,0 +1,205 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import {
6
+ type MailboxRegistry,
7
+ type SessionEntry,
8
+ createEmptyRegistry,
9
+ } from "./types";
10
+
11
+ const REGISTRY_DIR = join(homedir(), ".opencode", "plugins", "mailbox");
12
+ const REGISTRY_PATH = join(REGISTRY_DIR, "registry.json");
13
+ const LOCK_PATH = join(REGISTRY_DIR, "registry.lock");
14
+
15
+ async function ensureDir(): Promise<void> {
16
+ await mkdir(REGISTRY_DIR, { recursive: true });
17
+ }
18
+
19
+ async function acquireLock(timeout = 5000): Promise<void> {
20
+ const start = Date.now();
21
+ const lockFile = Bun.file(LOCK_PATH);
22
+
23
+ while (Date.now() - start < timeout) {
24
+ try {
25
+ const exists = await lockFile.exists();
26
+ if (!exists) {
27
+ await Bun.write(LOCK_PATH, String(process.pid));
28
+ return;
29
+ }
30
+
31
+ const stat = await lockFile.stat();
32
+ const isStale = stat && Date.now() - stat.mtime.getTime() > 30000;
33
+ if (isStale) {
34
+ await Bun.write(LOCK_PATH, String(process.pid));
35
+ return;
36
+ }
37
+
38
+ await Bun.sleep(50);
39
+ } catch {
40
+ await Bun.sleep(50);
41
+ }
42
+ }
43
+
44
+ throw new Error("Failed to acquire registry lock - timeout");
45
+ }
46
+
47
+ async function releaseLock(): Promise<void> {
48
+ try {
49
+ const { unlink } = await import("node:fs/promises");
50
+ await unlink(LOCK_PATH);
51
+ } catch {}
52
+ }
53
+
54
+ async function readRegistry(): Promise<MailboxRegistry> {
55
+ try {
56
+ const file = Bun.file(REGISTRY_PATH);
57
+ if (await file.exists()) {
58
+ return await file.json();
59
+ }
60
+ } catch {}
61
+ return createEmptyRegistry();
62
+ }
63
+
64
+ async function writeRegistry(registry: MailboxRegistry): Promise<void> {
65
+ await Bun.write(REGISTRY_PATH, JSON.stringify(registry, null, 2));
66
+ }
67
+
68
+ export async function withRegistry<T>(
69
+ fn: (registry: MailboxRegistry) => T | Promise<T>,
70
+ options: { write?: boolean } = {},
71
+ ): Promise<T> {
72
+ await ensureDir();
73
+ await acquireLock();
74
+
75
+ try {
76
+ const registry = await readRegistry();
77
+ const result = await fn(registry);
78
+
79
+ if (options.write) {
80
+ await writeRegistry(registry);
81
+ }
82
+
83
+ return result;
84
+ } finally {
85
+ await releaseLock();
86
+ }
87
+ }
88
+
89
+ export async function registerSession(
90
+ sessionId: string,
91
+ entry: Omit<SessionEntry, "registeredAt" | "lastSeen">,
92
+ ): Promise<void> {
93
+ await withRegistry(
94
+ (registry) => {
95
+ registry.sessions[sessionId] = {
96
+ ...entry,
97
+ registeredAt: Date.now(),
98
+ lastSeen: Date.now(),
99
+ };
100
+ },
101
+ { write: true },
102
+ );
103
+ }
104
+
105
+ export async function unregisterSession(sessionId: string): Promise<void> {
106
+ await withRegistry(
107
+ (registry) => {
108
+ delete registry.sessions[sessionId];
109
+ delete registry.lastSender[sessionId];
110
+
111
+ for (const [recipientId, sender] of Object.entries(registry.lastSender)) {
112
+ if (sender.sessionId === sessionId) {
113
+ delete registry.lastSender[recipientId];
114
+ }
115
+ }
116
+ },
117
+ { write: true },
118
+ );
119
+ }
120
+
121
+ export async function updateLastSeen(sessionId: string): Promise<void> {
122
+ await withRegistry(
123
+ (registry) => {
124
+ if (registry.sessions[sessionId]) {
125
+ registry.sessions[sessionId].lastSeen = Date.now();
126
+ }
127
+ },
128
+ { write: true },
129
+ );
130
+ }
131
+
132
+ export async function setLastSender(
133
+ recipientSessionId: string,
134
+ senderSessionId: string,
135
+ ): Promise<void> {
136
+ await withRegistry(
137
+ (registry) => {
138
+ registry.lastSender[recipientSessionId] = {
139
+ sessionId: senderSessionId,
140
+ timestamp: Date.now(),
141
+ };
142
+ },
143
+ { write: true },
144
+ );
145
+ }
146
+
147
+ export async function getLastSender(
148
+ recipientSessionId: string,
149
+ ): Promise<{ sessionId: string; timestamp: number } | null> {
150
+ return await withRegistry((registry) => {
151
+ return registry.lastSender[recipientSessionId] || null;
152
+ });
153
+ }
154
+
155
+ export async function getSession(
156
+ sessionId: string,
157
+ ): Promise<SessionEntry | null> {
158
+ return await withRegistry((registry) => {
159
+ return registry.sessions[sessionId] || null;
160
+ });
161
+ }
162
+
163
+ export async function getAllSessions(): Promise<
164
+ Array<{ sessionId: string } & SessionEntry>
165
+ > {
166
+ return await withRegistry((registry) => {
167
+ return Object.entries(registry.sessions).map(([sessionId, entry]) => ({
168
+ sessionId,
169
+ ...entry,
170
+ }));
171
+ });
172
+ }
173
+
174
+ export async function isRegistered(sessionId: string): Promise<boolean> {
175
+ return await withRegistry((registry) => {
176
+ return sessionId in registry.sessions;
177
+ });
178
+ }
179
+
180
+ export async function resolveSessionId(
181
+ partialId: string,
182
+ ): Promise<{ sessionId: string; entry: SessionEntry } | null> {
183
+ return await withRegistry((registry) => {
184
+ if (registry.sessions[partialId]) {
185
+ return { sessionId: partialId, entry: registry.sessions[partialId] };
186
+ }
187
+
188
+ const matches = Object.entries(registry.sessions).filter(([id]) =>
189
+ id.startsWith(partialId),
190
+ );
191
+
192
+ if (matches.length === 1) {
193
+ const match = matches[0]!;
194
+ return { sessionId: match[0], entry: match[1] };
195
+ }
196
+
197
+ if (matches.length > 1) {
198
+ throw new Error(
199
+ `Ambiguous session ID "${partialId}" - matches: ${matches.map(([id]) => id).join(", ")}`,
200
+ );
201
+ }
202
+
203
+ return null;
204
+ });
205
+ }
@@ -0,0 +1,46 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ import { isRegistered, getSession, getLastSender } from "../registry";
4
+
5
+ export function createCheckTool() {
6
+ return tool({
7
+ description:
8
+ "Check your mailbox registration status and who last messaged you.",
9
+ args: {},
10
+ async execute(_args, toolCtx) {
11
+ const registered = await isRegistered(toolCtx.sessionID);
12
+
13
+ if (!registered) {
14
+ return `Status: Not registered\nYour ID: ${toolCtx.sessionID}\n\nUse mailbox_register to register.`;
15
+ }
16
+
17
+ const session = await getSession(toolCtx.sessionID);
18
+ const lastSender = await getLastSender(toolCtx.sessionID);
19
+
20
+ let result = `Status: Registered\nYour ID: ${toolCtx.sessionID}`;
21
+
22
+ if (session?.description) {
23
+ result += `\nDescription: ${session.description}`;
24
+ }
25
+
26
+ if (lastSender) {
27
+ const ago = formatTimeAgo(lastSender.timestamp);
28
+ result += `\n\nLast message from: ${lastSender.sessionId} (${ago})`;
29
+ result += `\nUse mailbox_reply to respond.`;
30
+ } else {
31
+ result += `\n\nNo messages received yet.`;
32
+ }
33
+
34
+ return result;
35
+ },
36
+ });
37
+ }
38
+
39
+ function formatTimeAgo(timestamp: number): string {
40
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
41
+
42
+ if (seconds < 60) return "just now";
43
+ if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
44
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
45
+ return `${Math.floor(seconds / 86400)} days ago`;
46
+ }
@@ -0,0 +1,21 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ import { formatSessionTable } from "../format";
4
+ import { getAllSessions } from "../registry";
5
+
6
+ export function createListTool() {
7
+ return tool({
8
+ description:
9
+ "List all registered agent sessions available for mailbox communication.",
10
+ args: {},
11
+ async execute(_args, toolCtx) {
12
+ const sessions = await getAllSessions();
13
+
14
+ if (sessions.length === 0) {
15
+ return "No sessions registered. Use mailbox_register to register this session.";
16
+ }
17
+
18
+ return formatSessionTable(sessions, toolCtx.sessionID);
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,36 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ import { tool } from "@opencode-ai/plugin";
4
+
5
+ import { registerSession, isRegistered } from "../registry";
6
+
7
+ export function createRegisterTool(ctx: PluginInput) {
8
+ return tool({
9
+ description:
10
+ "Register this session for mailbox communication. Required before sending or receiving messages from other agents.",
11
+ args: {
12
+ description: tool.schema
13
+ .string("Optional description of what this agent is working on")
14
+ .optional(),
15
+ },
16
+ async execute(args, toolCtx) {
17
+ if (await isRegistered(toolCtx.sessionID)) {
18
+ return `Already registered: ${toolCtx.sessionID}`;
19
+ }
20
+
21
+ await registerSession(toolCtx.sessionID, {
22
+ projectID: ctx.project.id,
23
+ directory: ctx.directory,
24
+ description: args.description,
25
+ });
26
+
27
+ try {
28
+ await ctx.client.tui.showToast({
29
+ body: { message: `Mailbox: Registered`, variant: "success" },
30
+ });
31
+ } catch {}
32
+
33
+ return `Registered: ${toolCtx.sessionID}\nOther agents can now send you messages.`;
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,56 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ import { tool } from "@opencode-ai/plugin";
4
+
5
+ import { formatIncomingMessage } from "../format";
6
+ import { getLastSender, isRegistered, setLastSender } from "../registry";
7
+
8
+ export function createReplyTool(ctx: PluginInput) {
9
+ return tool({
10
+ description:
11
+ "Reply to the last agent session that messaged you. Convenience wrapper around mailbox_send.",
12
+ args: {
13
+ message: tool.schema.string("Your reply message"),
14
+ },
15
+ async execute(args, toolCtx) {
16
+ if (!(await isRegistered(toolCtx.sessionID))) {
17
+ return "Error: You must register first. Use mailbox_register.";
18
+ }
19
+
20
+ const lastSender = await getLastSender(toolCtx.sessionID);
21
+ if (!lastSender) {
22
+ return "Error: No previous sender to reply to. Use mailbox_send instead.";
23
+ }
24
+
25
+ const formattedMessage = formatIncomingMessage(
26
+ toolCtx.sessionID,
27
+ args.message,
28
+ );
29
+
30
+ try {
31
+ await ctx.client.session.prompt({
32
+ path: { id: lastSender.sessionId },
33
+ body: {
34
+ parts: [{ type: "text", text: formattedMessage }],
35
+ },
36
+ });
37
+ } catch (error) {
38
+ const msg = error instanceof Error ? error.message : String(error);
39
+ return `Error: Failed to deliver reply: ${msg}`;
40
+ }
41
+
42
+ await setLastSender(lastSender.sessionId, toolCtx.sessionID);
43
+
44
+ try {
45
+ await ctx.client.tui.showToast({
46
+ body: {
47
+ message: `Reply sent to ${lastSender.sessionId.slice(0, 12)}...`,
48
+ variant: "success",
49
+ },
50
+ });
51
+ } catch {}
52
+
53
+ return `Reply sent to ${lastSender.sessionId}`;
54
+ },
55
+ });
56
+ }
@@ -0,0 +1,76 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ import { tool } from "@opencode-ai/plugin";
4
+
5
+ import { formatIncomingMessage } from "../format";
6
+ import { resolveSessionId, isRegistered, setLastSender } from "../registry";
7
+
8
+ export function createSendTool(ctx: PluginInput) {
9
+ return tool({
10
+ description:
11
+ "Send a message to another registered agent session. The message is delivered in real-time to their chat. You must be registered (mailbox_register) before sending.",
12
+ args: {
13
+ to: tool.schema.string(
14
+ "Session ID of the recipient (or unique prefix, min 6 chars)",
15
+ ),
16
+ message: tool.schema.string("The message content to send"),
17
+ },
18
+ async execute(args, toolCtx) {
19
+ if (!(await isRegistered(toolCtx.sessionID))) {
20
+ return "Error: You must register first. Use mailbox_register.";
21
+ }
22
+
23
+ if (args.to.length < 6) {
24
+ return "Error: Session ID prefix must be at least 6 characters.";
25
+ }
26
+
27
+ let resolved: { sessionId: string } | null;
28
+ try {
29
+ resolved = await resolveSessionId(args.to);
30
+ } catch (error) {
31
+ const msg = error instanceof Error ? error.message : String(error);
32
+ return `Error: ${msg}`;
33
+ }
34
+
35
+ if (!resolved) {
36
+ return `Error: No session found matching "${args.to}". Use mailbox_list to see available sessions.`;
37
+ }
38
+
39
+ const recipientId = resolved.sessionId;
40
+
41
+ if (recipientId === toolCtx.sessionID) {
42
+ return "Error: Cannot send a message to yourself.";
43
+ }
44
+
45
+ const formattedMessage = formatIncomingMessage(
46
+ toolCtx.sessionID,
47
+ args.message,
48
+ );
49
+
50
+ try {
51
+ await ctx.client.session.prompt({
52
+ path: { id: recipientId },
53
+ body: {
54
+ parts: [{ type: "text", text: formattedMessage }],
55
+ },
56
+ });
57
+ } catch (error) {
58
+ const msg = error instanceof Error ? error.message : String(error);
59
+ return `Error: Failed to deliver message: ${msg}`;
60
+ }
61
+
62
+ await setLastSender(recipientId, toolCtx.sessionID);
63
+
64
+ try {
65
+ await ctx.client.tui.showToast({
66
+ body: {
67
+ message: `Message sent to ${recipientId.slice(0, 12)}...`,
68
+ variant: "success",
69
+ },
70
+ });
71
+ } catch {}
72
+
73
+ return `Message sent to ${recipientId}`;
74
+ },
75
+ });
76
+ }
package/src/types.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface SessionEntry {
2
+ projectID: string;
3
+ directory: string;
4
+ description?: string;
5
+ registeredAt: number;
6
+ lastSeen: number;
7
+ }
8
+
9
+ export interface LastSender {
10
+ sessionId: string;
11
+ timestamp: number;
12
+ }
13
+
14
+ export interface MailboxRegistry {
15
+ sessions: {
16
+ [sessionId: string]: SessionEntry;
17
+ };
18
+ lastSender: {
19
+ [recipientSessionId: string]: LastSender;
20
+ };
21
+ }
22
+
23
+ export interface Message {
24
+ id: string;
25
+ from: string;
26
+ to: string;
27
+ content: string;
28
+ timestamp: number;
29
+ }
30
+
31
+ export function createEmptyRegistry(): MailboxRegistry {
32
+ return {
33
+ sessions: {},
34
+ lastSender: {},
35
+ };
36
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }