@hasna/conversations 0.2.3 → 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,28 @@ 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
- const messages = rows.map(parseMessage);
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
+ }
2385
+ if (opts.max_content_length && opts.max_content_length > 0) {
2386
+ messages = messages.map((m) => {
2387
+ if (m.content.length > opts.max_content_length) {
2388
+ return { ...m, content: m.content.slice(0, opts.max_content_length) + "\u2026", truncated: true };
2389
+ }
2390
+ return m;
2391
+ });
2392
+ }
2376
2393
  if (opts.compact)
2377
2394
  return messages.map(compactMessage);
2378
2395
  return messages;
@@ -4316,7 +4333,7 @@ var init_poll = __esm(() => {
4316
4333
  var require_package = __commonJS((exports, module) => {
4317
4334
  module.exports = {
4318
4335
  name: "@hasna/conversations",
4319
- version: "0.2.3",
4336
+ version: "0.2.5",
4320
4337
  description: "Real-time CLI messaging for AI agents",
4321
4338
  type: "module",
4322
4339
  bin: {
@@ -33568,7 +33585,10 @@ var init_mcp2 = __esm(() => {
33568
33585
  since: exports_external.string().optional(),
33569
33586
  limit: exports_external.coerce.number().optional(),
33570
33587
  unread_only: exports_external.coerce.boolean().optional(),
33571
- mark_read: exports_external.coerce.boolean().optional()
33588
+ mark_read: exports_external.coerce.boolean().optional(),
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)")
33572
33592
  }
33573
33593
  }, async (args) => {
33574
33594
  const agent = resolveIdentity(args.from);
@@ -33596,7 +33616,7 @@ var init_mcp2 = __esm(() => {
33596
33616
  };
33597
33617
  });
33598
33618
  server.registerTool("reply", {
33599
- 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.",
33600
33620
  inputSchema: {
33601
33621
  message_id: exports_external.coerce.number(),
33602
33622
  content: exports_external.string(),
@@ -33613,13 +33633,13 @@ var init_mcp2 = __esm(() => {
33613
33633
  }
33614
33634
  const from = resolveIdentity(fromParam);
33615
33635
  const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
33616
- const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
33617
33636
  const msg = sendMessage({
33618
33637
  from,
33619
- to,
33638
+ to: space ?? (original.from_agent === from ? original.to_agent : original.from_agent),
33620
33639
  content,
33621
33640
  session_id: original.session_id,
33622
- space
33641
+ space,
33642
+ reply_to: message_id
33623
33643
  });
33624
33644
  return {
33625
33645
  content: [{ type: "text", text: JSON.stringify(msg) }]
@@ -33786,11 +33806,14 @@ var init_mcp2 = __esm(() => {
33786
33806
  space: exports_external.string(),
33787
33807
  since: exports_external.string().optional(),
33788
33808
  limit: exports_external.coerce.number().optional(),
33789
- mark_read: exports_external.coerce.boolean().optional()
33809
+ mark_read: exports_external.coerce.boolean().optional(),
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")
33790
33813
  }
33791
33814
  }, async (args) => {
33792
- const { space, since, limit, mark_read } = args;
33793
- const messages = readMessages({ space, since, limit });
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 });
33794
33817
  if (mark_read !== false && messages.length > 0) {
33795
33818
  markReadByIds(messages.map((m) => m.id));
33796
33819
  }
@@ -34441,13 +34464,30 @@ var init_mcp2 = __esm(() => {
34441
34464
  return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
34442
34465
  });
34443
34466
  server.registerTool("get_thread_replies", {
34444
- 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.",
34445
34468
  inputSchema: {
34446
- 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()
34447
34484
  }
34448
34485
  }, async (args) => {
34449
- const replies = getThreadReplies(args.message_id);
34450
- 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 }) }] };
34451
34491
  });
34452
34492
  server.registerTool("set_focus", {
34453
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.",
@@ -34695,7 +34735,7 @@ var init_mcp2 = __esm(() => {
34695
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)",
34696
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?",
34697
34737
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
34698
- 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?",
34699
34739
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
34700
34740
  search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
34701
34741
  export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
@@ -34739,6 +34779,7 @@ var init_mcp2 = __esm(() => {
34739
34779
  list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
34740
34780
  clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
34741
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?",
34742
34783
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
34743
34784
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
34744
34785
  unfocus: "Clear agent focus (session + DB). Optional: from?",
package/bin/mcp.js CHANGED
@@ -28877,11 +28877,28 @@ 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
- const messages = rows.map(parseMessage);
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
+ }
28894
+ if (opts.max_content_length && opts.max_content_length > 0) {
28895
+ messages = messages.map((m) => {
28896
+ if (m.content.length > opts.max_content_length) {
28897
+ return { ...m, content: m.content.slice(0, opts.max_content_length) + "\u2026", truncated: true };
28898
+ }
28899
+ return m;
28900
+ });
28901
+ }
28885
28902
  if (opts.compact)
28886
28903
  return messages.map(compactMessage);
28887
28904
  return messages;
@@ -30800,7 +30817,7 @@ function getGraphStats() {
30800
30817
  // package.json
30801
30818
  var package_default = {
30802
30819
  name: "@hasna/conversations",
30803
- version: "0.2.3",
30820
+ version: "0.2.5",
30804
30821
  description: "Real-time CLI messaging for AI agents",
30805
30822
  type: "module",
30806
30823
  bin: {
@@ -30931,7 +30948,10 @@ server.registerTool("read_messages", {
30931
30948
  since: exports_external.string().optional(),
30932
30949
  limit: exports_external.coerce.number().optional(),
30933
30950
  unread_only: exports_external.coerce.boolean().optional(),
30934
- mark_read: exports_external.coerce.boolean().optional()
30951
+ mark_read: exports_external.coerce.boolean().optional(),
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)")
30935
30955
  }
30936
30956
  }, async (args) => {
30937
30957
  const agent = resolveIdentity(args.from);
@@ -30959,7 +30979,7 @@ server.registerTool("list_sessions", {
30959
30979
  };
30960
30980
  });
30961
30981
  server.registerTool("reply", {
30962
- 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.",
30963
30983
  inputSchema: {
30964
30984
  message_id: exports_external.coerce.number(),
30965
30985
  content: exports_external.string(),
@@ -30976,13 +30996,13 @@ server.registerTool("reply", {
30976
30996
  }
30977
30997
  const from = resolveIdentity(fromParam);
30978
30998
  const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
30979
- const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
30980
30999
  const msg = sendMessage({
30981
31000
  from,
30982
- to,
31001
+ to: space ?? (original.from_agent === from ? original.to_agent : original.from_agent),
30983
31002
  content,
30984
31003
  session_id: original.session_id,
30985
- space
31004
+ space,
31005
+ reply_to: message_id
30986
31006
  });
30987
31007
  return {
30988
31008
  content: [{ type: "text", text: JSON.stringify(msg) }]
@@ -31149,11 +31169,14 @@ server.registerTool("read_space", {
31149
31169
  space: exports_external.string(),
31150
31170
  since: exports_external.string().optional(),
31151
31171
  limit: exports_external.coerce.number().optional(),
31152
- mark_read: exports_external.coerce.boolean().optional()
31172
+ mark_read: exports_external.coerce.boolean().optional(),
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")
31153
31176
  }
31154
31177
  }, async (args) => {
31155
- const { space, since, limit, mark_read } = args;
31156
- const messages = readMessages({ space, since, limit });
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 });
31157
31180
  if (mark_read !== false && messages.length > 0) {
31158
31181
  markReadByIds(messages.map((m) => m.id));
31159
31182
  }
@@ -31804,13 +31827,30 @@ server.registerTool("clean_expired_locks", {
31804
31827
  return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
31805
31828
  });
31806
31829
  server.registerTool("get_thread_replies", {
31807
- 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.",
31808
31831
  inputSchema: {
31809
- 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()
31810
31847
  }
31811
31848
  }, async (args) => {
31812
- const replies = getThreadReplies(args.message_id);
31813
- 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 }) }] };
31814
31854
  });
31815
31855
  server.registerTool("set_focus", {
31816
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.",
@@ -32058,7 +32098,7 @@ server.registerTool("describe_tools", {
32058
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)",
32059
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?",
32060
32100
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
32061
- 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?",
32062
32102
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
32063
32103
  search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
32064
32104
  export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
@@ -32102,6 +32142,7 @@ server.registerTool("describe_tools", {
32102
32142
  list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
32103
32143
  clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
32104
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?",
32105
32146
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
32106
32147
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
32107
32148
  unfocus: "Clear agent focus (session + DB). Optional: from?",
package/dist/index.js CHANGED
@@ -2342,11 +2342,28 @@ 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
- const messages = rows.map(parseMessage);
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
+ }
2359
+ if (opts.max_content_length && opts.max_content_length > 0) {
2360
+ messages = messages.map((m) => {
2361
+ if (m.content.length > opts.max_content_length) {
2362
+ return { ...m, content: m.content.slice(0, opts.max_content_length) + "\u2026", truncated: true };
2363
+ }
2364
+ return m;
2365
+ });
2366
+ }
2350
2367
  if (opts.compact)
2351
2368
  return messages.map(compactMessage);
2352
2369
  return messages;
package/dist/types.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface Message {
19
19
  blocking: boolean;
20
20
  attachments: Attachment[] | null;
21
21
  reply_to: number | null;
22
+ reply_count?: number;
23
+ truncated?: boolean;
22
24
  }
23
25
  export interface Reaction {
24
26
  id: number;
@@ -106,6 +108,9 @@ export interface ReadMessagesOptions {
106
108
  unread_only?: boolean;
107
109
  order?: "asc" | "desc";
108
110
  compact?: boolean;
111
+ max_content_length?: number;
112
+ threads_only?: boolean;
113
+ include_reply_counts?: boolean;
109
114
  }
110
115
  export interface SearchMessagesOptions {
111
116
  query: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {