@hasna/conversations 0.2.14 → 0.2.16

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
@@ -255,6 +255,16 @@ function getDb() {
255
255
  if (!presenceColNames.includes("project_id")) {
256
256
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
257
257
  }
258
+ db.exec(`
259
+ CREATE TABLE IF NOT EXISTS message_read_receipts (
260
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
261
+ agent TEXT NOT NULL,
262
+ read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
263
+ PRIMARY KEY (message_id, agent)
264
+ )
265
+ `);
266
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
267
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
258
268
  db.exec(`
259
269
  CREATE TABLE IF NOT EXISTS message_mentions (
260
270
  id INTEGER PRIMARY KEY AUTOINCREMENT,
package/bin/index.js CHANGED
@@ -2120,6 +2120,16 @@ function getDb() {
2120
2120
  if (!presenceColNames.includes("project_id")) {
2121
2121
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
2122
2122
  }
2123
+ db.exec(`
2124
+ CREATE TABLE IF NOT EXISTS message_read_receipts (
2125
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
2126
+ agent TEXT NOT NULL,
2127
+ read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
2128
+ PRIMARY KEY (message_id, agent)
2129
+ )
2130
+ `);
2131
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
2132
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
2123
2133
  db.exec(`
2124
2134
  CREATE TABLE IF NOT EXISTS message_mentions (
2125
2135
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -2814,6 +2824,33 @@ function markUnreadByIds(ids) {
2814
2824
  const result = db2.prepare(`UPDATE messages SET read_at = NULL WHERE id IN (${placeholders})`).run(...ids);
2815
2825
  return result.changes;
2816
2826
  }
2827
+ function recordReadReceipt(messageId, agent) {
2828
+ const db2 = getDb();
2829
+ db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
2830
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`).run(messageId, agent.toLowerCase());
2831
+ }
2832
+ function recordReadReceiptsBatch(messageIds, agent) {
2833
+ if (!messageIds.length || !agent)
2834
+ return;
2835
+ const db2 = getDb();
2836
+ const stmt = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
2837
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
2838
+ for (const id of messageIds) {
2839
+ stmt.run(id, agent.toLowerCase());
2840
+ }
2841
+ }
2842
+ function getReadReceipts(messageId) {
2843
+ const db2 = getDb();
2844
+ return db2.prepare("SELECT * FROM message_read_receipts WHERE message_id = ? ORDER BY read_at ASC").all(messageId);
2845
+ }
2846
+ function getMessageReadStatus(messageId, space) {
2847
+ const db2 = getDb();
2848
+ const receipts = getReadReceipts(messageId);
2849
+ const readers = new Set(receipts.map((r) => r.agent));
2850
+ const members = db2.prepare("SELECT agent FROM space_members WHERE space = ?").all(space);
2851
+ const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
2852
+ return { receipts, unread_by };
2853
+ }
2817
2854
  var init_messages = __esm(() => {
2818
2855
  init_db();
2819
2856
  init_webhooks();
@@ -4453,7 +4490,7 @@ var init_poll = __esm(() => {
4453
4490
  var require_package = __commonJS((exports, module) => {
4454
4491
  module.exports = {
4455
4492
  name: "@hasna/conversations",
4456
- version: "0.2.14",
4493
+ version: "0.2.16",
4457
4494
  description: "Real-time CLI messaging for AI agents",
4458
4495
  type: "module",
4459
4496
  bin: {
@@ -33914,6 +33951,55 @@ var init_mcp2 = __esm(() => {
33914
33951
  content: [{ type: "text", text: JSON.stringify(spaces) }]
33915
33952
  };
33916
33953
  });
33954
+ server.registerTool("read_receipts", {
33955
+ description: "Get per-agent read receipts for a message. Shows who has read it and (for space messages) who hasn't.",
33956
+ inputSchema: {
33957
+ message_id: exports_external.coerce.number(),
33958
+ space: exports_external.string().optional().describe("Space name \u2014 if provided, also returns list of members who haven't read yet")
33959
+ }
33960
+ }, async (args) => {
33961
+ const receipts = getReadReceipts(args.message_id);
33962
+ if (args.space) {
33963
+ const status = getMessageReadStatus(args.message_id, args.space);
33964
+ return { content: [{ type: "text", text: JSON.stringify(status) }] };
33965
+ }
33966
+ return { content: [{ type: "text", text: JSON.stringify({ receipts, count: receipts.length }) }] };
33967
+ });
33968
+ server.registerTool("mark_read_receipt", {
33969
+ description: "Manually record that an agent has read a specific message.",
33970
+ inputSchema: {
33971
+ message_id: exports_external.coerce.number(),
33972
+ agent: exports_external.string()
33973
+ }
33974
+ }, async (args) => {
33975
+ recordReadReceipt(args.message_id, args.agent);
33976
+ return { content: [{ type: "text", text: `\u2713 Marked message #${args.message_id} as read by ${args.agent}` }] };
33977
+ });
33978
+ server.registerTool("broadcast", {
33979
+ description: "Send the same message to multiple spaces at once. Useful for status updates, bug reports, or announcements that need to go to several spaces.",
33980
+ inputSchema: {
33981
+ spaces: exports_external.array(exports_external.string()).describe("List of space names to send to"),
33982
+ content: exports_external.string().describe("Message content"),
33983
+ from: exports_external.string().optional().describe("Sender agent name"),
33984
+ priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional()
33985
+ }
33986
+ }, async (args) => {
33987
+ const { spaces, content, from: fromParam, priority } = args;
33988
+ const from = resolveIdentity(fromParam);
33989
+ const results = [];
33990
+ const errors4 = [];
33991
+ for (const space of spaces) {
33992
+ try {
33993
+ const msg = sendMessage({ from, to: space, content, space, priority });
33994
+ results.push({ space, id: msg.id });
33995
+ } catch (e) {
33996
+ errors4.push(`${space}: ${e instanceof Error ? e.message : String(e)}`);
33997
+ }
33998
+ }
33999
+ return {
34000
+ content: [{ type: "text", text: JSON.stringify({ sent: results, errors: errors4, total: results.length }) }]
34001
+ };
34002
+ });
33917
34003
  server.registerTool("list_unread_counts", {
33918
34004
  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.",
33919
34005
  inputSchema: {
@@ -33990,6 +34076,7 @@ var init_mcp2 = __esm(() => {
33990
34076
  description: "Read messages from a space.",
33991
34077
  inputSchema: {
33992
34078
  space: exports_external.string(),
34079
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
33993
34080
  since: exports_external.string().optional(),
33994
34081
  limit: exports_external.coerce.number().optional(),
33995
34082
  mark_read: exports_external.coerce.boolean().optional(),
@@ -33999,11 +34086,15 @@ var init_mcp2 = __esm(() => {
33999
34086
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
34000
34087
  }
34001
34088
  }, async (args) => {
34002
- const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
34089
+ const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
34003
34090
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
34004
34091
  if (mark_read !== false && messages.length > 0) {
34005
34092
  markReadByIds(messages.map((m) => m.id));
34006
34093
  }
34094
+ if (fromParam && messages.length > 0) {
34095
+ const agent = resolveIdentity(fromParam);
34096
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
34097
+ }
34007
34098
  return {
34008
34099
  content: [{ type: "text", text: JSON.stringify(messages) }]
34009
34100
  };
package/bin/mcp.js CHANGED
@@ -6752,6 +6752,16 @@ function getDb() {
6752
6752
  if (!presenceColNames.includes("project_id")) {
6753
6753
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
6754
6754
  }
6755
+ db.exec(`
6756
+ CREATE TABLE IF NOT EXISTS message_read_receipts (
6757
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
6758
+ agent TEXT NOT NULL,
6759
+ read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6760
+ PRIMARY KEY (message_id, agent)
6761
+ )
6762
+ `);
6763
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
6764
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
6755
6765
  db.exec(`
6756
6766
  CREATE TABLE IF NOT EXISTS message_mentions (
6757
6767
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -29317,6 +29327,33 @@ function markUnreadByIds(ids) {
29317
29327
  const result = db2.prepare(`UPDATE messages SET read_at = NULL WHERE id IN (${placeholders})`).run(...ids);
29318
29328
  return result.changes;
29319
29329
  }
29330
+ function recordReadReceipt(messageId, agent) {
29331
+ const db2 = getDb();
29332
+ db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
29333
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`).run(messageId, agent.toLowerCase());
29334
+ }
29335
+ function recordReadReceiptsBatch(messageIds, agent) {
29336
+ if (!messageIds.length || !agent)
29337
+ return;
29338
+ const db2 = getDb();
29339
+ const stmt = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
29340
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
29341
+ for (const id of messageIds) {
29342
+ stmt.run(id, agent.toLowerCase());
29343
+ }
29344
+ }
29345
+ function getReadReceipts(messageId) {
29346
+ const db2 = getDb();
29347
+ return db2.prepare("SELECT * FROM message_read_receipts WHERE message_id = ? ORDER BY read_at ASC").all(messageId);
29348
+ }
29349
+ function getMessageReadStatus(messageId, space) {
29350
+ const db2 = getDb();
29351
+ const receipts = getReadReceipts(messageId);
29352
+ const readers = new Set(receipts.map((r) => r.agent));
29353
+ const members = db2.prepare("SELECT agent FROM space_members WHERE space = ?").all(space);
29354
+ const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
29355
+ return { receipts, unread_by };
29356
+ }
29320
29357
 
29321
29358
  // src/lib/sessions.ts
29322
29359
  init_db();
@@ -30943,7 +30980,7 @@ function getGraphStats() {
30943
30980
  // package.json
30944
30981
  var package_default = {
30945
30982
  name: "@hasna/conversations",
30946
- version: "0.2.14",
30983
+ version: "0.2.16",
30947
30984
  description: "Real-time CLI messaging for AI agents",
30948
30985
  type: "module",
30949
30986
  bin: {
@@ -31283,6 +31320,55 @@ server.registerTool("list_spaces", {
31283
31320
  content: [{ type: "text", text: JSON.stringify(spaces) }]
31284
31321
  };
31285
31322
  });
31323
+ server.registerTool("read_receipts", {
31324
+ description: "Get per-agent read receipts for a message. Shows who has read it and (for space messages) who hasn't.",
31325
+ inputSchema: {
31326
+ message_id: exports_external.coerce.number(),
31327
+ space: exports_external.string().optional().describe("Space name \u2014 if provided, also returns list of members who haven't read yet")
31328
+ }
31329
+ }, async (args) => {
31330
+ const receipts = getReadReceipts(args.message_id);
31331
+ if (args.space) {
31332
+ const status = getMessageReadStatus(args.message_id, args.space);
31333
+ return { content: [{ type: "text", text: JSON.stringify(status) }] };
31334
+ }
31335
+ return { content: [{ type: "text", text: JSON.stringify({ receipts, count: receipts.length }) }] };
31336
+ });
31337
+ server.registerTool("mark_read_receipt", {
31338
+ description: "Manually record that an agent has read a specific message.",
31339
+ inputSchema: {
31340
+ message_id: exports_external.coerce.number(),
31341
+ agent: exports_external.string()
31342
+ }
31343
+ }, async (args) => {
31344
+ recordReadReceipt(args.message_id, args.agent);
31345
+ return { content: [{ type: "text", text: `\u2713 Marked message #${args.message_id} as read by ${args.agent}` }] };
31346
+ });
31347
+ server.registerTool("broadcast", {
31348
+ description: "Send the same message to multiple spaces at once. Useful for status updates, bug reports, or announcements that need to go to several spaces.",
31349
+ inputSchema: {
31350
+ spaces: exports_external.array(exports_external.string()).describe("List of space names to send to"),
31351
+ content: exports_external.string().describe("Message content"),
31352
+ from: exports_external.string().optional().describe("Sender agent name"),
31353
+ priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional()
31354
+ }
31355
+ }, async (args) => {
31356
+ const { spaces, content, from: fromParam, priority } = args;
31357
+ const from = resolveIdentity(fromParam);
31358
+ const results = [];
31359
+ const errors3 = [];
31360
+ for (const space of spaces) {
31361
+ try {
31362
+ const msg = sendMessage({ from, to: space, content, space, priority });
31363
+ results.push({ space, id: msg.id });
31364
+ } catch (e) {
31365
+ errors3.push(`${space}: ${e instanceof Error ? e.message : String(e)}`);
31366
+ }
31367
+ }
31368
+ return {
31369
+ content: [{ type: "text", text: JSON.stringify({ sent: results, errors: errors3, total: results.length }) }]
31370
+ };
31371
+ });
31286
31372
  server.registerTool("list_unread_counts", {
31287
31373
  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.",
31288
31374
  inputSchema: {
@@ -31359,6 +31445,7 @@ server.registerTool("read_space", {
31359
31445
  description: "Read messages from a space.",
31360
31446
  inputSchema: {
31361
31447
  space: exports_external.string(),
31448
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
31362
31449
  since: exports_external.string().optional(),
31363
31450
  limit: exports_external.coerce.number().optional(),
31364
31451
  mark_read: exports_external.coerce.boolean().optional(),
@@ -31368,11 +31455,15 @@ server.registerTool("read_space", {
31368
31455
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
31369
31456
  }
31370
31457
  }, async (args) => {
31371
- const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
31458
+ const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
31372
31459
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
31373
31460
  if (mark_read !== false && messages.length > 0) {
31374
31461
  markReadByIds(messages.map((m) => m.id));
31375
31462
  }
31463
+ if (fromParam && messages.length > 0) {
31464
+ const agent = resolveIdentity(fromParam);
31465
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
31466
+ }
31376
31467
  return {
31377
31468
  content: [{ type: "text", text: JSON.stringify(messages) }]
31378
31469
  };
package/dist/index.js CHANGED
@@ -279,6 +279,16 @@ function getDb() {
279
279
  if (!presenceColNames.includes("project_id")) {
280
280
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
281
281
  }
282
+ db.exec(`
283
+ CREATE TABLE IF NOT EXISTS message_read_receipts (
284
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
285
+ agent TEXT NOT NULL,
286
+ read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
287
+ PRIMARY KEY (message_id, agent)
288
+ )
289
+ `);
290
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
291
+ db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
282
292
  db.exec(`
283
293
  CREATE TABLE IF NOT EXISTS message_mentions (
284
294
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -92,3 +92,19 @@ export declare function markMentionsRead(agent: string, space?: string): number;
92
92
  export declare function markUnread(messageId: number): number;
93
93
  /** Mark multiple messages as unread. */
94
94
  export declare function markUnreadByIds(ids: number[]): number;
95
+ export interface ReadReceipt {
96
+ message_id: number;
97
+ agent: string;
98
+ read_at: string;
99
+ }
100
+ /** Record that an agent has read a specific message. */
101
+ export declare function recordReadReceipt(messageId: number, agent: string): void;
102
+ /** Record read receipts for all messages in a batch. */
103
+ export declare function recordReadReceiptsBatch(messageIds: number[], agent: string): void;
104
+ /** Get all read receipts for a specific message. */
105
+ export declare function getReadReceipts(messageId: number): ReadReceipt[];
106
+ /** Get read status summary for a space message: who has read it and who hasn't. */
107
+ export declare function getMessageReadStatus(messageId: number, space: string): {
108
+ receipts: ReadReceipt[];
109
+ unread_by: string[];
110
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {