@hasna/conversations 0.2.15 → 0.2.17

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.17",
4457
4494
  description: "Real-time CLI messaging for AI agents",
4458
4495
  type: "module",
4459
4496
  bin: {
@@ -33914,6 +33951,46 @@ 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("react", {
33979
+ description: "Add an emoji reaction (alias for add_reaction). Quick acknowledgment without a full reply.",
33980
+ inputSchema: { message_id: exports_external.coerce.number(), emoji: exports_external.string(), from: exports_external.string().optional() }
33981
+ }, async (args) => {
33982
+ const agent = resolveIdentity(args.from);
33983
+ const reaction = addReaction(args.message_id, agent, args.emoji);
33984
+ return { content: [{ type: "text", text: JSON.stringify(reaction) }] };
33985
+ });
33986
+ server.registerTool("unreact", {
33987
+ description: "Remove an emoji reaction (alias for remove_reaction).",
33988
+ inputSchema: { message_id: exports_external.coerce.number(), emoji: exports_external.string(), from: exports_external.string().optional() }
33989
+ }, async (args) => {
33990
+ const agent = resolveIdentity(args.from);
33991
+ const removed = removeReaction(args.message_id, agent, args.emoji);
33992
+ return { content: [{ type: "text", text: JSON.stringify({ removed }) }] };
33993
+ });
33917
33994
  server.registerTool("broadcast", {
33918
33995
  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
33996
  inputSchema: {
@@ -34015,6 +34092,7 @@ var init_mcp2 = __esm(() => {
34015
34092
  description: "Read messages from a space.",
34016
34093
  inputSchema: {
34017
34094
  space: exports_external.string(),
34095
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
34018
34096
  since: exports_external.string().optional(),
34019
34097
  limit: exports_external.coerce.number().optional(),
34020
34098
  mark_read: exports_external.coerce.boolean().optional(),
@@ -34024,11 +34102,15 @@ var init_mcp2 = __esm(() => {
34024
34102
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
34025
34103
  }
34026
34104
  }, async (args) => {
34027
- const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
34105
+ const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
34028
34106
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
34029
34107
  if (mark_read !== false && messages.length > 0) {
34030
34108
  markReadByIds(messages.map((m) => m.id));
34031
34109
  }
34110
+ if (fromParam && messages.length > 0) {
34111
+ const agent = resolveIdentity(fromParam);
34112
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
34113
+ }
34032
34114
  return {
34033
34115
  content: [{ type: "text", text: JSON.stringify(messages) }]
34034
34116
  };
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.17",
30947
30984
  description: "Real-time CLI messaging for AI agents",
30948
30985
  type: "module",
30949
30986
  bin: {
@@ -31283,6 +31320,46 @@ 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("react", {
31348
+ description: "Add an emoji reaction (alias for add_reaction). Quick acknowledgment without a full reply.",
31349
+ inputSchema: { message_id: exports_external.coerce.number(), emoji: exports_external.string(), from: exports_external.string().optional() }
31350
+ }, async (args) => {
31351
+ const agent = resolveIdentity(args.from);
31352
+ const reaction = addReaction(args.message_id, agent, args.emoji);
31353
+ return { content: [{ type: "text", text: JSON.stringify(reaction) }] };
31354
+ });
31355
+ server.registerTool("unreact", {
31356
+ description: "Remove an emoji reaction (alias for remove_reaction).",
31357
+ inputSchema: { message_id: exports_external.coerce.number(), emoji: exports_external.string(), from: exports_external.string().optional() }
31358
+ }, async (args) => {
31359
+ const agent = resolveIdentity(args.from);
31360
+ const removed = removeReaction(args.message_id, agent, args.emoji);
31361
+ return { content: [{ type: "text", text: JSON.stringify({ removed }) }] };
31362
+ });
31286
31363
  server.registerTool("broadcast", {
31287
31364
  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
31365
  inputSchema: {
@@ -31384,6 +31461,7 @@ server.registerTool("read_space", {
31384
31461
  description: "Read messages from a space.",
31385
31462
  inputSchema: {
31386
31463
  space: exports_external.string(),
31464
+ from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
31387
31465
  since: exports_external.string().optional(),
31388
31466
  limit: exports_external.coerce.number().optional(),
31389
31467
  mark_read: exports_external.coerce.boolean().optional(),
@@ -31393,11 +31471,15 @@ server.registerTool("read_space", {
31393
31471
  latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
31394
31472
  }
31395
31473
  }, async (args) => {
31396
- const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
31474
+ const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
31397
31475
  const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
31398
31476
  if (mark_read !== false && messages.length > 0) {
31399
31477
  markReadByIds(messages.map((m) => m.id));
31400
31478
  }
31479
+ if (fromParam && messages.length > 0) {
31480
+ const agent = resolveIdentity(fromParam);
31481
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
31482
+ }
31401
31483
  return {
31402
31484
  content: [{ type: "text", text: JSON.stringify(messages) }]
31403
31485
  };
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.17",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {