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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayhanadev/opencode-plugin-mailbox",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "Inter-agent communication plugin for OpenCode - enables real-time messaging between running agent sessions",
5
5
  "homepage": "https://github.com/rayhanadev/opencode-plugin-mailbox#readme",
6
6
  "bugs": {
@@ -0,0 +1,52 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ import { getUnreadMessages } from "../inbox";
4
+ import { isRegistered } from "../registry";
5
+
6
+ type MessageInfo = {
7
+ sessionID: string;
8
+ role: string;
9
+ };
10
+
11
+ type MessageWithParts = {
12
+ info: MessageInfo;
13
+ parts: Array<{ type: string; text?: string }>;
14
+ };
15
+
16
+ function getSessionIdFromMessages(messages: MessageWithParts[]): string | null {
17
+ for (let i = messages.length - 1; i >= 0; i--) {
18
+ const msg = messages[i];
19
+ if (msg && msg.info.role === "user" && msg.info.sessionID) {
20
+ return msg.info.sessionID;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ export function createMessagesTransformHook(_ctx: PluginInput) {
27
+ return async (
28
+ _input: {},
29
+ output: { messages: MessageWithParts[] },
30
+ ) => {
31
+ const sessionID = getSessionIdFromMessages(output.messages);
32
+ if (!sessionID) {
33
+ return;
34
+ }
35
+
36
+ if (!(await isRegistered(sessionID))) {
37
+ return;
38
+ }
39
+
40
+ const unreadMessages = await getUnreadMessages(sessionID);
41
+ if (unreadMessages.length === 0) {
42
+ return;
43
+ }
44
+
45
+ const notification = `You have ${unreadMessages.length} unread message(s) in your mailbox. Use mailbox_read to view them.`;
46
+
47
+ output.messages.push({
48
+ info: { sessionID, role: "user" },
49
+ parts: [{ type: "text", text: notification }],
50
+ });
51
+ };
52
+ }
package/src/inbox.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { mkdir, readdir, unlink } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import type { Message } from "./types";
6
+
7
+ const INBOX_DIR = join(homedir(), ".opencode", "plugins", "mailbox", "inbox");
8
+
9
+ function getSessionInboxDir(sessionId: string): string {
10
+ return join(INBOX_DIR, sessionId);
11
+ }
12
+
13
+ function getMessagePath(sessionId: string, messageId: string): string {
14
+ return join(getSessionInboxDir(sessionId), `${messageId}.json`);
15
+ }
16
+
17
+ export async function saveMessage(
18
+ input: Pick<Message, "from" | "to" | "content">,
19
+ ): Promise<Message> {
20
+ const message: Message = {
21
+ id: crypto.randomUUID(),
22
+ from: input.from,
23
+ to: input.to,
24
+ content: input.content,
25
+ timestamp: Date.now(),
26
+ read: false,
27
+ };
28
+
29
+ const inboxDir = getSessionInboxDir(message.to);
30
+ await mkdir(inboxDir, { recursive: true });
31
+
32
+ const messagePath = getMessagePath(message.to, message.id);
33
+ await Bun.write(messagePath, JSON.stringify(message, null, 2));
34
+
35
+ return message;
36
+ }
37
+
38
+ export async function getUnreadMessages(sessionId: string): Promise<Message[]> {
39
+ const inboxDir = getSessionInboxDir(sessionId);
40
+
41
+ try {
42
+ const files = await readdir(inboxDir);
43
+ const messages: Message[] = [];
44
+
45
+ for (const file of files) {
46
+ if (!file.endsWith(".json")) continue;
47
+
48
+ const filePath = join(inboxDir, file);
49
+ const content = await Bun.file(filePath).json();
50
+ const message = content as Message;
51
+
52
+ if (!message.read) {
53
+ messages.push(message);
54
+ }
55
+ }
56
+
57
+ return messages.sort((a, b) => a.timestamp - b.timestamp);
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ export async function getAllMessages(sessionId: string): Promise<Message[]> {
64
+ const inboxDir = getSessionInboxDir(sessionId);
65
+
66
+ try {
67
+ const files = await readdir(inboxDir);
68
+ const messages: Message[] = [];
69
+
70
+ for (const file of files) {
71
+ if (!file.endsWith(".json")) continue;
72
+
73
+ const filePath = join(inboxDir, file);
74
+ const content = await Bun.file(filePath).json();
75
+ messages.push(content as Message);
76
+ }
77
+
78
+ return messages.sort((a, b) => a.timestamp - b.timestamp);
79
+ } catch {
80
+ return [];
81
+ }
82
+ }
83
+
84
+ export async function markAsRead(
85
+ sessionId: string,
86
+ messageId: string,
87
+ ): Promise<boolean> {
88
+ const messagePath = getMessagePath(sessionId, messageId);
89
+
90
+ try {
91
+ const file = Bun.file(messagePath);
92
+ if (!(await file.exists())) return false;
93
+
94
+ const message = (await file.json()) as Message;
95
+ message.read = true;
96
+ await Bun.write(messagePath, JSON.stringify(message, null, 2));
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ export async function markAllAsRead(sessionId: string): Promise<number> {
104
+ const messages = await getUnreadMessages(sessionId);
105
+ let count = 0;
106
+
107
+ for (const message of messages) {
108
+ if (await markAsRead(sessionId, message.id)) {
109
+ count++;
110
+ }
111
+ }
112
+
113
+ return count;
114
+ }
115
+
116
+ export async function deleteMessage(
117
+ sessionId: string,
118
+ messageId: string,
119
+ ): Promise<boolean> {
120
+ const messagePath = getMessagePath(sessionId, messageId);
121
+
122
+ try {
123
+ await unlink(messagePath);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ export async function getUnreadCount(sessionId: string): Promise<number> {
131
+ const messages = await getUnreadMessages(sessionId);
132
+ return messages.length;
133
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
 
3
3
  import { createEventHook } from "./hooks/events";
4
+ import { createMessagesTransformHook } from "./hooks/messages";
4
5
  import { createCheckTool } from "./tools/check";
5
6
  import { createListTool } from "./tools/list";
7
+ import { createReadTool } from "./tools/read";
6
8
  import { createRegisterTool } from "./tools/register";
7
9
  import { createReplyTool } from "./tools/reply";
8
10
  import { createSendTool } from "./tools/send";
@@ -12,11 +14,13 @@ export const MailboxPlugin: Plugin = async (ctx) => {
12
14
  tool: {
13
15
  mailbox_register: createRegisterTool(ctx),
14
16
  mailbox_list: createListTool(),
15
- mailbox_send: createSendTool(ctx),
16
- mailbox_reply: createReplyTool(ctx),
17
+ mailbox_send: createSendTool(),
18
+ mailbox_reply: createReplyTool(),
17
19
  mailbox_check: createCheckTool(),
20
+ mailbox_read: createReadTool(),
18
21
  },
19
22
  event: createEventHook(ctx),
23
+ "experimental.chat.messages.transform": createMessagesTransformHook(ctx),
20
24
  };
21
25
  };
22
26
 
@@ -0,0 +1,40 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ import { getUnreadMessages, markAllAsRead } from "../inbox";
4
+ import { isRegistered, getSession } from "../registry";
5
+
6
+ export function createReadTool() {
7
+ return tool({
8
+ description:
9
+ "Read all unread messages from your mailbox inbox. Messages are automatically marked as read after viewing.",
10
+ args: {},
11
+ async execute(_args, toolCtx) {
12
+ if (!(await isRegistered(toolCtx.sessionID))) {
13
+ return "Error: You must register first. Use mailbox_register.";
14
+ }
15
+
16
+ const messages = await getUnreadMessages(toolCtx.sessionID);
17
+
18
+ if (messages.length === 0) {
19
+ return "No unread messages in your inbox.";
20
+ }
21
+
22
+ const formatted = await Promise.all(
23
+ messages.map(async (msg) => {
24
+ const senderInfo = await getSession(msg.from);
25
+ const senderLabel = senderInfo?.description
26
+ ? `${msg.from.slice(0, 12)}... (${senderInfo.description})`
27
+ : msg.from.slice(0, 12) + "...";
28
+
29
+ const timestamp = new Date(msg.timestamp).toLocaleString();
30
+
31
+ return `--- Message from ${senderLabel} at ${timestamp} ---\n${msg.content}`;
32
+ }),
33
+ );
34
+
35
+ await markAllAsRead(toolCtx.sessionID);
36
+
37
+ return `You have ${messages.length} message(s):\n\n${formatted.join("\n\n")}`;
38
+ },
39
+ });
40
+ }
@@ -1,11 +1,9 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
-
3
1
  import { tool } from "@opencode-ai/plugin";
4
2
 
5
- import { formatIncomingMessage } from "../format";
3
+ import { saveMessage } from "../inbox";
6
4
  import { getLastSender, isRegistered, setLastSender } from "../registry";
7
5
 
8
- export function createReplyTool(ctx: PluginInput) {
6
+ export function createReplyTool() {
9
7
  return tool({
10
8
  description:
11
9
  "Reply to the last agent session that messaged you. Convenience wrapper around mailbox_send.",
@@ -22,35 +20,20 @@ export function createReplyTool(ctx: PluginInput) {
22
20
  return "Error: No previous sender to reply to. Use mailbox_send instead.";
23
21
  }
24
22
 
25
- const formattedMessage = formatIncomingMessage(
26
- toolCtx.sessionID,
27
- args.message,
28
- );
29
-
30
23
  try {
31
- await ctx.client.session.prompt({
32
- path: { id: lastSender.sessionId },
33
- body: {
34
- parts: [{ type: "text", text: formattedMessage }],
35
- },
24
+ await saveMessage({
25
+ from: toolCtx.sessionID,
26
+ to: lastSender.sessionId,
27
+ content: args.message,
36
28
  });
37
29
  } catch (error) {
38
30
  const msg = error instanceof Error ? error.message : String(error);
39
- return `Error: Failed to deliver reply: ${msg}`;
31
+ return `Error: Failed to save reply: ${msg}`;
40
32
  }
41
33
 
42
34
  await setLastSender(lastSender.sessionId, toolCtx.sessionID);
43
35
 
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}`;
36
+ return `Reply sent to ${lastSender.sessionId}. They will be notified on their next interaction.`;
54
37
  },
55
38
  });
56
39
  }
package/src/tools/send.ts CHANGED
@@ -1,14 +1,12 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
-
3
1
  import { tool } from "@opencode-ai/plugin";
4
2
 
5
- import { formatIncomingMessage } from "../format";
3
+ import { saveMessage } from "../inbox";
6
4
  import { resolveSessionId, isRegistered, setLastSender } from "../registry";
7
5
 
8
- export function createSendTool(ctx: PluginInput) {
6
+ export function createSendTool() {
9
7
  return tool({
10
8
  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.",
9
+ "Send a message to another registered agent session. The message is delivered to their inbox and they will be notified on their next interaction. You must be registered (mailbox_register) before sending.",
12
10
  args: {
13
11
  to: tool.schema.string(
14
12
  "Session ID of the recipient (or unique prefix, min 6 chars)",
@@ -42,35 +40,20 @@ export function createSendTool(ctx: PluginInput) {
42
40
  return "Error: Cannot send a message to yourself.";
43
41
  }
44
42
 
45
- const formattedMessage = formatIncomingMessage(
46
- toolCtx.sessionID,
47
- args.message,
48
- );
49
-
50
43
  try {
51
- await ctx.client.session.prompt({
52
- path: { id: recipientId },
53
- body: {
54
- parts: [{ type: "text", text: formattedMessage }],
55
- },
44
+ await saveMessage({
45
+ from: toolCtx.sessionID,
46
+ to: recipientId,
47
+ content: args.message,
56
48
  });
57
49
  } catch (error) {
58
50
  const msg = error instanceof Error ? error.message : String(error);
59
- return `Error: Failed to deliver message: ${msg}`;
51
+ return `Error: Failed to save message: ${msg}`;
60
52
  }
61
53
 
62
54
  await setLastSender(recipientId, toolCtx.sessionID);
63
55
 
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}`;
56
+ return `Message sent to ${recipientId}. They will be notified on their next interaction.`;
74
57
  },
75
58
  });
76
59
  }
package/src/types.ts CHANGED
@@ -26,6 +26,7 @@ export interface Message {
26
26
  to: string;
27
27
  content: string;
28
28
  timestamp: number;
29
+ read: boolean;
29
30
  }
30
31
 
31
32
  export function createEmptyRegistry(): MailboxRegistry {