@hasna/conversations 0.2.5 → 0.2.6

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/bin/hook.js CHANGED
@@ -252,6 +252,20 @@ function getDb() {
252
252
  if (!presenceColNames.includes("project_id")) {
253
253
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
254
254
  }
255
+ db.exec(`
256
+ CREATE TABLE IF NOT EXISTS message_mentions (
257
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
258
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
259
+ mentioned_agent TEXT NOT NULL,
260
+ from_agent TEXT NOT NULL,
261
+ space TEXT,
262
+ notified_at TEXT,
263
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
264
+ )
265
+ `);
266
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
267
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
268
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
255
269
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
256
270
  if (!ftsExists) {
257
271
  db.exec(`
package/bin/index.js CHANGED
@@ -2117,6 +2117,20 @@ function getDb() {
2117
2117
  if (!presenceColNames.includes("project_id")) {
2118
2118
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
2119
2119
  }
2120
+ db.exec(`
2121
+ CREATE TABLE IF NOT EXISTS message_mentions (
2122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2123
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
2124
+ mentioned_agent TEXT NOT NULL,
2125
+ from_agent TEXT NOT NULL,
2126
+ space TEXT,
2127
+ notified_at TEXT,
2128
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
2129
+ )
2130
+ `);
2131
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
2132
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
2133
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
2120
2134
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
2121
2135
  if (!ftsExists) {
2122
2136
  db.exec(`
@@ -2330,6 +2344,12 @@ function sendMessage(opts) {
2330
2344
  db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
2331
2345
  message.attachments = attachmentInfos;
2332
2346
  }
2347
+ if (opts.space) {
2348
+ const mentions = parseMentions(opts.content);
2349
+ if (mentions.length > 0) {
2350
+ processMentions(message.id, opts.from, opts.space, mentions, db2);
2351
+ }
2352
+ }
2333
2353
  fireWebhooks(message);
2334
2354
  return message;
2335
2355
  }
@@ -2371,6 +2391,10 @@ function readMessages(opts = {}) {
2371
2391
  if (opts.threads_only) {
2372
2392
  conditions.push("reply_to IS NULL");
2373
2393
  }
2394
+ if (opts.mentions_only) {
2395
+ conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
2396
+ params.push(opts.mentions_only.toLowerCase());
2397
+ }
2374
2398
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2375
2399
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
2376
2400
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
@@ -2694,6 +2718,73 @@ function listUnreadCounts(agent) {
2694
2718
  `).all();
2695
2719
  return rows;
2696
2720
  }
2721
+ function parseMentions(content) {
2722
+ const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
2723
+ return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
2724
+ }
2725
+ async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
2726
+ const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
2727
+ for (const agent of mentionedAgents) {
2728
+ try {
2729
+ stmt.run(messageId, agent, fromAgent, space);
2730
+ if (agent !== fromAgent.toLowerCase()) {
2731
+ sendMessage({
2732
+ from: fromAgent,
2733
+ to: agent,
2734
+ content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
2735
+ metadata: { type: "mention_notification", source_message_id: messageId, space }
2736
+ });
2737
+ }
2738
+ } catch {}
2739
+ }
2740
+ }
2741
+ function listUnreadCountsWithMentions(agent) {
2742
+ const db2 = getDb();
2743
+ const rows = db2.prepare(`
2744
+ SELECT
2745
+ space,
2746
+ COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
2747
+ (SELECT COUNT(*) FROM message_mentions mm WHERE mm.space = m.space AND mm.mentioned_agent = ? AND mm.notified_at IS NULL) AS mention_count,
2748
+ MAX(created_at) AS latest_message_at
2749
+ FROM messages m
2750
+ WHERE space IN (
2751
+ SELECT DISTINCT space FROM space_members WHERE agent = ?
2752
+ UNION
2753
+ SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
2754
+ )
2755
+ GROUP BY space
2756
+ HAVING COUNT(*) > 0
2757
+ ORDER BY mention_count DESC, unread_count DESC, latest_message_at DESC
2758
+ `).all(agent, agent, agent, agent);
2759
+ return rows;
2760
+ }
2761
+ function getMessagesForAgent(agent, opts) {
2762
+ const db2 = getDb();
2763
+ const conditions = ["mm.mentioned_agent = ?"];
2764
+ const params = [agent.toLowerCase()];
2765
+ if (opts?.space) {
2766
+ conditions.push("m.space = ?");
2767
+ params.push(opts.space);
2768
+ }
2769
+ if (opts?.unread_only) {
2770
+ conditions.push("mm.notified_at IS NULL");
2771
+ }
2772
+ const limit = opts?.limit ?? 50;
2773
+ const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
2774
+ JOIN message_mentions mm ON mm.message_id = m.id
2775
+ WHERE ${conditions.join(" AND ")}
2776
+ ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
2777
+ return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
2778
+ }
2779
+ function markMentionsRead(agent, space) {
2780
+ const db2 = getDb();
2781
+ if (space) {
2782
+ const result2 = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND space = ? AND notified_at IS NULL").run(agent, space);
2783
+ return result2.changes;
2784
+ }
2785
+ const result = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND notified_at IS NULL").run(agent);
2786
+ return result.changes;
2787
+ }
2697
2788
  var init_messages = __esm(() => {
2698
2789
  init_db();
2699
2790
  init_webhooks();
@@ -4333,7 +4424,7 @@ var init_poll = __esm(() => {
4333
4424
  var require_package = __commonJS((exports, module) => {
4334
4425
  module.exports = {
4335
4426
  name: "@hasna/conversations",
4336
- version: "0.2.5",
4427
+ version: "0.2.6",
4337
4428
  description: "Real-time CLI messaging for AI agents",
4338
4429
  type: "module",
4339
4430
  bin: {
@@ -33588,7 +33679,8 @@ var init_mcp2 = __esm(() => {
33588
33679
  mark_read: exports_external.coerce.boolean().optional(),
33589
33680
  max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
33590
33681
  threads_only: exports_external.coerce.boolean().optional().describe("Only return root messages (reply_to IS NULL) \u2014 hides thread replies"),
33591
- include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)")
33682
+ include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)"),
33683
+ mentions_only: exports_external.string().optional().describe("Only return messages that @mention this agent")
33592
33684
  }
33593
33685
  }, async (args) => {
33594
33686
  const agent = resolveIdentity(args.from);
@@ -33760,13 +33852,42 @@ var init_mcp2 = __esm(() => {
33760
33852
  server.registerTool("list_unread_counts", {
33761
33853
  description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
33762
33854
  inputSchema: {
33763
- agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts.")
33855
+ agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts."),
33856
+ include_mentions: exports_external.coerce.boolean().optional().describe("Include mention_count per space (requires agent)")
33764
33857
  }
33765
33858
  }, async (args) => {
33859
+ if (args.agent && args.include_mentions) {
33860
+ const counts2 = listUnreadCountsWithMentions(args.agent);
33861
+ return { content: [{ type: "text", text: JSON.stringify(counts2) }] };
33862
+ }
33766
33863
  const counts = listUnreadCounts(args.agent);
33767
- return {
33768
- content: [{ type: "text", text: JSON.stringify(counts) }]
33769
- };
33864
+ return { content: [{ type: "text", text: JSON.stringify(counts) }] };
33865
+ });
33866
+ server.registerTool("get_mentions", {
33867
+ description: "Get messages that @mention a specific agent. Useful for catching up on missed pings.",
33868
+ inputSchema: {
33869
+ agent: exports_external.string().describe("Agent name to find mentions for"),
33870
+ space: exports_external.string().optional().describe("Filter to a specific space"),
33871
+ unread_only: exports_external.coerce.boolean().optional().describe("Only unread (not yet notified) mentions (default: true)"),
33872
+ limit: exports_external.coerce.number().optional().describe("Max results (default: 50)")
33873
+ }
33874
+ }, async (args) => {
33875
+ const results = getMessagesForAgent(args.agent, {
33876
+ space: args.space,
33877
+ unread_only: args.unread_only ?? true,
33878
+ limit: args.limit
33879
+ });
33880
+ return { content: [{ type: "text", text: JSON.stringify({ mentions: results, count: results.length }) }] };
33881
+ });
33882
+ server.registerTool("mark_mentions_read", {
33883
+ description: "Mark @mentions as seen for an agent. Clears unread mention counts.",
33884
+ inputSchema: {
33885
+ agent: exports_external.string().describe("Agent name"),
33886
+ space: exports_external.string().optional().describe("Clear only mentions in this space")
33887
+ }
33888
+ }, async (args) => {
33889
+ const cleared = markMentionsRead(args.agent, args.space);
33890
+ return { content: [{ type: "text", text: JSON.stringify({ cleared }) }] };
33770
33891
  });
33771
33892
  server.registerTool("send_to_space", {
33772
33893
  description: "Post a message to a space.",
package/bin/mcp.js CHANGED
@@ -6749,6 +6749,20 @@ function getDb() {
6749
6749
  if (!presenceColNames.includes("project_id")) {
6750
6750
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
6751
6751
  }
6752
+ db.exec(`
6753
+ CREATE TABLE IF NOT EXISTS message_mentions (
6754
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6755
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
6756
+ mentioned_agent TEXT NOT NULL,
6757
+ from_agent TEXT NOT NULL,
6758
+ space TEXT,
6759
+ notified_at TEXT,
6760
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
6761
+ )
6762
+ `);
6763
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
6764
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
6765
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
6752
6766
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
6753
6767
  if (!ftsExists) {
6754
6768
  db.exec(`
@@ -28839,6 +28853,12 @@ function sendMessage(opts) {
28839
28853
  db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
28840
28854
  message.attachments = attachmentInfos;
28841
28855
  }
28856
+ if (opts.space) {
28857
+ const mentions = parseMentions(opts.content);
28858
+ if (mentions.length > 0) {
28859
+ processMentions(message.id, opts.from, opts.space, mentions, db2);
28860
+ }
28861
+ }
28842
28862
  fireWebhooks(message);
28843
28863
  return message;
28844
28864
  }
@@ -28880,6 +28900,10 @@ function readMessages(opts = {}) {
28880
28900
  if (opts.threads_only) {
28881
28901
  conditions.push("reply_to IS NULL");
28882
28902
  }
28903
+ if (opts.mentions_only) {
28904
+ conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
28905
+ params.push(opts.mentions_only.toLowerCase());
28906
+ }
28883
28907
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28884
28908
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
28885
28909
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
@@ -29191,6 +29215,73 @@ function listUnreadCounts(agent) {
29191
29215
  `).all();
29192
29216
  return rows;
29193
29217
  }
29218
+ function parseMentions(content) {
29219
+ const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
29220
+ return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
29221
+ }
29222
+ async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
29223
+ const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
29224
+ for (const agent of mentionedAgents) {
29225
+ try {
29226
+ stmt.run(messageId, agent, fromAgent, space);
29227
+ if (agent !== fromAgent.toLowerCase()) {
29228
+ sendMessage({
29229
+ from: fromAgent,
29230
+ to: agent,
29231
+ content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
29232
+ metadata: { type: "mention_notification", source_message_id: messageId, space }
29233
+ });
29234
+ }
29235
+ } catch {}
29236
+ }
29237
+ }
29238
+ function listUnreadCountsWithMentions(agent) {
29239
+ const db2 = getDb();
29240
+ const rows = db2.prepare(`
29241
+ SELECT
29242
+ space,
29243
+ COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
29244
+ (SELECT COUNT(*) FROM message_mentions mm WHERE mm.space = m.space AND mm.mentioned_agent = ? AND mm.notified_at IS NULL) AS mention_count,
29245
+ MAX(created_at) AS latest_message_at
29246
+ FROM messages m
29247
+ WHERE space IN (
29248
+ SELECT DISTINCT space FROM space_members WHERE agent = ?
29249
+ UNION
29250
+ SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
29251
+ )
29252
+ GROUP BY space
29253
+ HAVING COUNT(*) > 0
29254
+ ORDER BY mention_count DESC, unread_count DESC, latest_message_at DESC
29255
+ `).all(agent, agent, agent, agent);
29256
+ return rows;
29257
+ }
29258
+ function getMessagesForAgent(agent, opts) {
29259
+ const db2 = getDb();
29260
+ const conditions = ["mm.mentioned_agent = ?"];
29261
+ const params = [agent.toLowerCase()];
29262
+ if (opts?.space) {
29263
+ conditions.push("m.space = ?");
29264
+ params.push(opts.space);
29265
+ }
29266
+ if (opts?.unread_only) {
29267
+ conditions.push("mm.notified_at IS NULL");
29268
+ }
29269
+ const limit = opts?.limit ?? 50;
29270
+ const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
29271
+ JOIN message_mentions mm ON mm.message_id = m.id
29272
+ WHERE ${conditions.join(" AND ")}
29273
+ ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
29274
+ return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
29275
+ }
29276
+ function markMentionsRead(agent, space) {
29277
+ const db2 = getDb();
29278
+ if (space) {
29279
+ const result2 = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND space = ? AND notified_at IS NULL").run(agent, space);
29280
+ return result2.changes;
29281
+ }
29282
+ const result = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND notified_at IS NULL").run(agent);
29283
+ return result.changes;
29284
+ }
29194
29285
 
29195
29286
  // src/lib/sessions.ts
29196
29287
  init_db();
@@ -30817,7 +30908,7 @@ function getGraphStats() {
30817
30908
  // package.json
30818
30909
  var package_default = {
30819
30910
  name: "@hasna/conversations",
30820
- version: "0.2.5",
30911
+ version: "0.2.6",
30821
30912
  description: "Real-time CLI messaging for AI agents",
30822
30913
  type: "module",
30823
30914
  bin: {
@@ -30951,7 +31042,8 @@ server.registerTool("read_messages", {
30951
31042
  mark_read: exports_external.coerce.boolean().optional(),
30952
31043
  max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
30953
31044
  threads_only: exports_external.coerce.boolean().optional().describe("Only return root messages (reply_to IS NULL) \u2014 hides thread replies"),
30954
- include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)")
31045
+ include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)"),
31046
+ mentions_only: exports_external.string().optional().describe("Only return messages that @mention this agent")
30955
31047
  }
30956
31048
  }, async (args) => {
30957
31049
  const agent = resolveIdentity(args.from);
@@ -31123,13 +31215,42 @@ server.registerTool("list_spaces", {
31123
31215
  server.registerTool("list_unread_counts", {
31124
31216
  description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
31125
31217
  inputSchema: {
31126
- agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts.")
31218
+ agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts."),
31219
+ include_mentions: exports_external.coerce.boolean().optional().describe("Include mention_count per space (requires agent)")
31127
31220
  }
31128
31221
  }, async (args) => {
31222
+ if (args.agent && args.include_mentions) {
31223
+ const counts2 = listUnreadCountsWithMentions(args.agent);
31224
+ return { content: [{ type: "text", text: JSON.stringify(counts2) }] };
31225
+ }
31129
31226
  const counts = listUnreadCounts(args.agent);
31130
- return {
31131
- content: [{ type: "text", text: JSON.stringify(counts) }]
31132
- };
31227
+ return { content: [{ type: "text", text: JSON.stringify(counts) }] };
31228
+ });
31229
+ server.registerTool("get_mentions", {
31230
+ description: "Get messages that @mention a specific agent. Useful for catching up on missed pings.",
31231
+ inputSchema: {
31232
+ agent: exports_external.string().describe("Agent name to find mentions for"),
31233
+ space: exports_external.string().optional().describe("Filter to a specific space"),
31234
+ unread_only: exports_external.coerce.boolean().optional().describe("Only unread (not yet notified) mentions (default: true)"),
31235
+ limit: exports_external.coerce.number().optional().describe("Max results (default: 50)")
31236
+ }
31237
+ }, async (args) => {
31238
+ const results = getMessagesForAgent(args.agent, {
31239
+ space: args.space,
31240
+ unread_only: args.unread_only ?? true,
31241
+ limit: args.limit
31242
+ });
31243
+ return { content: [{ type: "text", text: JSON.stringify({ mentions: results, count: results.length }) }] };
31244
+ });
31245
+ server.registerTool("mark_mentions_read", {
31246
+ description: "Mark @mentions as seen for an agent. Clears unread mention counts.",
31247
+ inputSchema: {
31248
+ agent: exports_external.string().describe("Agent name"),
31249
+ space: exports_external.string().optional().describe("Clear only mentions in this space")
31250
+ }
31251
+ }, async (args) => {
31252
+ const cleared = markMentionsRead(args.agent, args.space);
31253
+ return { content: [{ type: "text", text: JSON.stringify({ cleared }) }] };
31133
31254
  });
31134
31255
  server.registerTool("send_to_space", {
31135
31256
  description: "Post a message to a space.",
package/dist/index.js CHANGED
@@ -276,6 +276,20 @@ function getDb() {
276
276
  if (!presenceColNames.includes("project_id")) {
277
277
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
278
278
  }
279
+ db.exec(`
280
+ CREATE TABLE IF NOT EXISTS message_mentions (
281
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
282
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
283
+ mentioned_agent TEXT NOT NULL,
284
+ from_agent TEXT NOT NULL,
285
+ space TEXT,
286
+ notified_at TEXT,
287
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
288
+ )
289
+ `);
290
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
291
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
292
+ db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
279
293
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
280
294
  if (!ftsExists) {
281
295
  db.exec(`
@@ -2304,6 +2318,12 @@ function sendMessage(opts) {
2304
2318
  db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
2305
2319
  message.attachments = attachmentInfos;
2306
2320
  }
2321
+ if (opts.space) {
2322
+ const mentions = parseMentions(opts.content);
2323
+ if (mentions.length > 0) {
2324
+ processMentions(message.id, opts.from, opts.space, mentions, db2);
2325
+ }
2326
+ }
2307
2327
  fireWebhooks(message);
2308
2328
  return message;
2309
2329
  }
@@ -2345,6 +2365,10 @@ function readMessages(opts = {}) {
2345
2365
  if (opts.threads_only) {
2346
2366
  conditions.push("reply_to IS NULL");
2347
2367
  }
2368
+ if (opts.mentions_only) {
2369
+ conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
2370
+ params.push(opts.mentions_only.toLowerCase());
2371
+ }
2348
2372
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2349
2373
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
2350
2374
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
@@ -2583,6 +2607,26 @@ function searchMessages(opts) {
2583
2607
  return { ...msg, snippet: null, relevance_score: 0 };
2584
2608
  });
2585
2609
  }
2610
+ function parseMentions(content) {
2611
+ const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
2612
+ return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
2613
+ }
2614
+ async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
2615
+ const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
2616
+ for (const agent of mentionedAgents) {
2617
+ try {
2618
+ stmt.run(messageId, agent, fromAgent, space);
2619
+ if (agent !== fromAgent.toLowerCase()) {
2620
+ sendMessage({
2621
+ from: fromAgent,
2622
+ to: agent,
2623
+ content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
2624
+ metadata: { type: "mention_notification", source_message_id: messageId, space }
2625
+ });
2626
+ }
2627
+ } catch {}
2628
+ }
2629
+ }
2586
2630
  // src/lib/sessions.ts
2587
2631
  init_db();
2588
2632
  function listSessions(agent) {
@@ -67,3 +67,24 @@ export interface UnreadCount {
67
67
  * If agent is omitted, returns counts for all spaces.
68
68
  */
69
69
  export declare function listUnreadCounts(agent?: string): UnreadCount[];
70
+ /** Extract @agentname mentions from message content. Returns unique agent names (lowercase). */
71
+ export declare function parseMentions(content: string): string[];
72
+ export interface MentionCount {
73
+ space: string;
74
+ unread_count: number;
75
+ mention_count: number;
76
+ latest_message_at: string | null;
77
+ }
78
+ /** Get unread counts AND mention counts per space for an agent. */
79
+ export declare function listUnreadCountsWithMentions(agent: string): MentionCount[];
80
+ /** Get messages that mention a specific agent. */
81
+ export declare function getMessagesForAgent(agent: string, opts?: {
82
+ space?: string;
83
+ unread_only?: boolean;
84
+ limit?: number;
85
+ }): Array<{
86
+ message: Message;
87
+ mention_id: number;
88
+ }>;
89
+ /** Mark mentions as notified (agent has seen them). */
90
+ export declare function markMentionsRead(agent: string, space?: string): number;
package/dist/types.d.ts CHANGED
@@ -111,6 +111,7 @@ export interface ReadMessagesOptions {
111
111
  max_content_length?: number;
112
112
  threads_only?: boolean;
113
113
  include_reply_counts?: boolean;
114
+ mentions_only?: string;
114
115
  }
115
116
  export interface SearchMessagesOptions {
116
117
  query: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {