@hasna/conversations 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -31,10 +31,10 @@ npx @hasna/conversations
31
31
 
32
32
  ```bash
33
33
  # Basic message
34
- convo send --to claude-code "Hello from codex"
34
+ conversations send --to claude-code "Hello from codex"
35
35
 
36
36
  # With context
37
- convo send --to claude-code "Check this branch" \
37
+ conversations send --to claude-code "Check this branch" \
38
38
  --from codex \
39
39
  --priority high \
40
40
  --working-dir /path/to/project \
@@ -42,56 +42,86 @@ convo send --to claude-code "Check this branch" \
42
42
  --branch feature/auth
43
43
 
44
44
  # With metadata
45
- convo send --to gemini "Deploy ready" --metadata '{"env":"staging"}'
45
+ conversations send --to gemini "Deploy ready" --metadata '{"env":"staging"}'
46
46
  ```
47
47
 
48
48
  ### Read Messages
49
49
 
50
50
  ```bash
51
51
  # Read all messages for an agent
52
- convo read --to codex
52
+ conversations read --to codex
53
53
 
54
54
  # Unread only, as JSON
55
- convo read --to codex --unread --json
55
+ conversations read --to codex --unread --json
56
56
 
57
57
  # Filter by session
58
- convo read --session alice-bob-abc123
58
+ conversations read --session alice-bob-abc123
59
59
 
60
60
  # Read and mark as read
61
- convo read --to codex --unread --mark-read
61
+ conversations read --to codex --unread --mark-read
62
62
  ```
63
63
 
64
64
  ### Sessions
65
65
 
66
66
  ```bash
67
67
  # List all sessions
68
- convo sessions
68
+ conversations sessions
69
69
 
70
70
  # Sessions for a specific agent
71
- convo sessions --agent claude-code --json
71
+ conversations sessions --agent claude-code --json
72
72
  ```
73
73
 
74
74
  ### Reply
75
75
 
76
76
  ```bash
77
77
  # Reply to a message (auto-resolves session and recipient)
78
- convo reply --to 42 "Got it, working on it now"
78
+ conversations reply --to 42 "Got it, working on it now"
79
+ ```
80
+
81
+ ### Channels
82
+
83
+ Channels are broadcast spaces — any agent can post, all members can read.
84
+
85
+ ```bash
86
+ # Create a channel
87
+ conversations channel create deployments --description "Deployment notifications"
88
+
89
+ # List channels
90
+ conversations channel list
91
+
92
+ # Join a channel
93
+ conversations channel join deployments --from codex
94
+
95
+ # Send to a channel
96
+ conversations channel send deployments "v1.2 deployed to staging" --from ops
97
+
98
+ # Read channel messages
99
+ conversations channel read deployments
100
+
101
+ # Leave a channel
102
+ conversations channel leave deployments --from codex
103
+
104
+ # List members
105
+ conversations channel members deployments
79
106
  ```
80
107
 
81
108
  ### Mark Read
82
109
 
83
110
  ```bash
84
111
  # Mark specific messages
85
- convo mark-read 1 2 3 --agent codex
112
+ conversations mark-read 1 2 3 --agent codex
86
113
 
87
114
  # Mark entire session
88
- convo mark-read --session abc123 --agent codex
115
+ conversations mark-read --session abc123 --agent codex
116
+
117
+ # Mark entire channel
118
+ conversations mark-read --channel deployments --agent codex
89
119
  ```
90
120
 
91
121
  ### Status
92
122
 
93
123
  ```bash
94
- convo status
124
+ conversations status
95
125
  # Conversations Status
96
126
  # DB Path: ~/.conversations/messages.db
97
127
  # Messages: 47
@@ -102,7 +132,7 @@ convo status
102
132
  ### Interactive TUI
103
133
 
104
134
  ```bash
105
- convo
135
+ conversations
106
136
  ```
107
137
 
108
138
  Arrow keys to navigate sessions, Enter to open, `n` for new conversation, `q` to quit, Esc to go back.
@@ -112,7 +142,7 @@ Arrow keys to navigate sessions, Enter to open, `n` for new conversation, `q` to
112
142
  For native AI agent integration via the Model Context Protocol:
113
143
 
114
144
  ```bash
115
- convo mcp
145
+ conversations mcp
116
146
  ```
117
147
 
118
148
  ### Agent Configuration
@@ -135,11 +165,17 @@ Add to your agent's MCP config:
135
165
 
136
166
  | Tool | Description |
137
167
  |------|-------------|
138
- | `send_message` | Send a message (sender auto-resolved from env) |
168
+ | `send_message` | Send a direct message (sender auto-resolved from env) |
139
169
  | `read_messages` | Read messages with filters |
140
170
  | `list_sessions` | List conversation sessions |
141
171
  | `reply` | Reply to a message by ID |
142
172
  | `mark_read` | Mark messages as read |
173
+ | `create_channel` | Create a new channel |
174
+ | `list_channels` | List all channels with member/message counts |
175
+ | `send_to_channel` | Send a message to a channel |
176
+ | `read_channel` | Read messages from a channel |
177
+ | `join_channel` | Join a channel |
178
+ | `leave_channel` | Leave a channel |
143
179
 
144
180
  ## Programmatic API
145
181
 
@@ -149,9 +185,12 @@ import {
149
185
  readMessages,
150
186
  listSessions,
151
187
  startPolling,
188
+ createChannel,
189
+ listChannels,
190
+ joinChannel,
152
191
  } from "@hasna/conversations";
153
192
 
154
- // Send a message
193
+ // Send a direct message
155
194
  const msg = sendMessage({
156
195
  from: "my-agent",
157
196
  to: "claude-code",
@@ -166,31 +205,44 @@ const { stop } = startPolling({
166
205
  to_agent: "my-agent",
167
206
  on_messages: (msgs) => console.log("New:", msgs),
168
207
  });
208
+
209
+ // Channels
210
+ createChannel("deploys", "my-agent", "Deploy notifications");
211
+ joinChannel("deploys", "claude-code");
212
+ sendMessage({
213
+ from: "my-agent",
214
+ to: "deploys",
215
+ content: "v2.0 shipped",
216
+ channel: "deploys",
217
+ session_id: "channel:deploys",
218
+ });
219
+ const channelMsgs = readMessages({ channel: "deploys" });
169
220
  ```
170
221
 
171
222
  ## Architecture
172
223
 
173
224
  ```
174
- ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
175
- Ink TUI Headless MCP Server
176
- `convo` `convo send`│ `convo mcp`
177
- └──────┬──────┘ └──────┬───────┘ └──────┬───────┘
178
-
179
- └──────────┬───────┴───────────────────┘
180
-
181
- ┌───────▼────────┐
182
- Core Library
183
- │ SQLite WAL │
184
- │ 200ms polling │
185
- └────────────────┘
186
-
187
- ~/.conversations/messages.db
225
+ ┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
226
+ Ink TUI Headless MCP Server
227
+ `conversations` `conversations send`│ `conversations mcp`
228
+ └────────┬─────────┘ └──────────┬──────────┘ └──────────┬───────────┘
229
+
230
+ └───────────┬───────────┴────────────────────────┘
231
+
232
+ ┌───────▼────────┐
233
+ Core Library
234
+ │ SQLite WAL │
235
+ │ 200ms polling │
236
+ └────────────────┘
237
+
238
+ ~/.conversations/messages.db
188
239
  ```
189
240
 
190
241
  - **SQLite WAL mode** for concurrent read/write across processes
191
242
  - **200ms polling** for near-instant message delivery
192
243
  - **Single shared database** at `~/.conversations/messages.db`
193
244
  - Sessions derived from messages (no separate table)
245
+ - **Channels** for broadcast messaging (many-to-many)
194
246
 
195
247
  ## Development
196
248
 
package/bin/index.js CHANGED
@@ -1890,6 +1890,7 @@ function getDb() {
1890
1890
  session_id TEXT NOT NULL,
1891
1891
  from_agent TEXT NOT NULL,
1892
1892
  to_agent TEXT NOT NULL,
1893
+ channel TEXT,
1893
1894
  content TEXT NOT NULL,
1894
1895
  priority TEXT NOT NULL DEFAULT 'normal',
1895
1896
  working_dir TEXT,
@@ -1903,6 +1904,27 @@ function getDb() {
1903
1904
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
1904
1905
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
1905
1906
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
1907
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
1908
+ const cols = db.prepare("PRAGMA table_info(messages)").all();
1909
+ if (!cols.some((c) => c.name === "channel")) {
1910
+ db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
1911
+ }
1912
+ db.exec(`
1913
+ CREATE TABLE IF NOT EXISTS channels (
1914
+ name TEXT PRIMARY KEY,
1915
+ description TEXT,
1916
+ created_by TEXT NOT NULL,
1917
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
1918
+ )
1919
+ `);
1920
+ db.exec(`
1921
+ CREATE TABLE IF NOT EXISTS channel_members (
1922
+ channel TEXT NOT NULL REFERENCES channels(name),
1923
+ agent TEXT NOT NULL,
1924
+ joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1925
+ PRIMARY KEY (channel, agent)
1926
+ )
1927
+ `);
1906
1928
  return db;
1907
1929
  }
1908
1930
  function closeDb() {
@@ -1927,11 +1949,11 @@ function sendMessage(opts) {
1927
1949
  const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
1928
1950
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
1929
1951
  const stmt = db2.prepare(`
1930
- INSERT INTO messages (session_id, from_agent, to_agent, content, priority, working_dir, repository, branch, metadata)
1931
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1952
+ INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
1953
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1932
1954
  RETURNING *
1933
1955
  `);
1934
- const row = stmt.get(sessionId, opts.from, opts.to, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
1956
+ const row = stmt.get(sessionId, opts.from, opts.to, opts.channel || null, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
1935
1957
  return parseMessage(row);
1936
1958
  }
1937
1959
  function readMessages(opts = {}) {
@@ -1950,6 +1972,10 @@ function readMessages(opts = {}) {
1950
1972
  conditions.push("to_agent = ?");
1951
1973
  params.push(opts.to);
1952
1974
  }
1975
+ if (opts.channel) {
1976
+ conditions.push("channel = ?");
1977
+ params.push(opts.channel);
1978
+ }
1953
1979
  if (opts.since) {
1954
1980
  conditions.push("created_at > ?");
1955
1981
  params.push(opts.since);
@@ -1977,6 +2003,12 @@ function markSessionRead(sessionId, reader) {
1977
2003
  const result = stmt.run(sessionId, reader);
1978
2004
  return result.changes;
1979
2005
  }
2006
+ function markChannelRead(channelName, reader) {
2007
+ const db2 = getDb();
2008
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE channel = ? AND from_agent != ? AND read_at IS NULL`);
2009
+ const result = stmt.run(channelName, reader);
2010
+ return result.changes;
2011
+ }
1980
2012
  function getMessageById(id) {
1981
2013
  const db2 = getDb();
1982
2014
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
@@ -2019,6 +2051,64 @@ var init_sessions = __esm(() => {
2019
2051
  init_db();
2020
2052
  });
2021
2053
 
2054
+ // src/lib/channels.ts
2055
+ function createChannel(name, createdBy, description) {
2056
+ const db2 = getDb();
2057
+ const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
2058
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
2059
+ return row;
2060
+ }
2061
+ function listChannels() {
2062
+ const db2 = getDb();
2063
+ const rows = db2.prepare(`
2064
+ SELECT
2065
+ c.name,
2066
+ c.description,
2067
+ c.created_by,
2068
+ c.created_at,
2069
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2070
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2071
+ FROM channels c
2072
+ ORDER BY c.name ASC
2073
+ `).all();
2074
+ return rows;
2075
+ }
2076
+ function getChannel(name) {
2077
+ const db2 = getDb();
2078
+ const row = db2.prepare(`
2079
+ SELECT
2080
+ c.name,
2081
+ c.description,
2082
+ c.created_by,
2083
+ c.created_at,
2084
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2085
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2086
+ FROM channels c
2087
+ WHERE c.name = ?
2088
+ `).get(name);
2089
+ return row;
2090
+ }
2091
+ function joinChannel(channelName, agent) {
2092
+ const db2 = getDb();
2093
+ const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
2094
+ if (!channel)
2095
+ return false;
2096
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
2097
+ return true;
2098
+ }
2099
+ function leaveChannel(channelName, agent) {
2100
+ const db2 = getDb();
2101
+ const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
2102
+ return result.changes > 0;
2103
+ }
2104
+ function getChannelMembers(channelName) {
2105
+ const db2 = getDb();
2106
+ return db2.prepare("SELECT channel, agent, joined_at FROM channel_members WHERE channel = ? ORDER BY joined_at ASC").all(channelName);
2107
+ }
2108
+ var init_channels = __esm(() => {
2109
+ init_db();
2110
+ });
2111
+
2022
2112
  // src/lib/identity.ts
2023
2113
  function resolveIdentity(explicit) {
2024
2114
  if (explicit)
@@ -30910,13 +31000,14 @@ var init_mcp2 = __esm(() => {
30910
31000
  init_zod();
30911
31001
  init_messages();
30912
31002
  init_sessions();
31003
+ init_channels();
30913
31004
  server = new McpServer({
30914
31005
  name: "conversations",
30915
- version: "0.0.2"
31006
+ version: "0.0.4"
30916
31007
  });
30917
31008
  server.registerTool("send_message", {
30918
31009
  title: "Send Message",
30919
- description: "Send a message to another agent. The sender is auto-resolved from CONVERSATIONS_AGENT_ID env var.",
31010
+ description: "Send a direct message to another agent. The sender is auto-resolved from CONVERSATIONS_AGENT_ID env var.",
30920
31011
  inputSchema: {
30921
31012
  to: exports_external.string().describe("Recipient agent ID"),
30922
31013
  content: exports_external.string().describe("Message content"),
@@ -30952,6 +31043,7 @@ var init_mcp2 = __esm(() => {
30952
31043
  session_id: exports_external.string().optional().describe("Filter by session ID"),
30953
31044
  from: exports_external.string().optional().describe("Filter by sender agent ID"),
30954
31045
  to: exports_external.string().optional().describe("Filter by recipient agent ID"),
31046
+ channel: exports_external.string().optional().describe("Filter by channel name"),
30955
31047
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
30956
31048
  limit: exports_external.number().optional().describe("Max messages to return"),
30957
31049
  unread_only: exports_external.boolean().optional().describe("Only return unread messages")
@@ -31015,6 +31107,114 @@ var init_mcp2 = __esm(() => {
31015
31107
  content: [{ type: "text", text: JSON.stringify({ marked_read: count }, null, 2) }]
31016
31108
  };
31017
31109
  });
31110
+ server.registerTool("create_channel", {
31111
+ title: "Create Channel",
31112
+ description: "Create a new channel. The creator is auto-joined.",
31113
+ inputSchema: {
31114
+ name: exports_external.string().describe("Channel name (e.g. 'deployments', 'code-review')"),
31115
+ description: exports_external.string().optional().describe("Channel description")
31116
+ }
31117
+ }, async ({ name, description }) => {
31118
+ const agent = resolveIdentity();
31119
+ try {
31120
+ const ch = createChannel(name, agent, description);
31121
+ return {
31122
+ content: [{ type: "text", text: JSON.stringify(ch, null, 2) }]
31123
+ };
31124
+ } catch (e) {
31125
+ if (e.message?.includes("UNIQUE constraint")) {
31126
+ return {
31127
+ content: [{ type: "text", text: `Channel #${name} already exists` }],
31128
+ isError: true
31129
+ };
31130
+ }
31131
+ throw e;
31132
+ }
31133
+ });
31134
+ server.registerTool("list_channels", {
31135
+ title: "List Channels",
31136
+ description: "List all available channels with member and message counts."
31137
+ }, async () => {
31138
+ const channels = listChannels();
31139
+ return {
31140
+ content: [{ type: "text", text: JSON.stringify(channels, null, 2) }]
31141
+ };
31142
+ });
31143
+ server.registerTool("send_to_channel", {
31144
+ title: "Send to Channel",
31145
+ description: "Send a message to a channel. All members can see it.",
31146
+ inputSchema: {
31147
+ channel: exports_external.string().describe("Channel name"),
31148
+ content: exports_external.string().describe("Message content"),
31149
+ priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional().describe("Message priority")
31150
+ }
31151
+ }, async ({ channel, content, priority }) => {
31152
+ const from = resolveIdentity();
31153
+ const ch = getChannel(channel);
31154
+ if (!ch) {
31155
+ return {
31156
+ content: [{ type: "text", text: `Channel #${channel} not found` }],
31157
+ isError: true
31158
+ };
31159
+ }
31160
+ const msg = sendMessage({
31161
+ from,
31162
+ to: channel,
31163
+ content,
31164
+ channel,
31165
+ session_id: `channel:${channel}`,
31166
+ priority
31167
+ });
31168
+ return {
31169
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
31170
+ };
31171
+ });
31172
+ server.registerTool("read_channel", {
31173
+ title: "Read Channel",
31174
+ description: "Read messages from a channel.",
31175
+ inputSchema: {
31176
+ channel: exports_external.string().describe("Channel name"),
31177
+ since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
31178
+ limit: exports_external.number().optional().describe("Max messages to return")
31179
+ }
31180
+ }, async ({ channel, since, limit }) => {
31181
+ const messages = readMessages({ channel, since, limit });
31182
+ return {
31183
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
31184
+ };
31185
+ });
31186
+ server.registerTool("join_channel", {
31187
+ title: "Join Channel",
31188
+ description: "Join a channel to receive messages.",
31189
+ inputSchema: {
31190
+ channel: exports_external.string().describe("Channel name to join")
31191
+ }
31192
+ }, async ({ channel }) => {
31193
+ const agent = resolveIdentity();
31194
+ const ok = joinChannel(channel, agent);
31195
+ if (!ok) {
31196
+ return {
31197
+ content: [{ type: "text", text: `Channel #${channel} not found` }],
31198
+ isError: true
31199
+ };
31200
+ }
31201
+ return {
31202
+ content: [{ type: "text", text: JSON.stringify({ channel, agent, joined: true }, null, 2) }]
31203
+ };
31204
+ });
31205
+ server.registerTool("leave_channel", {
31206
+ title: "Leave Channel",
31207
+ description: "Leave a channel.",
31208
+ inputSchema: {
31209
+ channel: exports_external.string().describe("Channel name to leave")
31210
+ }
31211
+ }, async ({ channel }) => {
31212
+ const agent = resolveIdentity();
31213
+ const left = leaveChannel(channel, agent);
31214
+ return {
31215
+ content: [{ type: "text", text: JSON.stringify({ channel, agent, left }, null, 2) }]
31216
+ };
31217
+ });
31018
31218
  isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
31019
31219
  if (isDirectRun) {
31020
31220
  startMcpServer().catch((error48) => {
@@ -31043,6 +31243,7 @@ var {
31043
31243
  // src/cli/index.tsx
31044
31244
  init_messages();
31045
31245
  init_sessions();
31246
+ init_channels();
31046
31247
  init_db();
31047
31248
  import chalk2 from "chalk";
31048
31249
  import { render } from "ink";
@@ -31643,6 +31844,7 @@ function startPolling(opts) {
31643
31844
  const messages = readMessages({
31644
31845
  session_id: opts.session_id,
31645
31846
  to: opts.to_agent,
31847
+ channel: opts.channel,
31646
31848
  since: lastSeen
31647
31849
  });
31648
31850
  if (messages.length > 0) {
@@ -31904,7 +32106,7 @@ function App({ agent }) {
31904
32106
 
31905
32107
  // src/cli/index.tsx
31906
32108
  var program2 = new Command;
31907
- program2.name("convo").description("Real-time CLI messaging for AI agents").version("0.0.2");
32109
+ program2.name("conversations").description("Real-time CLI messaging for AI agents").version("0.0.4");
31908
32110
  program2.command("send").description("Send a message to an agent").argument("<message>", "Message content").requiredOption("--to <agent>", "Recipient agent ID").option("--from <agent>", "Sender agent ID").option("--session <id>", "Session ID (auto-generated if omitted)").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--working-dir <path>", "Working directory context").option("--repository <repo>", "Repository context").option("--branch <branch>", "Branch context").option("--metadata <json>", "JSON metadata string").option("--json", "Output as JSON").action((message, opts) => {
31909
32111
  const from = resolveIdentity(opts.from);
31910
32112
  const metadata = opts.metadata ? JSON.parse(opts.metadata) : undefined;
@@ -31926,11 +32128,12 @@ program2.command("send").description("Send a message to an agent").argument("<me
31926
32128
  }
31927
32129
  closeDb();
31928
32130
  });
31929
- program2.command("read").description("Read messages").option("--session <id>", "Filter by session ID").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--unread", "Only unread messages").option("--mark-read", "Mark returned messages as read").option("--json", "Output as JSON").action((opts) => {
32131
+ program2.command("read").description("Read messages").option("--session <id>", "Filter by session ID").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--channel <name>", "Filter by channel").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--unread", "Only unread messages").option("--mark-read", "Mark returned messages as read").option("--json", "Output as JSON").action((opts) => {
31930
32132
  const messages = readMessages({
31931
32133
  session_id: opts.session,
31932
32134
  from: opts.from,
31933
32135
  to: opts.to,
32136
+ channel: opts.channel,
31934
32137
  since: opts.since,
31935
32138
  limit: opts.limit,
31936
32139
  unread_only: opts.unread
@@ -31949,7 +32152,7 @@ program2.command("read").description("Read messages").option("--session <id>", "
31949
32152
  for (const msg of messages) {
31950
32153
  const time3 = chalk2.dim(msg.created_at.slice(11, 19));
31951
32154
  const from = chalk2.cyan(msg.from_agent);
31952
- const to = chalk2.yellow(msg.to_agent);
32155
+ const to = msg.channel ? chalk2.magenta(`#${msg.channel}`) : chalk2.yellow(msg.to_agent);
31953
32156
  const priority = msg.priority !== "normal" ? chalk2.red(` [${msg.priority}]`) : "";
31954
32157
  const unread = !msg.read_at ? chalk2.green(" *") : "";
31955
32158
  console.log(`${time3} ${from} \u2192 ${to}${priority}${unread}: ${msg.content}`);
@@ -31996,15 +32199,17 @@ program2.command("reply").description("Reply to a message (uses same session)").
31996
32199
  }
31997
32200
  closeDb();
31998
32201
  });
31999
- program2.command("mark-read").description("Mark messages as read").argument("[ids...]", "Message IDs to mark as read").option("--session <id>", "Mark all messages in session as read").option("--agent <id>", "Agent marking messages as read").option("--json", "Output as JSON").action((ids, opts) => {
32202
+ program2.command("mark-read").description("Mark messages as read").argument("[ids...]", "Message IDs to mark as read").option("--session <id>", "Mark all messages in session as read").option("--channel <name>", "Mark all messages in channel as read").option("--agent <id>", "Agent marking messages as read").option("--json", "Output as JSON").action((ids, opts) => {
32000
32203
  const agent = resolveIdentity(opts.agent);
32001
32204
  let count = 0;
32002
32205
  if (opts.session) {
32003
32206
  count = markSessionRead(opts.session, agent);
32207
+ } else if (opts.channel) {
32208
+ count = markChannelRead(opts.channel, agent);
32004
32209
  } else if (ids.length > 0) {
32005
32210
  count = markRead(ids.map(Number), agent);
32006
32211
  } else {
32007
- console.error(chalk2.red("Provide message IDs or --session flag."));
32212
+ console.error(chalk2.red("Provide message IDs, --session, or --channel flag."));
32008
32213
  process.exit(1);
32009
32214
  }
32010
32215
  if (opts.json) {
@@ -32020,10 +32225,12 @@ program2.command("status").description("Show database stats").option("--json", "
32020
32225
  const totalMessages = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
32021
32226
  const totalSessions = db2.prepare("SELECT COUNT(DISTINCT session_id) as count FROM messages").get().count;
32022
32227
  const totalUnread = db2.prepare("SELECT COUNT(*) as count FROM messages WHERE read_at IS NULL").get().count;
32228
+ const totalChannels = db2.prepare("SELECT COUNT(*) as count FROM channels").get().count;
32023
32229
  const stats = {
32024
32230
  db_path: dbPath,
32025
32231
  total_messages: totalMessages,
32026
32232
  total_sessions: totalSessions,
32233
+ total_channels: totalChannels,
32027
32234
  unread_messages: totalUnread
32028
32235
  };
32029
32236
  if (opts.json) {
@@ -32033,10 +32240,134 @@ program2.command("status").description("Show database stats").option("--json", "
32033
32240
  console.log(` DB Path: ${stats.db_path}`);
32034
32241
  console.log(` Messages: ${stats.total_messages}`);
32035
32242
  console.log(` Sessions: ${stats.total_sessions}`);
32243
+ console.log(` Channels: ${stats.total_channels}`);
32036
32244
  console.log(` Unread: ${stats.unread_messages}`);
32037
32245
  }
32038
32246
  closeDb();
32039
32247
  });
32248
+ var channel = program2.command("channel").description("Manage channels");
32249
+ channel.command("create").description("Create a new channel").argument("<name>", "Channel name").option("--description <text>", "Channel description").option("--from <agent>", "Creator agent ID").option("--json", "Output as JSON").action((name, opts) => {
32250
+ const agent = resolveIdentity(opts.from);
32251
+ try {
32252
+ const ch = createChannel(name, agent, opts.description);
32253
+ if (opts.json) {
32254
+ console.log(JSON.stringify(ch, null, 2));
32255
+ } else {
32256
+ console.log(chalk2.green(`Channel #${ch.name} created`) + (ch.description ? chalk2.dim(` \u2014 ${ch.description}`) : ""));
32257
+ }
32258
+ } catch (e) {
32259
+ if (e.message?.includes("UNIQUE constraint")) {
32260
+ console.error(chalk2.red(`Channel #${name} already exists.`));
32261
+ process.exit(1);
32262
+ }
32263
+ throw e;
32264
+ }
32265
+ closeDb();
32266
+ });
32267
+ channel.command("list").description("List all channels").option("--json", "Output as JSON").action((opts) => {
32268
+ const channels = listChannels();
32269
+ if (opts.json) {
32270
+ console.log(JSON.stringify(channels, null, 2));
32271
+ } else {
32272
+ if (channels.length === 0) {
32273
+ console.log(chalk2.dim("No channels found."));
32274
+ } else {
32275
+ for (const ch of channels) {
32276
+ const desc = ch.description ? chalk2.dim(` \u2014 ${ch.description}`) : "";
32277
+ console.log(`${chalk2.magenta(`#${ch.name}`)}${desc} ${ch.member_count} members, ${ch.message_count} messages`);
32278
+ }
32279
+ }
32280
+ }
32281
+ closeDb();
32282
+ });
32283
+ channel.command("send").description("Send a message to a channel").argument("<channel>", "Channel name").argument("<message>", "Message content").option("--from <agent>", "Sender agent ID").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--json", "Output as JSON").action((channelName, message, opts) => {
32284
+ const from = resolveIdentity(opts.from);
32285
+ const ch = getChannel(channelName);
32286
+ if (!ch) {
32287
+ console.error(chalk2.red(`Channel #${channelName} not found.`));
32288
+ process.exit(1);
32289
+ }
32290
+ const msg = sendMessage({
32291
+ from,
32292
+ to: channelName,
32293
+ content: message,
32294
+ channel: channelName,
32295
+ session_id: `channel:${channelName}`,
32296
+ priority: opts.priority
32297
+ });
32298
+ if (opts.json) {
32299
+ console.log(JSON.stringify(msg, null, 2));
32300
+ } else {
32301
+ console.log(chalk2.green(`Message sent to #${channelName}`) + chalk2.dim(` (id: ${msg.id})`));
32302
+ }
32303
+ closeDb();
32304
+ });
32305
+ channel.command("read").description("Read messages from a channel").argument("<channel>", "Channel name").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--json", "Output as JSON").action((channelName, opts) => {
32306
+ const messages = readMessages({
32307
+ channel: channelName,
32308
+ since: opts.since,
32309
+ limit: opts.limit
32310
+ });
32311
+ if (opts.json) {
32312
+ console.log(JSON.stringify(messages, null, 2));
32313
+ } else {
32314
+ if (messages.length === 0) {
32315
+ console.log(chalk2.dim(`No messages in #${channelName}.`));
32316
+ } else {
32317
+ for (const msg of messages) {
32318
+ const time3 = chalk2.dim(msg.created_at.slice(11, 19));
32319
+ const from = chalk2.cyan(msg.from_agent);
32320
+ const priority = msg.priority !== "normal" ? chalk2.red(` [${msg.priority}]`) : "";
32321
+ console.log(`${time3} ${from} \u2192 ${chalk2.magenta(`#${channelName}`)}${priority}: ${msg.content}`);
32322
+ }
32323
+ }
32324
+ }
32325
+ closeDb();
32326
+ });
32327
+ channel.command("join").description("Join a channel").argument("<channel>", "Channel name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((channelName, opts) => {
32328
+ const agent = resolveIdentity(opts.from);
32329
+ const ok = joinChannel(channelName, agent);
32330
+ if (!ok) {
32331
+ console.error(chalk2.red(`Channel #${channelName} not found.`));
32332
+ process.exit(1);
32333
+ }
32334
+ if (opts.json) {
32335
+ console.log(JSON.stringify({ channel: channelName, agent, joined: true }));
32336
+ } else {
32337
+ console.log(chalk2.green(`${agent} joined #${channelName}`));
32338
+ }
32339
+ closeDb();
32340
+ });
32341
+ channel.command("leave").description("Leave a channel").argument("<channel>", "Channel name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((channelName, opts) => {
32342
+ const agent = resolveIdentity(opts.from);
32343
+ const ok = leaveChannel(channelName, agent);
32344
+ if (opts.json) {
32345
+ console.log(JSON.stringify({ channel: channelName, agent, left: ok }));
32346
+ } else {
32347
+ if (ok) {
32348
+ console.log(chalk2.green(`${agent} left #${channelName}`));
32349
+ } else {
32350
+ console.log(chalk2.dim(`${agent} was not a member of #${channelName}`));
32351
+ }
32352
+ }
32353
+ closeDb();
32354
+ });
32355
+ channel.command("members").description("List channel members").argument("<channel>", "Channel name").option("--json", "Output as JSON").action((channelName, opts) => {
32356
+ const members = getChannelMembers(channelName);
32357
+ if (opts.json) {
32358
+ console.log(JSON.stringify(members, null, 2));
32359
+ } else {
32360
+ if (members.length === 0) {
32361
+ console.log(chalk2.dim(`No members in #${channelName}.`));
32362
+ } else {
32363
+ console.log(chalk2.magenta(`#${channelName}`) + chalk2.dim(` \u2014 ${members.length} member(s)`));
32364
+ for (const m of members) {
32365
+ console.log(` ${chalk2.cyan(m.agent)} ${chalk2.dim(`joined ${m.joined_at.slice(0, 10)}`)}`);
32366
+ }
32367
+ }
32368
+ }
32369
+ closeDb();
32370
+ });
32040
32371
  program2.command("mcp").description("Start MCP server").action(async () => {
32041
32372
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
32042
32373
  await startMcpServer2();
package/bin/mcp.js CHANGED
@@ -28339,6 +28339,7 @@ function getDb() {
28339
28339
  session_id TEXT NOT NULL,
28340
28340
  from_agent TEXT NOT NULL,
28341
28341
  to_agent TEXT NOT NULL,
28342
+ channel TEXT,
28342
28343
  content TEXT NOT NULL,
28343
28344
  priority TEXT NOT NULL DEFAULT 'normal',
28344
28345
  working_dir TEXT,
@@ -28352,6 +28353,27 @@ function getDb() {
28352
28353
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
28353
28354
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
28354
28355
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
28356
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
28357
+ const cols = db.prepare("PRAGMA table_info(messages)").all();
28358
+ if (!cols.some((c) => c.name === "channel")) {
28359
+ db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
28360
+ }
28361
+ db.exec(`
28362
+ CREATE TABLE IF NOT EXISTS channels (
28363
+ name TEXT PRIMARY KEY,
28364
+ description TEXT,
28365
+ created_by TEXT NOT NULL,
28366
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
28367
+ )
28368
+ `);
28369
+ db.exec(`
28370
+ CREATE TABLE IF NOT EXISTS channel_members (
28371
+ channel TEXT NOT NULL REFERENCES channels(name),
28372
+ agent TEXT NOT NULL,
28373
+ joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
28374
+ PRIMARY KEY (channel, agent)
28375
+ )
28376
+ `);
28355
28377
  return db;
28356
28378
  }
28357
28379
 
@@ -28368,11 +28390,11 @@ function sendMessage(opts) {
28368
28390
  const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
28369
28391
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
28370
28392
  const stmt = db2.prepare(`
28371
- INSERT INTO messages (session_id, from_agent, to_agent, content, priority, working_dir, repository, branch, metadata)
28372
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
28393
+ INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
28394
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28373
28395
  RETURNING *
28374
28396
  `);
28375
- const row = stmt.get(sessionId, opts.from, opts.to, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
28397
+ const row = stmt.get(sessionId, opts.from, opts.to, opts.channel || null, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
28376
28398
  return parseMessage(row);
28377
28399
  }
28378
28400
  function readMessages(opts = {}) {
@@ -28391,6 +28413,10 @@ function readMessages(opts = {}) {
28391
28413
  conditions.push("to_agent = ?");
28392
28414
  params.push(opts.to);
28393
28415
  }
28416
+ if (opts.channel) {
28417
+ conditions.push("channel = ?");
28418
+ params.push(opts.channel);
28419
+ }
28394
28420
  if (opts.since) {
28395
28421
  conditions.push("created_at > ?");
28396
28422
  params.push(opts.since);
@@ -28448,6 +28474,57 @@ function listSessions(agent) {
28448
28474
  });
28449
28475
  }
28450
28476
 
28477
+ // src/lib/channels.ts
28478
+ function createChannel(name, createdBy, description) {
28479
+ const db2 = getDb();
28480
+ const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
28481
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
28482
+ return row;
28483
+ }
28484
+ function listChannels() {
28485
+ const db2 = getDb();
28486
+ const rows = db2.prepare(`
28487
+ SELECT
28488
+ c.name,
28489
+ c.description,
28490
+ c.created_by,
28491
+ c.created_at,
28492
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
28493
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
28494
+ FROM channels c
28495
+ ORDER BY c.name ASC
28496
+ `).all();
28497
+ return rows;
28498
+ }
28499
+ function getChannel(name) {
28500
+ const db2 = getDb();
28501
+ const row = db2.prepare(`
28502
+ SELECT
28503
+ c.name,
28504
+ c.description,
28505
+ c.created_by,
28506
+ c.created_at,
28507
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
28508
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
28509
+ FROM channels c
28510
+ WHERE c.name = ?
28511
+ `).get(name);
28512
+ return row;
28513
+ }
28514
+ function joinChannel(channelName, agent) {
28515
+ const db2 = getDb();
28516
+ const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
28517
+ if (!channel)
28518
+ return false;
28519
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
28520
+ return true;
28521
+ }
28522
+ function leaveChannel(channelName, agent) {
28523
+ const db2 = getDb();
28524
+ const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
28525
+ return result.changes > 0;
28526
+ }
28527
+
28451
28528
  // src/lib/identity.ts
28452
28529
  function resolveIdentity(explicit) {
28453
28530
  if (explicit)
@@ -28460,11 +28537,11 @@ function resolveIdentity(explicit) {
28460
28537
  // src/mcp/index.ts
28461
28538
  var server = new McpServer({
28462
28539
  name: "conversations",
28463
- version: "0.0.2"
28540
+ version: "0.0.4"
28464
28541
  });
28465
28542
  server.registerTool("send_message", {
28466
28543
  title: "Send Message",
28467
- description: "Send a message to another agent. The sender is auto-resolved from CONVERSATIONS_AGENT_ID env var.",
28544
+ description: "Send a direct message to another agent. The sender is auto-resolved from CONVERSATIONS_AGENT_ID env var.",
28468
28545
  inputSchema: {
28469
28546
  to: exports_external.string().describe("Recipient agent ID"),
28470
28547
  content: exports_external.string().describe("Message content"),
@@ -28500,6 +28577,7 @@ server.registerTool("read_messages", {
28500
28577
  session_id: exports_external.string().optional().describe("Filter by session ID"),
28501
28578
  from: exports_external.string().optional().describe("Filter by sender agent ID"),
28502
28579
  to: exports_external.string().optional().describe("Filter by recipient agent ID"),
28580
+ channel: exports_external.string().optional().describe("Filter by channel name"),
28503
28581
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
28504
28582
  limit: exports_external.number().optional().describe("Max messages to return"),
28505
28583
  unread_only: exports_external.boolean().optional().describe("Only return unread messages")
@@ -28563,6 +28641,114 @@ server.registerTool("mark_read", {
28563
28641
  content: [{ type: "text", text: JSON.stringify({ marked_read: count }, null, 2) }]
28564
28642
  };
28565
28643
  });
28644
+ server.registerTool("create_channel", {
28645
+ title: "Create Channel",
28646
+ description: "Create a new channel. The creator is auto-joined.",
28647
+ inputSchema: {
28648
+ name: exports_external.string().describe("Channel name (e.g. 'deployments', 'code-review')"),
28649
+ description: exports_external.string().optional().describe("Channel description")
28650
+ }
28651
+ }, async ({ name, description }) => {
28652
+ const agent = resolveIdentity();
28653
+ try {
28654
+ const ch = createChannel(name, agent, description);
28655
+ return {
28656
+ content: [{ type: "text", text: JSON.stringify(ch, null, 2) }]
28657
+ };
28658
+ } catch (e) {
28659
+ if (e.message?.includes("UNIQUE constraint")) {
28660
+ return {
28661
+ content: [{ type: "text", text: `Channel #${name} already exists` }],
28662
+ isError: true
28663
+ };
28664
+ }
28665
+ throw e;
28666
+ }
28667
+ });
28668
+ server.registerTool("list_channels", {
28669
+ title: "List Channels",
28670
+ description: "List all available channels with member and message counts."
28671
+ }, async () => {
28672
+ const channels = listChannels();
28673
+ return {
28674
+ content: [{ type: "text", text: JSON.stringify(channels, null, 2) }]
28675
+ };
28676
+ });
28677
+ server.registerTool("send_to_channel", {
28678
+ title: "Send to Channel",
28679
+ description: "Send a message to a channel. All members can see it.",
28680
+ inputSchema: {
28681
+ channel: exports_external.string().describe("Channel name"),
28682
+ content: exports_external.string().describe("Message content"),
28683
+ priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional().describe("Message priority")
28684
+ }
28685
+ }, async ({ channel, content, priority }) => {
28686
+ const from = resolveIdentity();
28687
+ const ch = getChannel(channel);
28688
+ if (!ch) {
28689
+ return {
28690
+ content: [{ type: "text", text: `Channel #${channel} not found` }],
28691
+ isError: true
28692
+ };
28693
+ }
28694
+ const msg = sendMessage({
28695
+ from,
28696
+ to: channel,
28697
+ content,
28698
+ channel,
28699
+ session_id: `channel:${channel}`,
28700
+ priority
28701
+ });
28702
+ return {
28703
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
28704
+ };
28705
+ });
28706
+ server.registerTool("read_channel", {
28707
+ title: "Read Channel",
28708
+ description: "Read messages from a channel.",
28709
+ inputSchema: {
28710
+ channel: exports_external.string().describe("Channel name"),
28711
+ since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
28712
+ limit: exports_external.number().optional().describe("Max messages to return")
28713
+ }
28714
+ }, async ({ channel, since, limit }) => {
28715
+ const messages = readMessages({ channel, since, limit });
28716
+ return {
28717
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
28718
+ };
28719
+ });
28720
+ server.registerTool("join_channel", {
28721
+ title: "Join Channel",
28722
+ description: "Join a channel to receive messages.",
28723
+ inputSchema: {
28724
+ channel: exports_external.string().describe("Channel name to join")
28725
+ }
28726
+ }, async ({ channel }) => {
28727
+ const agent = resolveIdentity();
28728
+ const ok = joinChannel(channel, agent);
28729
+ if (!ok) {
28730
+ return {
28731
+ content: [{ type: "text", text: `Channel #${channel} not found` }],
28732
+ isError: true
28733
+ };
28734
+ }
28735
+ return {
28736
+ content: [{ type: "text", text: JSON.stringify({ channel, agent, joined: true }, null, 2) }]
28737
+ };
28738
+ });
28739
+ server.registerTool("leave_channel", {
28740
+ title: "Leave Channel",
28741
+ description: "Leave a channel.",
28742
+ inputSchema: {
28743
+ channel: exports_external.string().describe("Channel name to leave")
28744
+ }
28745
+ }, async ({ channel }) => {
28746
+ const agent = resolveIdentity();
28747
+ const left = leaveChannel(channel, agent);
28748
+ return {
28749
+ content: [{ type: "text", text: JSON.stringify({ channel, agent, left }, null, 2) }]
28750
+ };
28751
+ });
28566
28752
  async function startMcpServer() {
28567
28753
  const transport = new StdioServerTransport;
28568
28754
  await server.connect(transport);
package/dist/index.d.ts CHANGED
@@ -2,15 +2,17 @@
2
2
  * @hasna/conversations - Real-time CLI messaging for AI agents
3
3
  *
4
4
  * Send and receive messages between AI agents on the same machine:
5
- * convo send --to claude-code "hello from codex"
6
- * convo read --to codex --json
5
+ * conversations send --to claude-code "hello from codex"
6
+ * conversations read --to codex --json
7
+ * conversations channel send deployments "v1.2 deployed"
7
8
  *
8
9
  * Or use the interactive TUI:
9
- * convo
10
+ * conversations
10
11
  */
11
- export { sendMessage, readMessages, markRead, markSessionRead, getMessageById, } from "./lib/messages.js";
12
+ export { sendMessage, readMessages, markRead, markSessionRead, markChannelRead, getMessageById, } from "./lib/messages.js";
12
13
  export { listSessions, getSession, } from "./lib/sessions.js";
14
+ export { createChannel, listChannels, getChannel, joinChannel, leaveChannel, getChannelMembers, isChannelMember, } from "./lib/channels.js";
13
15
  export { getDb, getDbPath, closeDb, } from "./lib/db.js";
14
- export { startPolling, } from "./lib/poll.js";
16
+ export { startPolling, useChannelMessages, } from "./lib/poll.js";
15
17
  export { resolveIdentity, requireIdentity, } from "./lib/identity.js";
16
- export type { Message, Session, Priority, SendMessageOptions, ReadMessagesOptions, } from "./types.js";
18
+ export type { Message, Session, Channel, ChannelInfo, ChannelMember, Priority, SendMessageOptions, ReadMessagesOptions, } from "./types.js";
package/dist/index.js CHANGED
@@ -1851,6 +1851,7 @@ function getDb() {
1851
1851
  session_id TEXT NOT NULL,
1852
1852
  from_agent TEXT NOT NULL,
1853
1853
  to_agent TEXT NOT NULL,
1854
+ channel TEXT,
1854
1855
  content TEXT NOT NULL,
1855
1856
  priority TEXT NOT NULL DEFAULT 'normal',
1856
1857
  working_dir TEXT,
@@ -1864,6 +1865,27 @@ function getDb() {
1864
1865
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
1865
1866
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
1866
1867
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
1868
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
1869
+ const cols = db.prepare("PRAGMA table_info(messages)").all();
1870
+ if (!cols.some((c) => c.name === "channel")) {
1871
+ db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
1872
+ }
1873
+ db.exec(`
1874
+ CREATE TABLE IF NOT EXISTS channels (
1875
+ name TEXT PRIMARY KEY,
1876
+ description TEXT,
1877
+ created_by TEXT NOT NULL,
1878
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
1879
+ )
1880
+ `);
1881
+ db.exec(`
1882
+ CREATE TABLE IF NOT EXISTS channel_members (
1883
+ channel TEXT NOT NULL REFERENCES channels(name),
1884
+ agent TEXT NOT NULL,
1885
+ joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1886
+ PRIMARY KEY (channel, agent)
1887
+ )
1888
+ `);
1867
1889
  return db;
1868
1890
  }
1869
1891
  function closeDb() {
@@ -1886,11 +1908,11 @@ function sendMessage(opts) {
1886
1908
  const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
1887
1909
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
1888
1910
  const stmt = db2.prepare(`
1889
- INSERT INTO messages (session_id, from_agent, to_agent, content, priority, working_dir, repository, branch, metadata)
1890
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1911
+ INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
1912
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1891
1913
  RETURNING *
1892
1914
  `);
1893
- const row = stmt.get(sessionId, opts.from, opts.to, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
1915
+ const row = stmt.get(sessionId, opts.from, opts.to, opts.channel || null, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
1894
1916
  return parseMessage(row);
1895
1917
  }
1896
1918
  function readMessages(opts = {}) {
@@ -1909,6 +1931,10 @@ function readMessages(opts = {}) {
1909
1931
  conditions.push("to_agent = ?");
1910
1932
  params.push(opts.to);
1911
1933
  }
1934
+ if (opts.channel) {
1935
+ conditions.push("channel = ?");
1936
+ params.push(opts.channel);
1937
+ }
1912
1938
  if (opts.since) {
1913
1939
  conditions.push("created_at > ?");
1914
1940
  params.push(opts.since);
@@ -1936,6 +1962,12 @@ function markSessionRead(sessionId, reader) {
1936
1962
  const result = stmt.run(sessionId, reader);
1937
1963
  return result.changes;
1938
1964
  }
1965
+ function markChannelRead(channelName, reader) {
1966
+ const db2 = getDb();
1967
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE channel = ? AND from_agent != ? AND read_at IS NULL`);
1968
+ const result = stmt.run(channelName, reader);
1969
+ return result.changes;
1970
+ }
1939
1971
  function getMessageById(id) {
1940
1972
  const db2 = getDb();
1941
1973
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
@@ -1995,6 +2027,65 @@ function getSession(sessionId) {
1995
2027
  unread_count: row.unread_count
1996
2028
  };
1997
2029
  }
2030
+ // src/lib/channels.ts
2031
+ function createChannel(name, createdBy, description) {
2032
+ const db2 = getDb();
2033
+ const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
2034
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
2035
+ return row;
2036
+ }
2037
+ function listChannels() {
2038
+ const db2 = getDb();
2039
+ const rows = db2.prepare(`
2040
+ SELECT
2041
+ c.name,
2042
+ c.description,
2043
+ c.created_by,
2044
+ c.created_at,
2045
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2046
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2047
+ FROM channels c
2048
+ ORDER BY c.name ASC
2049
+ `).all();
2050
+ return rows;
2051
+ }
2052
+ function getChannel(name) {
2053
+ const db2 = getDb();
2054
+ const row = db2.prepare(`
2055
+ SELECT
2056
+ c.name,
2057
+ c.description,
2058
+ c.created_by,
2059
+ c.created_at,
2060
+ (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2061
+ (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2062
+ FROM channels c
2063
+ WHERE c.name = ?
2064
+ `).get(name);
2065
+ return row;
2066
+ }
2067
+ function joinChannel(channelName, agent) {
2068
+ const db2 = getDb();
2069
+ const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
2070
+ if (!channel)
2071
+ return false;
2072
+ db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
2073
+ return true;
2074
+ }
2075
+ function leaveChannel(channelName, agent) {
2076
+ const db2 = getDb();
2077
+ const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
2078
+ return result.changes > 0;
2079
+ }
2080
+ function getChannelMembers(channelName) {
2081
+ const db2 = getDb();
2082
+ return db2.prepare("SELECT channel, agent, joined_at FROM channel_members WHERE channel = ? ORDER BY joined_at ASC").all(channelName);
2083
+ }
2084
+ function isChannelMember(channelName, agent) {
2085
+ const db2 = getDb();
2086
+ const row = db2.prepare("SELECT 1 FROM channel_members WHERE channel = ? AND agent = ?").get(channelName, agent);
2087
+ return !!row;
2088
+ }
1998
2089
  // src/lib/poll.ts
1999
2090
  var import_react = __toESM(require_react(), 1);
2000
2091
  function startPolling(opts) {
@@ -2007,6 +2098,7 @@ function startPolling(opts) {
2007
2098
  const messages = readMessages({
2008
2099
  session_id: opts.session_id,
2009
2100
  to: opts.to_agent,
2101
+ channel: opts.channel,
2010
2102
  since: lastSeen
2011
2103
  });
2012
2104
  if (messages.length > 0) {
@@ -2022,6 +2114,26 @@ function startPolling(opts) {
2022
2114
  }
2023
2115
  };
2024
2116
  }
2117
+ function useChannelMessages(channelName) {
2118
+ const [messages, setMessages] = import_react.useState([]);
2119
+ const initialLoad = import_react.useRef(false);
2120
+ import_react.useEffect(() => {
2121
+ if (!initialLoad.current) {
2122
+ const existing = readMessages({ channel: channelName });
2123
+ setMessages(existing);
2124
+ initialLoad.current = true;
2125
+ }
2126
+ const { stop } = startPolling({
2127
+ channel: channelName,
2128
+ interval_ms: 200,
2129
+ on_messages: (newMessages) => {
2130
+ setMessages((prev) => [...prev, ...newMessages]);
2131
+ }
2132
+ });
2133
+ return stop;
2134
+ }, [channelName]);
2135
+ return messages;
2136
+ }
2025
2137
  // src/lib/identity.ts
2026
2138
  function resolveIdentity(explicit) {
2027
2139
  if (explicit)
@@ -2038,6 +2150,7 @@ function requireIdentity(explicit) {
2038
2150
  throw new Error("Agent identity required. Set CONVERSATIONS_AGENT_ID env var or pass --from flag.");
2039
2151
  }
2040
2152
  export {
2153
+ useChannelMessages,
2041
2154
  startPolling,
2042
2155
  sendMessage,
2043
2156
  resolveIdentity,
@@ -2045,10 +2158,18 @@ export {
2045
2158
  readMessages,
2046
2159
  markSessionRead,
2047
2160
  markRead,
2161
+ markChannelRead,
2048
2162
  listSessions,
2163
+ listChannels,
2164
+ leaveChannel,
2165
+ joinChannel,
2166
+ isChannelMember,
2049
2167
  getSession,
2050
2168
  getMessageById,
2051
2169
  getDbPath,
2052
2170
  getDb,
2171
+ getChannelMembers,
2172
+ getChannel,
2173
+ createChannel,
2053
2174
  closeDb
2054
2175
  };
@@ -0,0 +1,8 @@
1
+ import type { Channel, ChannelInfo, ChannelMember } from "../types.js";
2
+ export declare function createChannel(name: string, createdBy: string, description?: string): Channel;
3
+ export declare function listChannels(): ChannelInfo[];
4
+ export declare function getChannel(name: string): ChannelInfo | null;
5
+ export declare function joinChannel(channelName: string, agent: string): boolean;
6
+ export declare function leaveChannel(channelName: string, agent: string): boolean;
7
+ export declare function getChannelMembers(channelName: string): ChannelMember[];
8
+ export declare function isChannelMember(channelName: string, agent: string): boolean;
@@ -3,4 +3,5 @@ export declare function sendMessage(opts: SendMessageOptions): Message;
3
3
  export declare function readMessages(opts?: ReadMessagesOptions): Message[];
4
4
  export declare function markRead(ids: number[], reader: string): number;
5
5
  export declare function markSessionRead(sessionId: string, reader: string): number;
6
+ export declare function markChannelRead(channelName: string, reader: string): number;
6
7
  export declare function getMessageById(id: number): Message | null;
@@ -2,6 +2,7 @@ import type { Message } from "../types.js";
2
2
  export interface PollOptions {
3
3
  session_id?: string;
4
4
  to_agent?: string;
5
+ channel?: string;
5
6
  interval_ms?: number;
6
7
  on_messages: (messages: Message[]) => void;
7
8
  }
@@ -15,3 +16,7 @@ export declare function startPolling(opts: PollOptions): {
15
16
  * React hook for polling messages in a session.
16
17
  */
17
18
  export declare function useMessages(sessionId: string, agent?: string): Message[];
19
+ /**
20
+ * React hook for polling messages in a channel.
21
+ */
22
+ export declare function useChannelMessages(channelName: string): Message[];
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * MCP server for conversations.
4
- * Exposes tools for sending, reading, and managing messages between agents.
4
+ * Exposes tools for sending, reading, and managing messages and channels between agents.
5
5
  *
6
6
  * Usage:
7
- * convo mcp # Start MCP server on stdio
8
- * convo-mcp # Direct binary
7
+ * conversations mcp # Start MCP server on stdio
8
+ * conversations-mcp # Direct binary
9
9
  */
10
10
  export declare function startMcpServer(): Promise<void>;
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface Message {
4
4
  session_id: string;
5
5
  from_agent: string;
6
6
  to_agent: string;
7
+ channel: string | null;
7
8
  content: string;
8
9
  priority: Priority;
9
10
  working_dir: string | null;
@@ -20,11 +21,27 @@ export interface Session {
20
21
  message_count: number;
21
22
  unread_count: number;
22
23
  }
24
+ export interface Channel {
25
+ name: string;
26
+ description: string | null;
27
+ created_by: string;
28
+ created_at: string;
29
+ }
30
+ export interface ChannelMember {
31
+ channel: string;
32
+ agent: string;
33
+ joined_at: string;
34
+ }
35
+ export interface ChannelInfo extends Channel {
36
+ member_count: number;
37
+ message_count: number;
38
+ }
23
39
  export interface SendMessageOptions {
24
40
  from: string;
25
41
  to: string;
26
42
  content: string;
27
43
  session_id?: string;
44
+ channel?: string;
28
45
  priority?: Priority;
29
46
  working_dir?: string;
30
47
  repository?: string;
@@ -35,6 +52,7 @@ export interface ReadMessagesOptions {
35
52
  session_id?: string;
36
53
  from?: string;
37
54
  to?: string;
55
+ channel?: string;
38
56
  since?: string;
39
57
  limit?: number;
40
58
  unread_only?: boolean;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
7
- "convo": "./bin/index.js",
8
- "convo-mcp": "./bin/mcp.js"
7
+ "conversations": "bin/index.js",
8
+ "conversations-mcp": "bin/mcp.js"
9
9
  },
10
10
  "exports": {
11
11
  ".": {