@hasna/conversations 0.2.4 → 0.2.5

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/index.js CHANGED
@@ -2368,11 +2368,20 @@ function readMessages(opts = {}) {
2368
2368
  if (opts.unread_only) {
2369
2369
  conditions.push("read_at IS NULL");
2370
2370
  }
2371
+ if (opts.threads_only) {
2372
+ conditions.push("reply_to IS NULL");
2373
+ }
2371
2374
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2372
2375
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
2373
2376
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
2374
2377
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit}`).all(...params);
2375
2378
  let messages = rows.map(parseMessage);
2379
+ if (opts.include_reply_counts && messages.length > 0) {
2380
+ const db22 = getDb();
2381
+ const counts = db22.prepare(`SELECT reply_to, COUNT(*) as c FROM messages WHERE reply_to IN (${messages.map(() => "?").join(",")}) GROUP BY reply_to`).all(...messages.map((m) => m.id));
2382
+ const countMap = new Map(counts.map((r) => [r.reply_to, r.c]));
2383
+ messages = messages.map((m) => ({ ...m, reply_count: countMap.get(m.id) ?? 0 }));
2384
+ }
2376
2385
  if (opts.max_content_length && opts.max_content_length > 0) {
2377
2386
  messages = messages.map((m) => {
2378
2387
  if (m.content.length > opts.max_content_length) {
@@ -4324,7 +4333,7 @@ var init_poll = __esm(() => {
4324
4333
  var require_package = __commonJS((exports, module) => {
4325
4334
  module.exports = {
4326
4335
  name: "@hasna/conversations",
4327
- version: "0.2.4",
4336
+ version: "0.2.5",
4328
4337
  description: "Real-time CLI messaging for AI agents",
4329
4338
  type: "module",
4330
4339
  bin: {
@@ -33577,7 +33586,9 @@ var init_mcp2 = __esm(() => {
33577
33586
  limit: exports_external.coerce.number().optional(),
33578
33587
  unread_only: exports_external.coerce.boolean().optional(),
33579
33588
  mark_read: exports_external.coerce.boolean().optional(),
33580
- max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)")
33589
+ max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
33590
+ 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)")
33581
33592
  }
33582
33593
  }, async (args) => {
33583
33594
  const agent = resolveIdentity(args.from);
@@ -33605,7 +33616,7 @@ var init_mcp2 = __esm(() => {
33605
33616
  };
33606
33617
  });
33607
33618
  server.registerTool("reply", {
33608
- description: "Reply to a message by ID.",
33619
+ description: "Reply to a specific message, creating a thread. Sets reply_to so it can be retrieved with get_thread_replies.",
33609
33620
  inputSchema: {
33610
33621
  message_id: exports_external.coerce.number(),
33611
33622
  content: exports_external.string(),
@@ -33622,13 +33633,13 @@ var init_mcp2 = __esm(() => {
33622
33633
  }
33623
33634
  const from = resolveIdentity(fromParam);
33624
33635
  const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
33625
- const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
33626
33636
  const msg = sendMessage({
33627
33637
  from,
33628
- to,
33638
+ to: space ?? (original.from_agent === from ? original.to_agent : original.from_agent),
33629
33639
  content,
33630
33640
  session_id: original.session_id,
33631
- space
33641
+ space,
33642
+ reply_to: message_id
33632
33643
  });
33633
33644
  return {
33634
33645
  content: [{ type: "text", text: JSON.stringify(msg) }]
@@ -33796,11 +33807,13 @@ var init_mcp2 = __esm(() => {
33796
33807
  since: exports_external.string().optional(),
33797
33808
  limit: exports_external.coerce.number().optional(),
33798
33809
  mark_read: exports_external.coerce.boolean().optional(),
33799
- max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)")
33810
+ max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
33811
+ threads_only: exports_external.coerce.boolean().optional().describe("Only return root messages (hides thread replies)"),
33812
+ include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message")
33800
33813
  }
33801
33814
  }, async (args) => {
33802
- const { space, since, limit, mark_read, max_content_length } = args;
33803
- const messages = readMessages({ space, since, limit, max_content_length });
33815
+ const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts } = args;
33816
+ const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts });
33804
33817
  if (mark_read !== false && messages.length > 0) {
33805
33818
  markReadByIds(messages.map((m) => m.id));
33806
33819
  }
@@ -34451,13 +34464,30 @@ var init_mcp2 = __esm(() => {
34451
34464
  return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
34452
34465
  });
34453
34466
  server.registerTool("get_thread_replies", {
34454
- description: "Get all replies in a thread for a given parent message ID.",
34467
+ description: "Get all replies in a thread for a given parent message ID. Also accessible as read_thread.",
34455
34468
  inputSchema: {
34456
- message_id: exports_external.coerce.number()
34469
+ message_id: exports_external.coerce.number(),
34470
+ limit: exports_external.coerce.number().optional()
34471
+ }
34472
+ }, async (args) => {
34473
+ let replies = getThreadReplies(args.message_id);
34474
+ if (args.limit)
34475
+ replies = replies.slice(0, args.limit);
34476
+ const parent = getMessageById(args.message_id);
34477
+ return { content: [{ type: "text", text: JSON.stringify({ parent, replies, reply_count: replies.length }) }] };
34478
+ });
34479
+ server.registerTool("read_thread", {
34480
+ description: "Alias for get_thread_replies. Read all replies to a specific message, forming a thread view.",
34481
+ inputSchema: {
34482
+ message_id: exports_external.coerce.number(),
34483
+ limit: exports_external.coerce.number().optional()
34457
34484
  }
34458
34485
  }, async (args) => {
34459
- const replies = getThreadReplies(args.message_id);
34460
- return { content: [{ type: "text", text: JSON.stringify(replies) }] };
34486
+ let replies = getThreadReplies(args.message_id);
34487
+ if (args.limit)
34488
+ replies = replies.slice(0, args.limit);
34489
+ const parent = getMessageById(args.message_id);
34490
+ return { content: [{ type: "text", text: JSON.stringify({ parent, replies, reply_count: replies.length }) }] };
34461
34491
  });
34462
34492
  server.registerTool("set_focus", {
34463
34493
  description: "Set agent focus to a project. All read-heavy tools will default to this project scope. Stores in MCP session memory AND updates agent_presence.project_id in DB.",
@@ -34705,7 +34735,7 @@ var init_mcp2 = __esm(() => {
34705
34735
  read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?, mark_read?(default true \u2014 auto-marks returned messages as read, pass false to peek without consuming)",
34706
34736
  read_digest: "Lightweight unread digest \u2014 preview only (no full bodies), auto-marks read, never overflows tokens. Returns { messages, total_unread, shown }. Optional: space?, session_id?, to?, since?(ISO), limit?, project_id?",
34707
34737
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
34708
- reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
34738
+ reply: "Reply to a specific message, creating a thread (sets reply_to). Use read_thread to retrieve. Required: message_id, content. Optional: from?",
34709
34739
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
34710
34740
  search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
34711
34741
  export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
@@ -34749,6 +34779,7 @@ var init_mcp2 = __esm(() => {
34749
34779
  list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
34750
34780
  clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
34751
34781
  get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
34782
+ read_thread: "Alias for get_thread_replies. Required: message_id. Optional: limit?",
34752
34783
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
34753
34784
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
34754
34785
  unfocus: "Clear agent focus (session + DB). Optional: from?",
package/bin/mcp.js CHANGED
@@ -28877,11 +28877,20 @@ function readMessages(opts = {}) {
28877
28877
  if (opts.unread_only) {
28878
28878
  conditions.push("read_at IS NULL");
28879
28879
  }
28880
+ if (opts.threads_only) {
28881
+ conditions.push("reply_to IS NULL");
28882
+ }
28880
28883
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28881
28884
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
28882
28885
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
28883
28886
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit}`).all(...params);
28884
28887
  let messages = rows.map(parseMessage);
28888
+ if (opts.include_reply_counts && messages.length > 0) {
28889
+ const db22 = getDb();
28890
+ const counts = db22.prepare(`SELECT reply_to, COUNT(*) as c FROM messages WHERE reply_to IN (${messages.map(() => "?").join(",")}) GROUP BY reply_to`).all(...messages.map((m) => m.id));
28891
+ const countMap = new Map(counts.map((r) => [r.reply_to, r.c]));
28892
+ messages = messages.map((m) => ({ ...m, reply_count: countMap.get(m.id) ?? 0 }));
28893
+ }
28885
28894
  if (opts.max_content_length && opts.max_content_length > 0) {
28886
28895
  messages = messages.map((m) => {
28887
28896
  if (m.content.length > opts.max_content_length) {
@@ -30808,7 +30817,7 @@ function getGraphStats() {
30808
30817
  // package.json
30809
30818
  var package_default = {
30810
30819
  name: "@hasna/conversations",
30811
- version: "0.2.4",
30820
+ version: "0.2.5",
30812
30821
  description: "Real-time CLI messaging for AI agents",
30813
30822
  type: "module",
30814
30823
  bin: {
@@ -30940,7 +30949,9 @@ server.registerTool("read_messages", {
30940
30949
  limit: exports_external.coerce.number().optional(),
30941
30950
  unread_only: exports_external.coerce.boolean().optional(),
30942
30951
  mark_read: exports_external.coerce.boolean().optional(),
30943
- max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)")
30952
+ max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
30953
+ 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)")
30944
30955
  }
30945
30956
  }, async (args) => {
30946
30957
  const agent = resolveIdentity(args.from);
@@ -30968,7 +30979,7 @@ server.registerTool("list_sessions", {
30968
30979
  };
30969
30980
  });
30970
30981
  server.registerTool("reply", {
30971
- description: "Reply to a message by ID.",
30982
+ description: "Reply to a specific message, creating a thread. Sets reply_to so it can be retrieved with get_thread_replies.",
30972
30983
  inputSchema: {
30973
30984
  message_id: exports_external.coerce.number(),
30974
30985
  content: exports_external.string(),
@@ -30985,13 +30996,13 @@ server.registerTool("reply", {
30985
30996
  }
30986
30997
  const from = resolveIdentity(fromParam);
30987
30998
  const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
30988
- const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
30989
30999
  const msg = sendMessage({
30990
31000
  from,
30991
- to,
31001
+ to: space ?? (original.from_agent === from ? original.to_agent : original.from_agent),
30992
31002
  content,
30993
31003
  session_id: original.session_id,
30994
- space
31004
+ space,
31005
+ reply_to: message_id
30995
31006
  });
30996
31007
  return {
30997
31008
  content: [{ type: "text", text: JSON.stringify(msg) }]
@@ -31159,11 +31170,13 @@ server.registerTool("read_space", {
31159
31170
  since: exports_external.string().optional(),
31160
31171
  limit: exports_external.coerce.number().optional(),
31161
31172
  mark_read: exports_external.coerce.boolean().optional(),
31162
- max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)")
31173
+ max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
31174
+ threads_only: exports_external.coerce.boolean().optional().describe("Only return root messages (hides thread replies)"),
31175
+ include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message")
31163
31176
  }
31164
31177
  }, async (args) => {
31165
- const { space, since, limit, mark_read, max_content_length } = args;
31166
- const messages = readMessages({ space, since, limit, max_content_length });
31178
+ const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts } = args;
31179
+ const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts });
31167
31180
  if (mark_read !== false && messages.length > 0) {
31168
31181
  markReadByIds(messages.map((m) => m.id));
31169
31182
  }
@@ -31814,13 +31827,30 @@ server.registerTool("clean_expired_locks", {
31814
31827
  return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
31815
31828
  });
31816
31829
  server.registerTool("get_thread_replies", {
31817
- description: "Get all replies in a thread for a given parent message ID.",
31830
+ description: "Get all replies in a thread for a given parent message ID. Also accessible as read_thread.",
31818
31831
  inputSchema: {
31819
- message_id: exports_external.coerce.number()
31832
+ message_id: exports_external.coerce.number(),
31833
+ limit: exports_external.coerce.number().optional()
31834
+ }
31835
+ }, async (args) => {
31836
+ let replies = getThreadReplies(args.message_id);
31837
+ if (args.limit)
31838
+ replies = replies.slice(0, args.limit);
31839
+ const parent = getMessageById(args.message_id);
31840
+ return { content: [{ type: "text", text: JSON.stringify({ parent, replies, reply_count: replies.length }) }] };
31841
+ });
31842
+ server.registerTool("read_thread", {
31843
+ description: "Alias for get_thread_replies. Read all replies to a specific message, forming a thread view.",
31844
+ inputSchema: {
31845
+ message_id: exports_external.coerce.number(),
31846
+ limit: exports_external.coerce.number().optional()
31820
31847
  }
31821
31848
  }, async (args) => {
31822
- const replies = getThreadReplies(args.message_id);
31823
- return { content: [{ type: "text", text: JSON.stringify(replies) }] };
31849
+ let replies = getThreadReplies(args.message_id);
31850
+ if (args.limit)
31851
+ replies = replies.slice(0, args.limit);
31852
+ const parent = getMessageById(args.message_id);
31853
+ return { content: [{ type: "text", text: JSON.stringify({ parent, replies, reply_count: replies.length }) }] };
31824
31854
  });
31825
31855
  server.registerTool("set_focus", {
31826
31856
  description: "Set agent focus to a project. All read-heavy tools will default to this project scope. Stores in MCP session memory AND updates agent_presence.project_id in DB.",
@@ -32068,7 +32098,7 @@ server.registerTool("describe_tools", {
32068
32098
  read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?, mark_read?(default true \u2014 auto-marks returned messages as read, pass false to peek without consuming)",
32069
32099
  read_digest: "Lightweight unread digest \u2014 preview only (no full bodies), auto-marks read, never overflows tokens. Returns { messages, total_unread, shown }. Optional: space?, session_id?, to?, since?(ISO), limit?, project_id?",
32070
32100
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
32071
- reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
32101
+ reply: "Reply to a specific message, creating a thread (sets reply_to). Use read_thread to retrieve. Required: message_id, content. Optional: from?",
32072
32102
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
32073
32103
  search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
32074
32104
  export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
@@ -32112,6 +32142,7 @@ server.registerTool("describe_tools", {
32112
32142
  list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
32113
32143
  clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
32114
32144
  get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
32145
+ read_thread: "Alias for get_thread_replies. Required: message_id. Optional: limit?",
32115
32146
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
32116
32147
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
32117
32148
  unfocus: "Clear agent focus (session + DB). Optional: from?",
package/dist/index.js CHANGED
@@ -2342,11 +2342,20 @@ function readMessages(opts = {}) {
2342
2342
  if (opts.unread_only) {
2343
2343
  conditions.push("read_at IS NULL");
2344
2344
  }
2345
+ if (opts.threads_only) {
2346
+ conditions.push("reply_to IS NULL");
2347
+ }
2345
2348
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2346
2349
  const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
2347
2350
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
2348
2351
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit}`).all(...params);
2349
2352
  let messages = rows.map(parseMessage);
2353
+ if (opts.include_reply_counts && messages.length > 0) {
2354
+ const db22 = getDb();
2355
+ const counts = db22.prepare(`SELECT reply_to, COUNT(*) as c FROM messages WHERE reply_to IN (${messages.map(() => "?").join(",")}) GROUP BY reply_to`).all(...messages.map((m) => m.id));
2356
+ const countMap = new Map(counts.map((r) => [r.reply_to, r.c]));
2357
+ messages = messages.map((m) => ({ ...m, reply_count: countMap.get(m.id) ?? 0 }));
2358
+ }
2350
2359
  if (opts.max_content_length && opts.max_content_length > 0) {
2351
2360
  messages = messages.map((m) => {
2352
2361
  if (m.content.length > opts.max_content_length) {
package/dist/types.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface Message {
19
19
  blocking: boolean;
20
20
  attachments: Attachment[] | null;
21
21
  reply_to: number | null;
22
+ reply_count?: number;
22
23
  truncated?: boolean;
23
24
  }
24
25
  export interface Reaction {
@@ -108,6 +109,8 @@ export interface ReadMessagesOptions {
108
109
  order?: "asc" | "desc";
109
110
  compact?: boolean;
110
111
  max_content_length?: number;
112
+ threads_only?: boolean;
113
+ include_reply_counts?: boolean;
111
114
  }
112
115
  export interface SearchMessagesOptions {
113
116
  query: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {