@hasna/conversations 0.2.15 → 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.15",
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,30 @@ 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
+ });
33917
33978
  server.registerTool("broadcast", {
33918
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.",
33919
33980
  inputSchema: {
@@ -34015,6 +34076,7 @@ var init_mcp2 = __esm(() => {
34015
34076
  description: "Read messages from a space.",
34016
34077
  inputSchema: {
34017
34078
  space: exports_external.string(),
34079
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
34018
34080
  since: exports_external.string().optional(),
34019
34081
  limit: exports_external.coerce.number().optional(),
34020
34082
  mark_read: exports_external.coerce.boolean().optional(),
@@ -34024,11 +34086,15 @@ var init_mcp2 = __esm(() => {
34024
34086
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
34025
34087
  }
34026
34088
  }, async (args) => {
34027
- 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;
34028
34090
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
34029
34091
  if (mark_read !== false && messages.length > 0) {
34030
34092
  markReadByIds(messages.map((m) => m.id));
34031
34093
  }
34094
+ if (fromParam && messages.length > 0) {
34095
+ const agent = resolveIdentity(fromParam);
34096
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
34097
+ }
34032
34098
  return {
34033
34099
  content: [{ type: "text", text: JSON.stringify(messages) }]
34034
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.15",
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,30 @@ 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
+ });
31286
31347
  server.registerTool("broadcast", {
31287
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.",
31288
31349
  inputSchema: {
@@ -31384,6 +31445,7 @@ server.registerTool("read_space", {
31384
31445
  description: "Read messages from a space.",
31385
31446
  inputSchema: {
31386
31447
  space: exports_external.string(),
31448
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
31387
31449
  since: exports_external.string().optional(),
31388
31450
  limit: exports_external.coerce.number().optional(),
31389
31451
  mark_read: exports_external.coerce.boolean().optional(),
@@ -31393,11 +31455,15 @@ server.registerTool("read_space", {
31393
31455
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
31394
31456
  }
31395
31457
  }, async (args) => {
31396
- 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;
31397
31459
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
31398
31460
  if (mark_read !== false && messages.length > 0) {
31399
31461
  markReadByIds(messages.map((m) => m.id));
31400
31462
  }
31463
+ if (fromParam && messages.length > 0) {
31464
+ const agent = resolveIdentity(fromParam);
31465
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
31466
+ }
31401
31467
  return {
31402
31468
  content: [{ type: "text", text: JSON.stringify(messages) }]
31403
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.15",
3
+ "version": "0.2.16",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {