@hasna/conversations 0.1.18 → 0.1.20

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/mcp.js CHANGED
@@ -6677,6 +6677,43 @@ function getDb() {
6677
6677
  if (!colNames2.includes("attachments")) {
6678
6678
  db.exec("ALTER TABLE messages ADD COLUMN attachments TEXT");
6679
6679
  }
6680
+ if (!colNames2.includes("reply_to")) {
6681
+ db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
6682
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
6683
+ }
6684
+ const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
6685
+ if (!ftsExists) {
6686
+ db.exec(`
6687
+ CREATE VIRTUAL TABLE messages_fts USING fts5(
6688
+ content, from_agent, to_agent, space,
6689
+ content_rowid='id', content='messages'
6690
+ )
6691
+ `);
6692
+ db.exec(`
6693
+ INSERT INTO messages_fts(rowid, content, from_agent, to_agent, space)
6694
+ SELECT id, content, from_agent, to_agent, space FROM messages
6695
+ `);
6696
+ db.exec(`
6697
+ CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
6698
+ INSERT INTO messages_fts(rowid, content, from_agent, to_agent, space)
6699
+ VALUES (new.id, new.content, new.from_agent, new.to_agent, new.space);
6700
+ END
6701
+ `);
6702
+ db.exec(`
6703
+ CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
6704
+ INSERT INTO messages_fts(messages_fts, rowid, content, from_agent, to_agent, space)
6705
+ VALUES ('delete', old.id, old.content, old.from_agent, old.to_agent, old.space);
6706
+ END
6707
+ `);
6708
+ db.exec(`
6709
+ CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE OF content ON messages BEGIN
6710
+ INSERT INTO messages_fts(messages_fts, rowid, content, from_agent, to_agent, space)
6711
+ VALUES ('delete', old.id, old.content, old.from_agent, old.to_agent, old.space);
6712
+ INSERT INTO messages_fts(rowid, content, from_agent, to_agent, space)
6713
+ VALUES (new.id, new.content, new.from_agent, new.to_agent, new.space);
6714
+ END
6715
+ `);
6716
+ }
6680
6717
  return db;
6681
6718
  }
6682
6719
  function closeDb() {
@@ -28509,8 +28546,84 @@ class StdioServerTransport {
28509
28546
  init_db();
28510
28547
  import { randomUUID } from "crypto";
28511
28548
  import { mkdirSync as mkdirSync2, copyFileSync, statSync } from "fs";
28549
+ import { join as join3 } from "path";
28550
+ import { homedir as homedir3 } from "os";
28551
+
28552
+ // src/lib/webhooks.ts
28553
+ import { readFileSync } from "fs";
28512
28554
  import { join as join2 } from "path";
28513
28555
  import { homedir as homedir2 } from "os";
28556
+ var cachedConfig = null;
28557
+ var configLoadedAt = 0;
28558
+ var CONFIG_CACHE_MS = 1e4;
28559
+ function getConfigPath() {
28560
+ return process.env.CONVERSATIONS_CONFIG_PATH || join2(homedir2(), ".conversations", "config.json");
28561
+ }
28562
+ function loadConfig() {
28563
+ const now = Date.now();
28564
+ if (cachedConfig && now - configLoadedAt < CONFIG_CACHE_MS)
28565
+ return cachedConfig;
28566
+ try {
28567
+ const raw = readFileSync(getConfigPath(), "utf-8");
28568
+ cachedConfig = JSON.parse(raw);
28569
+ configLoadedAt = now;
28570
+ return cachedConfig;
28571
+ } catch {
28572
+ cachedConfig = {};
28573
+ configLoadedAt = now;
28574
+ return cachedConfig;
28575
+ }
28576
+ }
28577
+ function matchesEvent(webhook, msg) {
28578
+ for (const event of webhook.events) {
28579
+ if (event === "dm" && !msg.space)
28580
+ return true;
28581
+ if (event === "blocker" && msg.blocking)
28582
+ return true;
28583
+ if (event === "space" && msg.space)
28584
+ return true;
28585
+ if (event === "mention" && webhook.agent && msg.content.includes(`@${webhook.agent}`))
28586
+ return true;
28587
+ }
28588
+ return false;
28589
+ }
28590
+ function fireWebhooks(msg) {
28591
+ const config2 = loadConfig();
28592
+ if (!config2.webhooks || config2.webhooks.length === 0)
28593
+ return;
28594
+ for (const webhook of config2.webhooks) {
28595
+ if (webhook.agent && msg.to_agent !== webhook.agent && !msg.space)
28596
+ continue;
28597
+ if (!matchesEvent(webhook, msg))
28598
+ continue;
28599
+ fetch(webhook.url, {
28600
+ method: "POST",
28601
+ headers: { "Content-Type": "application/json" },
28602
+ body: JSON.stringify({
28603
+ id: msg.id,
28604
+ from: msg.from_agent,
28605
+ to: msg.to_agent,
28606
+ space: msg.space,
28607
+ content: msg.content,
28608
+ priority: msg.priority,
28609
+ blocking: msg.blocking,
28610
+ created_at: msg.created_at
28611
+ })
28612
+ }).catch(() => {});
28613
+ }
28614
+ }
28615
+
28616
+ // src/lib/messages.ts
28617
+ function compactMessage(msg) {
28618
+ const result = {};
28619
+ for (const key of Object.keys(msg)) {
28620
+ const val = msg[key];
28621
+ if (val !== null && val !== undefined) {
28622
+ result[key] = val;
28623
+ }
28624
+ }
28625
+ return result;
28626
+ }
28514
28627
  function parseMessage(row) {
28515
28628
  let metadata = null;
28516
28629
  if (row.metadata) {
@@ -28532,13 +28645,14 @@ function parseMessage(row) {
28532
28645
  ...row,
28533
28646
  metadata,
28534
28647
  attachments,
28535
- blocking: !!row.blocking
28648
+ blocking: !!row.blocking,
28649
+ reply_to: row.reply_to || null
28536
28650
  };
28537
28651
  }
28538
28652
  function getAttachmentsDir() {
28539
28653
  if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
28540
28654
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
28541
- return join2(homedir2(), ".conversations", "attachments");
28655
+ return join3(homedir3(), ".conversations", "attachments");
28542
28656
  }
28543
28657
  function guessMimeType(name) {
28544
28658
  const ext = name.split(".").pop()?.toLowerCase();
@@ -28574,19 +28688,20 @@ function sendMessage(opts) {
28574
28688
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
28575
28689
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
28576
28690
  const blocking = opts.blocking ? 1 : 0;
28691
+ const replyTo = opts.reply_to || null;
28577
28692
  const stmt = db2.prepare(`
28578
- INSERT INTO messages (session_id, from_agent, to_agent, space, content, priority, working_dir, repository, branch, metadata, blocking)
28579
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28693
+ INSERT INTO messages (session_id, from_agent, to_agent, space, content, priority, working_dir, repository, branch, metadata, blocking, reply_to)
28694
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28580
28695
  RETURNING *
28581
28696
  `);
28582
- const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking);
28697
+ const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking, replyTo);
28583
28698
  const message = parseMessage(row);
28584
28699
  if (opts.attachments && opts.attachments.length > 0) {
28585
- const attachmentsDir = join2(getAttachmentsDir(), String(message.id));
28700
+ const attachmentsDir = join3(getAttachmentsDir(), String(message.id));
28586
28701
  mkdirSync2(attachmentsDir, { recursive: true });
28587
28702
  const attachmentInfos = [];
28588
28703
  for (const att of opts.attachments) {
28589
- const destPath = join2(attachmentsDir, att.name);
28704
+ const destPath = join3(attachmentsDir, att.name);
28590
28705
  copyFileSync(att.source_path, destPath);
28591
28706
  const stat = statSync(destPath);
28592
28707
  attachmentInfos.push({
@@ -28600,6 +28715,7 @@ function sendMessage(opts) {
28600
28715
  db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
28601
28716
  message.attachments = attachmentInfos;
28602
28717
  }
28718
+ fireWebhooks(message);
28603
28719
  return message;
28604
28720
  }
28605
28721
  function readMessages(opts = {}) {
@@ -28634,10 +28750,13 @@ function readMessages(opts = {}) {
28634
28750
  conditions.push("read_at IS NULL");
28635
28751
  }
28636
28752
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28637
- const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
28753
+ const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
28638
28754
  const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
28639
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} ${limit}`).all(...params);
28640
- return rows.map(parseMessage);
28755
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit}`).all(...params);
28756
+ const messages = rows.map(parseMessage);
28757
+ if (opts.compact)
28758
+ return messages.map(compactMessage);
28759
+ return messages;
28641
28760
  }
28642
28761
  function markRead(ids, reader) {
28643
28762
  const db2 = getDb();
@@ -28771,6 +28890,33 @@ function getUnreadBlockers(agent) {
28771
28890
  }
28772
28891
  function searchMessages(opts) {
28773
28892
  const db2 = getDb();
28893
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
28894
+ try {
28895
+ const ftsConditions = [];
28896
+ const ftsParams = [];
28897
+ const words = opts.query.trim().split(/\s+/).filter(Boolean);
28898
+ const ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
28899
+ ftsConditions.push("messages_fts MATCH ?");
28900
+ ftsParams.push(ftsQuery);
28901
+ let extraWhere = "";
28902
+ if (opts.space) {
28903
+ extraWhere += " AND m.space = ?";
28904
+ ftsParams.push(opts.space);
28905
+ }
28906
+ if (opts.from) {
28907
+ extraWhere += " AND m.from_agent = ?";
28908
+ ftsParams.push(opts.from);
28909
+ }
28910
+ if (opts.to) {
28911
+ extraWhere += " AND m.to_agent = ?";
28912
+ ftsParams.push(opts.to);
28913
+ }
28914
+ const rows2 = db2.prepare(`SELECT m.* FROM messages m
28915
+ JOIN messages_fts ON messages_fts.rowid = m.id
28916
+ WHERE ${ftsConditions.join(" AND ")}${extraWhere}
28917
+ ORDER BY m.created_at DESC, m.id DESC LIMIT ${limit}`).all(...ftsParams);
28918
+ return rows2.map(parseMessage);
28919
+ } catch {}
28774
28920
  const conditions = ["content LIKE ?"];
28775
28921
  const params = [`%${opts.query}%`];
28776
28922
  if (opts.space) {
@@ -28785,7 +28931,6 @@ function searchMessages(opts) {
28785
28931
  conditions.push("to_agent = ?");
28786
28932
  params.push(opts.to);
28787
28933
  }
28788
- const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
28789
28934
  const where = `WHERE ${conditions.join(" AND ")}`;
28790
28935
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
28791
28936
  return rows.map(parseMessage);
@@ -29163,9 +29308,9 @@ function deleteProject(id) {
29163
29308
  }
29164
29309
 
29165
29310
  // src/lib/identity.ts
29166
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
29167
- import { join as join3, dirname as dirname2 } from "path";
29168
- import { homedir as homedir3 } from "os";
29311
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
29312
+ import { join as join4, dirname as dirname2 } from "path";
29313
+ import { homedir as homedir4 } from "os";
29169
29314
 
29170
29315
  // src/lib/names.ts
29171
29316
  var AGENT_NAMES = [
@@ -29517,7 +29662,7 @@ var AGENT_NAMES = [
29517
29662
  ];
29518
29663
 
29519
29664
  // src/lib/identity.ts
29520
- var AGENT_ID_FILE = join3(homedir3(), ".conversations", "agent-id");
29665
+ var AGENT_ID_FILE = join4(homedir4(), ".conversations", "agent-id");
29521
29666
  var cachedAutoName = null;
29522
29667
  function isNameTaken(name) {
29523
29668
  try {
@@ -29533,7 +29678,7 @@ function getAutoName() {
29533
29678
  if (cachedAutoName)
29534
29679
  return cachedAutoName;
29535
29680
  try {
29536
- const name2 = readFileSync(AGENT_ID_FILE, "utf-8").trim();
29681
+ const name2 = readFileSync2(AGENT_ID_FILE, "utf-8").trim();
29537
29682
  if (name2) {
29538
29683
  cachedAutoName = name2;
29539
29684
  return name2;
@@ -29632,7 +29777,7 @@ function renameAgent(oldName, newName) {
29632
29777
  // package.json
29633
29778
  var package_default = {
29634
29779
  name: "@hasna/conversations",
29635
- version: "0.1.18",
29780
+ version: "0.1.20",
29636
29781
  description: "Real-time CLI messaging for AI agents",
29637
29782
  type: "module",
29638
29783
  bin: {
@@ -29716,9 +29861,9 @@ var server = new McpServer({
29716
29861
  });
29717
29862
  server.registerTool("send_message", {
29718
29863
  title: "Send Message",
29719
- description: "Send a direct message to another agent. Pass 'from' to identify yourself, or it falls back to CONVERSATIONS_AGENT_ID env var.",
29864
+ description: "Send a direct message to another agent.",
29720
29865
  inputSchema: {
29721
- from: exports_external.string().optional().describe("Your agent ID (e.g. 'claude-1', 'assistant'). Falls back to CONVERSATIONS_AGENT_ID env var."),
29866
+ from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
29722
29867
  to: exports_external.string().describe("Recipient agent ID"),
29723
29868
  content: exports_external.string().describe("Message content"),
29724
29869
  session_id: exports_external.string().optional().describe("Session ID (auto-generated if omitted)"),
@@ -29727,7 +29872,7 @@ server.registerTool("send_message", {
29727
29872
  repository: exports_external.string().optional().describe("Repository context"),
29728
29873
  branch: exports_external.string().optional().describe("Branch context"),
29729
29874
  metadata: exports_external.string().optional().describe("JSON metadata string"),
29730
- blocking: exports_external.boolean().optional().describe("Send as a blocking message. Recipients must acknowledge before continuing.")
29875
+ blocking: exports_external.boolean().optional().describe("Blocking message \u2014 recipients must acknowledge before continuing")
29731
29876
  }
29732
29877
  }, async ({ from: fromParam, to, content, session_id, priority, working_dir, repository, branch, metadata, blocking }) => {
29733
29878
  const from = resolveIdentity(fromParam);
@@ -29737,7 +29882,7 @@ server.registerTool("send_message", {
29737
29882
  parsedMetadata = JSON.parse(metadata);
29738
29883
  } catch {
29739
29884
  return {
29740
- content: [{ type: "text", text: "Invalid metadata JSON." }],
29885
+ content: [{ type: "text", text: "invalid JSON" }],
29741
29886
  isError: true
29742
29887
  };
29743
29888
  }
@@ -29760,15 +29905,15 @@ server.registerTool("send_message", {
29760
29905
  });
29761
29906
  server.registerTool("read_messages", {
29762
29907
  title: "Read Messages",
29763
- description: "Read messages with optional filters. Returns messages sorted by time.",
29908
+ description: "Read messages with filters, newest first.",
29764
29909
  inputSchema: {
29765
29910
  session_id: exports_external.string().optional().describe("Filter by session ID"),
29766
- from: exports_external.string().optional().describe("Filter by sender agent ID"),
29767
- to: exports_external.string().optional().describe("Filter by recipient agent ID"),
29911
+ from: exports_external.string().optional().describe("Filter by sender"),
29912
+ to: exports_external.string().optional().describe("Filter by recipient"),
29768
29913
  space: exports_external.string().optional().describe("Filter by space name"),
29769
- since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
29770
- limit: exports_external.number().optional().describe("Max messages to return"),
29771
- unread_only: exports_external.boolean().optional().describe("Only return unread messages")
29914
+ since: exports_external.string().optional().describe("ISO timestamp lower bound"),
29915
+ limit: exports_external.number().optional().describe("Max messages to return (default 20)"),
29916
+ unread_only: exports_external.boolean().optional().describe("Only unread messages")
29772
29917
  }
29773
29918
  }, async (opts) => {
29774
29919
  const messages = readMessages(opts);
@@ -29778,7 +29923,7 @@ server.registerTool("read_messages", {
29778
29923
  });
29779
29924
  server.registerTool("list_sessions", {
29780
29925
  title: "List Sessions",
29781
- description: "List conversation sessions, optionally filtered to a specific agent.",
29926
+ description: "List sessions, optionally filtered by agent.",
29782
29927
  inputSchema: {
29783
29928
  agent: exports_external.string().optional().describe("Filter sessions involving this agent")
29784
29929
  }
@@ -29790,7 +29935,7 @@ server.registerTool("list_sessions", {
29790
29935
  });
29791
29936
  server.registerTool("reply", {
29792
29937
  title: "Reply to Message",
29793
- description: "Reply to a message by its ID. Automatically uses the same session and sends to the original sender.",
29938
+ description: "Reply to a message (same session, original sender).",
29794
29939
  inputSchema: {
29795
29940
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
29796
29941
  message_id: exports_external.number().describe("ID of the message to reply to"),
@@ -29822,7 +29967,7 @@ server.registerTool("reply", {
29822
29967
  });
29823
29968
  server.registerTool("mark_read", {
29824
29969
  title: "Mark Read",
29825
- description: "Mark message IDs as read for the current agent. Set 'all' to true to mark all unread messages as read.",
29970
+ description: "Mark messages as read. Provide IDs or set 'all' to true.",
29826
29971
  inputSchema: {
29827
29972
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
29828
29973
  ids: exports_external.array(exports_external.number()).optional().describe("Message IDs to mark as read"),
@@ -29837,7 +29982,7 @@ server.registerTool("mark_read", {
29837
29982
  count = markRead(ids, agent);
29838
29983
  } else {
29839
29984
  return {
29840
- content: [{ type: "text", text: "Provide message IDs or set 'all' to true." }],
29985
+ content: [{ type: "text", text: "provide ids or set all=true" }],
29841
29986
  isError: true
29842
29987
  };
29843
29988
  }
@@ -29847,13 +29992,13 @@ server.registerTool("mark_read", {
29847
29992
  });
29848
29993
  server.registerTool("search_messages", {
29849
29994
  title: "Search Messages",
29850
- description: "Full-text search across message content. Returns matching messages ordered by newest first.",
29995
+ description: "Full-text search across message content, newest first.",
29851
29996
  inputSchema: {
29852
- query: exports_external.string().describe("Search query string"),
29853
- space: exports_external.string().optional().describe("Filter by space name"),
29854
- from: exports_external.string().optional().describe("Filter by sender agent ID"),
29855
- to: exports_external.string().optional().describe("Filter by recipient agent ID"),
29856
- limit: exports_external.number().optional().describe("Max results to return (default 50)")
29997
+ query: exports_external.string().describe("Search query"),
29998
+ space: exports_external.string().optional().describe("Filter by space"),
29999
+ from: exports_external.string().optional().describe("Filter by sender"),
30000
+ to: exports_external.string().optional().describe("Filter by recipient"),
30001
+ limit: exports_external.number().optional().describe("Max results (default 20)")
29857
30002
  }
29858
30003
  }, async ({ query, space, from, to, limit }) => {
29859
30004
  const messages = searchMessages({ query, space, from, to, limit });
@@ -29865,11 +30010,11 @@ server.registerTool("export_messages", {
29865
30010
  title: "Export Messages",
29866
30011
  description: "Export messages as JSON or CSV with optional filters.",
29867
30012
  inputSchema: {
29868
- space: exports_external.string().optional().describe("Filter by space name"),
30013
+ space: exports_external.string().optional().describe("Filter by space"),
29869
30014
  session_id: exports_external.string().optional().describe("Filter by session ID"),
29870
- from: exports_external.string().optional().describe("Filter by sender agent ID"),
29871
- since: exports_external.string().optional().describe("Messages after this ISO date"),
29872
- until: exports_external.string().optional().describe("Messages before this ISO date"),
30015
+ from: exports_external.string().optional().describe("Filter by sender"),
30016
+ since: exports_external.string().optional().describe("ISO date lower bound"),
30017
+ until: exports_external.string().optional().describe("ISO date upper bound"),
29873
30018
  format: exports_external.enum(["json", "csv"]).optional().describe("Output format (default: json)")
29874
30019
  }
29875
30020
  }, async ({ space, session_id, from, since, until, format }) => {
@@ -29880,13 +30025,13 @@ server.registerTool("export_messages", {
29880
30025
  });
29881
30026
  server.registerTool("create_space", {
29882
30027
  title: "Create Space",
29883
- description: "Create a new space. The creator is auto-joined. Spaces can be nested (max 3 levels) and associated with a project.",
30028
+ description: "Create a space. Auto-joined. Supports nesting and projects.",
29884
30029
  inputSchema: {
29885
30030
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
29886
- name: exports_external.string().describe("Space name (e.g. 'deployments', 'code-review')"),
30031
+ name: exports_external.string().describe("Space name"),
29887
30032
  description: exports_external.string().optional().describe("Space description"),
29888
- parent_id: exports_external.string().optional().describe("Parent space name for nesting (max 3 levels deep)"),
29889
- project_id: exports_external.string().optional().describe("Project ID to associate this space with")
30033
+ parent_id: exports_external.string().optional().describe("Parent space name (max 3 levels deep)"),
30034
+ project_id: exports_external.string().optional().describe("Project ID to associate with")
29890
30035
  }
29891
30036
  }, async ({ from: fromParam, name, description, parent_id, project_id }) => {
29892
30037
  const agent = resolveIdentity(fromParam);
@@ -29898,7 +30043,7 @@ server.registerTool("create_space", {
29898
30043
  } catch (e) {
29899
30044
  if (e.message?.includes("UNIQUE constraint")) {
29900
30045
  return {
29901
- content: [{ type: "text", text: `Space #${name} already exists` }],
30046
+ content: [{ type: "text", text: `space "${name}" already exists` }],
29902
30047
  isError: true
29903
30048
  };
29904
30049
  }
@@ -29910,11 +30055,11 @@ server.registerTool("create_space", {
29910
30055
  });
29911
30056
  server.registerTool("list_spaces", {
29912
30057
  title: "List Spaces",
29913
- description: "List all available spaces with member and message counts. Can filter by project or parent. Archived spaces are excluded by default.",
30058
+ description: "List spaces with member/message counts.",
29914
30059
  inputSchema: {
29915
30060
  project_id: exports_external.string().optional().describe("Filter by project ID"),
29916
- parent_id: exports_external.string().optional().describe("Filter by parent space name. Use 'null' for top-level only."),
29917
- include_archived: exports_external.boolean().optional().describe("Include archived spaces (default: false)")
30061
+ parent_id: exports_external.string().optional().describe("Filter by parent space. Use 'null' for top-level only."),
30062
+ include_archived: exports_external.boolean().optional().describe("Include archived spaces")
29918
30063
  }
29919
30064
  }, async ({ project_id, parent_id, include_archived }) => {
29920
30065
  const opts = {};
@@ -29940,14 +30085,14 @@ server.registerTool("send_to_space", {
29940
30085
  space: exports_external.string().describe("Space name"),
29941
30086
  content: exports_external.string().describe("Message content"),
29942
30087
  priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional().describe("Message priority"),
29943
- blocking: exports_external.boolean().optional().describe("Send as a blocking message. All space members must acknowledge.")
30088
+ blocking: exports_external.boolean().optional().describe("Blocking message \u2014 all space members must acknowledge")
29944
30089
  }
29945
30090
  }, async ({ from: fromParam, space, content, priority, blocking }) => {
29946
30091
  const from = resolveIdentity(fromParam);
29947
30092
  const sp = getSpace(space);
29948
30093
  if (!sp) {
29949
30094
  return {
29950
- content: [{ type: "text", text: `Space #${space} not found` }],
30095
+ content: [{ type: "text", text: `space "${space}" not found` }],
29951
30096
  isError: true
29952
30097
  };
29953
30098
  }
@@ -29969,7 +30114,7 @@ server.registerTool("read_space", {
29969
30114
  description: "Read messages from a space.",
29970
30115
  inputSchema: {
29971
30116
  space: exports_external.string().describe("Space name"),
29972
- since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
30117
+ since: exports_external.string().optional().describe("ISO timestamp lower bound"),
29973
30118
  limit: exports_external.number().optional().describe("Max messages to return")
29974
30119
  }
29975
30120
  }, async ({ space, since, limit }) => {
@@ -29990,7 +30135,7 @@ server.registerTool("join_space", {
29990
30135
  const ok = joinSpace(space, agent);
29991
30136
  if (!ok) {
29992
30137
  return {
29993
- content: [{ type: "text", text: `Space #${space} not found` }],
30138
+ content: [{ type: "text", text: `space "${space}" not found` }],
29994
30139
  isError: true
29995
30140
  };
29996
30141
  }
@@ -30014,12 +30159,12 @@ server.registerTool("leave_space", {
30014
30159
  });
30015
30160
  server.registerTool("update_space", {
30016
30161
  title: "Update Space",
30017
- description: "Update a space's description, parent, or project association.",
30162
+ description: "Update a space's description, parent, or project.",
30018
30163
  inputSchema: {
30019
- name: exports_external.string().describe("Space name to update"),
30164
+ name: exports_external.string().describe("Space name"),
30020
30165
  description: exports_external.string().optional().describe("New description"),
30021
- parent_id: exports_external.string().optional().describe("New parent space name (use 'null' to remove parent)"),
30022
- project_id: exports_external.string().optional().describe("New project ID (use 'null' to remove project)")
30166
+ parent_id: exports_external.string().optional().describe("New parent space (use 'null' to remove)"),
30167
+ project_id: exports_external.string().optional().describe("New project ID (use 'null' to remove)")
30023
30168
  }
30024
30169
  }, async ({ name, description, parent_id, project_id }) => {
30025
30170
  const updates = {};
@@ -30043,7 +30188,7 @@ server.registerTool("update_space", {
30043
30188
  });
30044
30189
  server.registerTool("archive_space", {
30045
30190
  title: "Archive Space",
30046
- description: "Archive a space. Archived spaces are hidden from list by default.",
30191
+ description: "Archive a space. Hidden from list by default.",
30047
30192
  inputSchema: {
30048
30193
  name: exports_external.string().describe("Space name to archive")
30049
30194
  }
@@ -30081,16 +30226,16 @@ server.registerTool("unarchive_space", {
30081
30226
  });
30082
30227
  server.registerTool("create_project", {
30083
30228
  title: "Create Project",
30084
- description: "Create a new project. Projects organize spaces and provide context for agent collaboration.",
30229
+ description: "Create a project for spaces and agent collaboration.",
30085
30230
  inputSchema: {
30086
30231
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30087
30232
  name: exports_external.string().describe("Project name (unique)"),
30088
30233
  description: exports_external.string().optional().describe("Project description"),
30089
- path: exports_external.string().optional().describe("Absolute path to project on disk"),
30234
+ path: exports_external.string().optional().describe("Absolute path on disk"),
30090
30235
  repository: exports_external.string().optional().describe("Repository URL"),
30091
- tags: exports_external.string().optional().describe(`JSON array of tags (e.g. '["backend", "api"]')`),
30092
- metadata: exports_external.string().optional().describe("JSON metadata string"),
30093
- settings: exports_external.string().optional().describe("JSON settings string")
30236
+ tags: exports_external.string().optional().describe("JSON array of tags"),
30237
+ metadata: exports_external.string().optional().describe("JSON metadata"),
30238
+ settings: exports_external.string().optional().describe("JSON settings")
30094
30239
  }
30095
30240
  }, async ({ from: fromParam, name, description, path, repository, tags, metadata, settings }) => {
30096
30241
  const agent = resolveIdentity(fromParam);
@@ -30100,7 +30245,7 @@ server.registerTool("create_project", {
30100
30245
  parsedTags = JSON.parse(tags);
30101
30246
  } catch {
30102
30247
  return {
30103
- content: [{ type: "text", text: "Invalid tags JSON. Expected array of strings." }],
30248
+ content: [{ type: "text", text: "invalid tags JSON (expected array)" }],
30104
30249
  isError: true
30105
30250
  };
30106
30251
  }
@@ -30111,7 +30256,7 @@ server.registerTool("create_project", {
30111
30256
  parsedMetadata = JSON.parse(metadata);
30112
30257
  } catch {
30113
30258
  return {
30114
- content: [{ type: "text", text: "Invalid metadata JSON." }],
30259
+ content: [{ type: "text", text: "invalid JSON" }],
30115
30260
  isError: true
30116
30261
  };
30117
30262
  }
@@ -30122,7 +30267,7 @@ server.registerTool("create_project", {
30122
30267
  parsedSettings = JSON.parse(settings);
30123
30268
  } catch {
30124
30269
  return {
30125
- content: [{ type: "text", text: "Invalid settings JSON." }],
30270
+ content: [{ type: "text", text: "invalid JSON" }],
30126
30271
  isError: true
30127
30272
  };
30128
30273
  }
@@ -30144,7 +30289,7 @@ server.registerTool("create_project", {
30144
30289
  } catch (e) {
30145
30290
  if (e.message?.includes("UNIQUE constraint")) {
30146
30291
  return {
30147
- content: [{ type: "text", text: `Project "${name}" already exists` }],
30292
+ content: [{ type: "text", text: `project "${name}" already exists` }],
30148
30293
  isError: true
30149
30294
  };
30150
30295
  }
@@ -30158,7 +30303,7 @@ server.registerTool("list_projects", {
30158
30303
  title: "List Projects",
30159
30304
  description: "List all registered projects.",
30160
30305
  inputSchema: {
30161
- status: exports_external.enum(["active", "archived"]).optional().describe("Filter by project status")
30306
+ status: exports_external.enum(["active", "archived"]).optional().describe("Filter by status")
30162
30307
  }
30163
30308
  }, async ({ status }) => {
30164
30309
  const projects = listProjects(status ? { status } : undefined);
@@ -30179,7 +30324,7 @@ server.registerTool("get_project", {
30179
30324
  }
30180
30325
  if (!project) {
30181
30326
  return {
30182
- content: [{ type: "text", text: `Project "${id}" not found` }],
30327
+ content: [{ type: "text", text: `project "${id}" not found` }],
30183
30328
  isError: true
30184
30329
  };
30185
30330
  }
@@ -30192,14 +30337,14 @@ server.registerTool("update_project", {
30192
30337
  description: "Update a project's fields.",
30193
30338
  inputSchema: {
30194
30339
  id: exports_external.string().describe("Project ID (UUID)"),
30195
- name: exports_external.string().optional().describe("New project name"),
30340
+ name: exports_external.string().optional().describe("New name"),
30196
30341
  description: exports_external.string().optional().describe("New description"),
30197
30342
  path: exports_external.string().optional().describe("New path"),
30198
30343
  status: exports_external.enum(["active", "archived"]).optional().describe("New status"),
30199
30344
  repository: exports_external.string().optional().describe("New repository URL"),
30200
30345
  tags: exports_external.string().optional().describe("JSON array of tags"),
30201
- metadata: exports_external.string().optional().describe("JSON metadata string"),
30202
- settings: exports_external.string().optional().describe("JSON settings string")
30346
+ metadata: exports_external.string().optional().describe("JSON metadata"),
30347
+ settings: exports_external.string().optional().describe("JSON settings")
30203
30348
  }
30204
30349
  }, async ({ id, name, description, path, status, repository, tags, metadata, settings }) => {
30205
30350
  const updates = {};
@@ -30218,7 +30363,7 @@ server.registerTool("update_project", {
30218
30363
  updates.tags = JSON.parse(tags);
30219
30364
  } catch {
30220
30365
  return {
30221
- content: [{ type: "text", text: "Invalid tags JSON." }],
30366
+ content: [{ type: "text", text: "invalid tags JSON" }],
30222
30367
  isError: true
30223
30368
  };
30224
30369
  }
@@ -30228,7 +30373,7 @@ server.registerTool("update_project", {
30228
30373
  updates.metadata = JSON.parse(metadata);
30229
30374
  } catch {
30230
30375
  return {
30231
- content: [{ type: "text", text: "Invalid metadata JSON." }],
30376
+ content: [{ type: "text", text: "invalid JSON" }],
30232
30377
  isError: true
30233
30378
  };
30234
30379
  }
@@ -30238,7 +30383,7 @@ server.registerTool("update_project", {
30238
30383
  updates.settings = JSON.parse(settings);
30239
30384
  } catch {
30240
30385
  return {
30241
- content: [{ type: "text", text: "Invalid settings JSON." }],
30386
+ content: [{ type: "text", text: "invalid JSON" }],
30242
30387
  isError: true
30243
30388
  };
30244
30389
  }
@@ -30257,7 +30402,7 @@ server.registerTool("update_project", {
30257
30402
  });
30258
30403
  server.registerTool("delete_project", {
30259
30404
  title: "Delete Project",
30260
- description: "Delete a project permanently. Fails if spaces still reference it.",
30405
+ description: "Delete a project permanently. Fails if spaces reference it.",
30261
30406
  inputSchema: {
30262
30407
  id: exports_external.string().describe("Project ID (UUID)")
30263
30408
  }
@@ -30266,7 +30411,7 @@ server.registerTool("delete_project", {
30266
30411
  const deleted = deleteProject(id);
30267
30412
  if (!deleted) {
30268
30413
  return {
30269
- content: [{ type: "text", text: `Project "${id}" not found` }],
30414
+ content: [{ type: "text", text: `project "${id}" not found` }],
30270
30415
  isError: true
30271
30416
  };
30272
30417
  }
@@ -30282,7 +30427,7 @@ server.registerTool("delete_project", {
30282
30427
  });
30283
30428
  server.registerTool("delete_message", {
30284
30429
  title: "Delete Message",
30285
- description: "Delete a message. Only the sender can delete their own messages.",
30430
+ description: "Delete a message. Sender only.",
30286
30431
  inputSchema: {
30287
30432
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30288
30433
  id: exports_external.number().describe("Message ID to delete")
@@ -30292,7 +30437,7 @@ server.registerTool("delete_message", {
30292
30437
  const deleted = deleteMessage(id, agent);
30293
30438
  if (!deleted) {
30294
30439
  return {
30295
- content: [{ type: "text", text: `Message #${id} not found or not your message` }],
30440
+ content: [{ type: "text", text: `not found or forbidden` }],
30296
30441
  isError: true
30297
30442
  };
30298
30443
  }
@@ -30302,7 +30447,7 @@ server.registerTool("delete_message", {
30302
30447
  });
30303
30448
  server.registerTool("edit_message", {
30304
30449
  title: "Edit Message",
30305
- description: "Edit a message's content. Only the sender can edit their own messages.",
30450
+ description: "Edit a message's content. Sender only.",
30306
30451
  inputSchema: {
30307
30452
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30308
30453
  id: exports_external.number().describe("Message ID to edit"),
@@ -30313,7 +30458,7 @@ server.registerTool("edit_message", {
30313
30458
  const msg = editMessage(id, agent, content);
30314
30459
  if (!msg) {
30315
30460
  return {
30316
- content: [{ type: "text", text: `Message #${id} not found or not your message` }],
30461
+ content: [{ type: "text", text: `not found or forbidden` }],
30317
30462
  isError: true
30318
30463
  };
30319
30464
  }
@@ -30323,7 +30468,7 @@ server.registerTool("edit_message", {
30323
30468
  });
30324
30469
  server.registerTool("pin_message", {
30325
30470
  title: "Pin Message",
30326
- description: "Pin a message. Pinned messages can be retrieved with get_pinned_messages.",
30471
+ description: "Pin a message in a space or session.",
30327
30472
  inputSchema: {
30328
30473
  id: exports_external.number().describe("Message ID to pin")
30329
30474
  }
@@ -30331,7 +30476,7 @@ server.registerTool("pin_message", {
30331
30476
  const msg = pinMessage(id);
30332
30477
  if (!msg) {
30333
30478
  return {
30334
- content: [{ type: "text", text: `Message #${id} not found` }],
30479
+ content: [{ type: "text", text: `message #${id} not found` }],
30335
30480
  isError: true
30336
30481
  };
30337
30482
  }
@@ -30349,7 +30494,7 @@ server.registerTool("unpin_message", {
30349
30494
  const msg = unpinMessage(id);
30350
30495
  if (!msg) {
30351
30496
  return {
30352
- content: [{ type: "text", text: `Message #${id} not found` }],
30497
+ content: [{ type: "text", text: `message #${id} not found` }],
30353
30498
  isError: true
30354
30499
  };
30355
30500
  }
@@ -30359,9 +30504,9 @@ server.registerTool("unpin_message", {
30359
30504
  });
30360
30505
  server.registerTool("get_pinned_messages", {
30361
30506
  title: "Get Pinned Messages",
30362
- description: "Retrieve pinned messages, optionally filtered by space or session.",
30507
+ description: "Get pinned messages, filtered by space or session.",
30363
30508
  inputSchema: {
30364
- space: exports_external.string().optional().describe("Filter by space name"),
30509
+ space: exports_external.string().optional().describe("Filter by space"),
30365
30510
  session_id: exports_external.string().optional().describe("Filter by session ID"),
30366
30511
  limit: exports_external.number().optional().describe("Max messages to return")
30367
30512
  }
@@ -30373,7 +30518,7 @@ server.registerTool("get_pinned_messages", {
30373
30518
  });
30374
30519
  server.registerTool("heartbeat", {
30375
30520
  title: "Heartbeat",
30376
- description: "Send a heartbeat to indicate agent is alive. Optionally set a status.",
30521
+ description: "Send heartbeat. Optionally set agent status.",
30377
30522
  inputSchema: {
30378
30523
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30379
30524
  status: exports_external.string().optional().describe("Agent status (e.g. 'online', 'busy', 'idle'). Defaults to 'online'.")
@@ -30387,9 +30532,9 @@ server.registerTool("heartbeat", {
30387
30532
  });
30388
30533
  server.registerTool("list_agents", {
30389
30534
  title: "List Agents",
30390
- description: "List all agents with their presence status. Returns agent name, status, last seen time, and whether they are online.",
30535
+ description: "List agents with presence status.",
30391
30536
  inputSchema: {
30392
- online_only: exports_external.boolean().optional().describe("Only return agents that are currently online (seen within last 60 seconds)")
30537
+ online_only: exports_external.boolean().optional().describe("Only return agents online within last 60s")
30393
30538
  }
30394
30539
  }, async ({ online_only }) => {
30395
30540
  const agents = listAgents({ online_only });
@@ -30399,7 +30544,7 @@ server.registerTool("list_agents", {
30399
30544
  });
30400
30545
  server.registerTool("get_blockers", {
30401
30546
  title: "Get Blockers",
30402
- description: "Check for unread blocking messages targeting you. Returns messages that must be acknowledged before continuing.",
30547
+ description: "Check for unread blocking messages. Must acknowledge.",
30403
30548
  inputSchema: {
30404
30549
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var.")
30405
30550
  }
@@ -30412,7 +30557,7 @@ server.registerTool("get_blockers", {
30412
30557
  });
30413
30558
  server.registerTool("remove_agent", {
30414
30559
  title: "Remove Agent",
30415
- description: "Remove an agent from the presence list. Only the agent itself should remove its own presence.",
30560
+ description: "Remove an agent from the presence list.",
30416
30561
  inputSchema: {
30417
30562
  from: exports_external.string().optional().describe("Your agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30418
30563
  agent: exports_external.string().optional().describe("Agent to remove (defaults to yourself)")
@@ -30423,7 +30568,7 @@ server.registerTool("remove_agent", {
30423
30568
  const removed = removePresence(agent);
30424
30569
  if (!removed) {
30425
30570
  return {
30426
- content: [{ type: "text", text: `Agent "${agent}" not found` }],
30571
+ content: [{ type: "text", text: `agent "${agent}" not found` }],
30427
30572
  isError: true
30428
30573
  };
30429
30574
  }
@@ -30433,7 +30578,7 @@ server.registerTool("remove_agent", {
30433
30578
  });
30434
30579
  server.registerTool("rename_agent", {
30435
30580
  title: "Rename Agent",
30436
- description: "Rename an agent in the presence list. By default renames yourself.",
30581
+ description: "Rename an agent in the presence list.",
30437
30582
  inputSchema: {
30438
30583
  from: exports_external.string().optional().describe("Your current agent ID. Falls back to CONVERSATIONS_AGENT_ID env var."),
30439
30584
  new_name: exports_external.string().describe("The new name for the agent")
@@ -30443,7 +30588,7 @@ server.registerTool("rename_agent", {
30443
30588
  const newName = new_name.trim();
30444
30589
  if (!newName) {
30445
30590
  return {
30446
- content: [{ type: "text", text: "New name cannot be empty" }],
30591
+ content: [{ type: "text", text: "new name cannot be empty" }],
30447
30592
  isError: true
30448
30593
  };
30449
30594
  }
@@ -30451,7 +30596,7 @@ server.registerTool("rename_agent", {
30451
30596
  const renamed = renameAgent(oldName, newName);
30452
30597
  if (!renamed) {
30453
30598
  return {
30454
- content: [{ type: "text", text: `Agent "${oldName}" not found in presence list` }],
30599
+ content: [{ type: "text", text: `agent "${oldName}" not found` }],
30455
30600
  isError: true
30456
30601
  };
30457
30602
  }
@@ -30465,6 +30610,78 @@ server.registerTool("rename_agent", {
30465
30610
  };
30466
30611
  }
30467
30612
  });
30613
+ server.registerTool("search_tools", {
30614
+ title: "Search Tools",
30615
+ description: "List tool names, optionally filtered by keyword.",
30616
+ inputSchema: {
30617
+ query: exports_external.string().optional().describe("Keyword filter")
30618
+ }
30619
+ }, async ({ query }) => {
30620
+ const all = [
30621
+ "send_message",
30622
+ "read_messages",
30623
+ "list_sessions",
30624
+ "reply",
30625
+ "mark_read",
30626
+ "search_messages",
30627
+ "export_messages",
30628
+ "create_space",
30629
+ "list_spaces",
30630
+ "send_to_space",
30631
+ "read_space",
30632
+ "join_space",
30633
+ "leave_space",
30634
+ "update_space",
30635
+ "archive_space",
30636
+ "unarchive_space",
30637
+ "create_project",
30638
+ "list_projects",
30639
+ "get_project",
30640
+ "update_project",
30641
+ "delete_project",
30642
+ "delete_message",
30643
+ "edit_message",
30644
+ "pin_message",
30645
+ "unpin_message",
30646
+ "get_pinned_messages",
30647
+ "heartbeat",
30648
+ "list_agents",
30649
+ "get_blockers",
30650
+ "remove_agent",
30651
+ "rename_agent",
30652
+ "search_tools",
30653
+ "describe_tools"
30654
+ ];
30655
+ const q = query?.toLowerCase();
30656
+ const matches = q ? all.filter((n) => n.includes(q)) : all;
30657
+ return { content: [{ type: "text", text: matches.join(", ") }] };
30658
+ });
30659
+ server.registerTool("describe_tools", {
30660
+ title: "Describe Tools",
30661
+ description: "Get descriptions for specific tools by name.",
30662
+ inputSchema: {
30663
+ names: exports_external.array(exports_external.string()).describe("Tool names from search_tools")
30664
+ }
30665
+ }, async ({ names }) => {
30666
+ const descriptions = {
30667
+ send_message: "Send DM to agent. Params: to, content, from?, priority?",
30668
+ read_messages: "Read messages. Params: space?, from?, to?, unread_only?, limit?",
30669
+ list_sessions: "List sessions. Params: agent?",
30670
+ reply: "Reply to message. Params: id, content, from?",
30671
+ send_to_space: "Send message to space. Params: space, content, from?",
30672
+ read_space: "Read space messages. Params: space, limit?, since?",
30673
+ join_space: "Join a space. Params: space, from?",
30674
+ create_space: "Create space. Params: name, description?, parent_id?",
30675
+ list_spaces: "List spaces with counts. No required params.",
30676
+ heartbeat: "Send heartbeat. Params: from?, status?",
30677
+ list_agents: "List agents with presence. No required params.",
30678
+ get_blockers: "Check for blocking messages. Params: name?",
30679
+ search_messages: "Search messages. Params: query, space?, limit?"
30680
+ };
30681
+ const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
30682
+ `);
30683
+ return { content: [{ type: "text", text: result }] };
30684
+ });
30468
30685
  async function startMcpServer() {
30469
30686
  const transport = new StdioServerTransport;
30470
30687
  await server.connect(transport);