@hasna/conversations 0.2.5 → 0.2.7
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 +14 -0
- package/bin/index.js +159 -15
- package/bin/mcp.js +159 -15
- package/dist/index.js +60 -0
- package/dist/lib/messages.d.ts +21 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/bin/hook.js
CHANGED
|
@@ -252,6 +252,20 @@ function getDb() {
|
|
|
252
252
|
if (!presenceColNames.includes("project_id")) {
|
|
253
253
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
254
254
|
}
|
|
255
|
+
db.exec(`
|
|
256
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
257
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
258
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
259
|
+
mentioned_agent TEXT NOT NULL,
|
|
260
|
+
from_agent TEXT NOT NULL,
|
|
261
|
+
space TEXT,
|
|
262
|
+
notified_at TEXT,
|
|
263
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
264
|
+
)
|
|
265
|
+
`);
|
|
266
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
|
|
267
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
|
|
268
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
|
|
255
269
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
256
270
|
if (!ftsExists) {
|
|
257
271
|
db.exec(`
|
package/bin/index.js
CHANGED
|
@@ -2117,6 +2117,20 @@ function getDb() {
|
|
|
2117
2117
|
if (!presenceColNames.includes("project_id")) {
|
|
2118
2118
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
2119
2119
|
}
|
|
2120
|
+
db.exec(`
|
|
2121
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
2122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2123
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
2124
|
+
mentioned_agent TEXT NOT NULL,
|
|
2125
|
+
from_agent TEXT NOT NULL,
|
|
2126
|
+
space TEXT,
|
|
2127
|
+
notified_at TEXT,
|
|
2128
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
2129
|
+
)
|
|
2130
|
+
`);
|
|
2131
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
|
|
2132
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
|
|
2133
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
|
|
2120
2134
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
2121
2135
|
if (!ftsExists) {
|
|
2122
2136
|
db.exec(`
|
|
@@ -2330,6 +2344,12 @@ function sendMessage(opts) {
|
|
|
2330
2344
|
db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
|
|
2331
2345
|
message.attachments = attachmentInfos;
|
|
2332
2346
|
}
|
|
2347
|
+
if (opts.space) {
|
|
2348
|
+
const mentions = parseMentions(opts.content);
|
|
2349
|
+
if (mentions.length > 0) {
|
|
2350
|
+
processMentions(message.id, opts.from, opts.space, mentions, db2);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2333
2353
|
fireWebhooks(message);
|
|
2334
2354
|
return message;
|
|
2335
2355
|
}
|
|
@@ -2371,6 +2391,10 @@ function readMessages(opts = {}) {
|
|
|
2371
2391
|
if (opts.threads_only) {
|
|
2372
2392
|
conditions.push("reply_to IS NULL");
|
|
2373
2393
|
}
|
|
2394
|
+
if (opts.mentions_only) {
|
|
2395
|
+
conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
|
|
2396
|
+
params.push(opts.mentions_only.toLowerCase());
|
|
2397
|
+
}
|
|
2374
2398
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2375
2399
|
const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
2376
2400
|
const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
@@ -2622,6 +2646,14 @@ function searchMessages(opts) {
|
|
|
2622
2646
|
extraWhere += " AND m.to_agent = ?";
|
|
2623
2647
|
ftsParams.push(opts.to);
|
|
2624
2648
|
}
|
|
2649
|
+
if (opts.since) {
|
|
2650
|
+
extraWhere += " AND m.created_at >= ?";
|
|
2651
|
+
ftsParams.push(opts.since);
|
|
2652
|
+
}
|
|
2653
|
+
if (opts.until) {
|
|
2654
|
+
extraWhere += " AND m.created_at <= ?";
|
|
2655
|
+
ftsParams.push(opts.until);
|
|
2656
|
+
}
|
|
2625
2657
|
const orderClause = sortByRelevance ? "ORDER BY rank" : "ORDER BY m.created_at DESC, m.id DESC";
|
|
2626
2658
|
const rows2 = db2.prepare(`SELECT m.*, rank,
|
|
2627
2659
|
snippet(messages_fts, 0, '**', '**', '...', 20) as snippet
|
|
@@ -2654,6 +2686,14 @@ function searchMessages(opts) {
|
|
|
2654
2686
|
conditions.push("to_agent = ?");
|
|
2655
2687
|
params.push(opts.to);
|
|
2656
2688
|
}
|
|
2689
|
+
if (opts.since) {
|
|
2690
|
+
conditions.push("created_at >= ?");
|
|
2691
|
+
params.push(opts.since);
|
|
2692
|
+
}
|
|
2693
|
+
if (opts.until) {
|
|
2694
|
+
conditions.push("created_at <= ?");
|
|
2695
|
+
params.push(opts.until);
|
|
2696
|
+
}
|
|
2657
2697
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
2658
2698
|
const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
|
|
2659
2699
|
return rows.map((row) => {
|
|
@@ -2694,6 +2734,73 @@ function listUnreadCounts(agent) {
|
|
|
2694
2734
|
`).all();
|
|
2695
2735
|
return rows;
|
|
2696
2736
|
}
|
|
2737
|
+
function parseMentions(content) {
|
|
2738
|
+
const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
|
|
2739
|
+
return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
|
|
2740
|
+
}
|
|
2741
|
+
async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
|
|
2742
|
+
const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
|
|
2743
|
+
for (const agent of mentionedAgents) {
|
|
2744
|
+
try {
|
|
2745
|
+
stmt.run(messageId, agent, fromAgent, space);
|
|
2746
|
+
if (agent !== fromAgent.toLowerCase()) {
|
|
2747
|
+
sendMessage({
|
|
2748
|
+
from: fromAgent,
|
|
2749
|
+
to: agent,
|
|
2750
|
+
content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
|
|
2751
|
+
metadata: { type: "mention_notification", source_message_id: messageId, space }
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
} catch {}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
function listUnreadCountsWithMentions(agent) {
|
|
2758
|
+
const db2 = getDb();
|
|
2759
|
+
const rows = db2.prepare(`
|
|
2760
|
+
SELECT
|
|
2761
|
+
space,
|
|
2762
|
+
COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
|
|
2763
|
+
(SELECT COUNT(*) FROM message_mentions mm WHERE mm.space = m.space AND mm.mentioned_agent = ? AND mm.notified_at IS NULL) AS mention_count,
|
|
2764
|
+
MAX(created_at) AS latest_message_at
|
|
2765
|
+
FROM messages m
|
|
2766
|
+
WHERE space IN (
|
|
2767
|
+
SELECT DISTINCT space FROM space_members WHERE agent = ?
|
|
2768
|
+
UNION
|
|
2769
|
+
SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
|
|
2770
|
+
)
|
|
2771
|
+
GROUP BY space
|
|
2772
|
+
HAVING COUNT(*) > 0
|
|
2773
|
+
ORDER BY mention_count DESC, unread_count DESC, latest_message_at DESC
|
|
2774
|
+
`).all(agent, agent, agent, agent);
|
|
2775
|
+
return rows;
|
|
2776
|
+
}
|
|
2777
|
+
function getMessagesForAgent(agent, opts) {
|
|
2778
|
+
const db2 = getDb();
|
|
2779
|
+
const conditions = ["mm.mentioned_agent = ?"];
|
|
2780
|
+
const params = [agent.toLowerCase()];
|
|
2781
|
+
if (opts?.space) {
|
|
2782
|
+
conditions.push("m.space = ?");
|
|
2783
|
+
params.push(opts.space);
|
|
2784
|
+
}
|
|
2785
|
+
if (opts?.unread_only) {
|
|
2786
|
+
conditions.push("mm.notified_at IS NULL");
|
|
2787
|
+
}
|
|
2788
|
+
const limit = opts?.limit ?? 50;
|
|
2789
|
+
const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
|
|
2790
|
+
JOIN message_mentions mm ON mm.message_id = m.id
|
|
2791
|
+
WHERE ${conditions.join(" AND ")}
|
|
2792
|
+
ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
|
|
2793
|
+
return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
|
|
2794
|
+
}
|
|
2795
|
+
function markMentionsRead(agent, space) {
|
|
2796
|
+
const db2 = getDb();
|
|
2797
|
+
if (space) {
|
|
2798
|
+
const result2 = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND space = ? AND notified_at IS NULL").run(agent, space);
|
|
2799
|
+
return result2.changes;
|
|
2800
|
+
}
|
|
2801
|
+
const result = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND notified_at IS NULL").run(agent);
|
|
2802
|
+
return result.changes;
|
|
2803
|
+
}
|
|
2697
2804
|
var init_messages = __esm(() => {
|
|
2698
2805
|
init_db();
|
|
2699
2806
|
init_webhooks();
|
|
@@ -4333,7 +4440,7 @@ var init_poll = __esm(() => {
|
|
|
4333
4440
|
var require_package = __commonJS((exports, module) => {
|
|
4334
4441
|
module.exports = {
|
|
4335
4442
|
name: "@hasna/conversations",
|
|
4336
|
-
version: "0.2.
|
|
4443
|
+
version: "0.2.7",
|
|
4337
4444
|
description: "Real-time CLI messaging for AI agents",
|
|
4338
4445
|
type: "module",
|
|
4339
4446
|
bin: {
|
|
@@ -33588,7 +33695,8 @@ var init_mcp2 = __esm(() => {
|
|
|
33588
33695
|
mark_read: exports_external.coerce.boolean().optional(),
|
|
33589
33696
|
max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
|
|
33590
33697
|
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)")
|
|
33698
|
+
include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)"),
|
|
33699
|
+
mentions_only: exports_external.string().optional().describe("Only return messages that @mention this agent")
|
|
33592
33700
|
}
|
|
33593
33701
|
}, async (args) => {
|
|
33594
33702
|
const agent = resolveIdentity(args.from);
|
|
@@ -33671,19 +33779,26 @@ var init_mcp2 = __esm(() => {
|
|
|
33671
33779
|
};
|
|
33672
33780
|
});
|
|
33673
33781
|
server.registerTool("search_messages", {
|
|
33674
|
-
description: "Full-text search across messages.",
|
|
33782
|
+
description: "Full-text search across messages. Uses FTS5 with BM25 ranking if available, falls back to LIKE. Returns messages with snippet and relevance_score.",
|
|
33675
33783
|
inputSchema: {
|
|
33676
|
-
query: exports_external.string(),
|
|
33677
|
-
space: exports_external.string().optional(),
|
|
33678
|
-
from: exports_external.string().optional(),
|
|
33679
|
-
to: exports_external.string().optional(),
|
|
33680
|
-
|
|
33784
|
+
query: exports_external.string().describe(`Search query. Wrap in quotes for exact phrase: '"BUG-005"'`),
|
|
33785
|
+
space: exports_external.string().optional().describe("Limit to a specific space"),
|
|
33786
|
+
from: exports_external.string().optional().describe("Filter by sender"),
|
|
33787
|
+
to: exports_external.string().optional().describe("Filter by recipient"),
|
|
33788
|
+
since: exports_external.string().optional().describe("ISO 8601 date \u2014 only messages after this"),
|
|
33789
|
+
until: exports_external.string().optional().describe("ISO 8601 date \u2014 only messages before this"),
|
|
33790
|
+
sort: exports_external.enum(["relevance", "recent"]).optional().describe("Sort order (default: relevance)"),
|
|
33791
|
+
limit: exports_external.coerce.number().optional().describe("Max results (default: 20)")
|
|
33681
33792
|
}
|
|
33682
33793
|
}, async (args) => {
|
|
33683
|
-
const { query, space, from, to, limit } = args;
|
|
33684
|
-
const
|
|
33794
|
+
const { query, space, from, to, since, until, sort, limit } = args;
|
|
33795
|
+
const results = searchMessages({ query, space, from, to, since, until, sort, limit });
|
|
33685
33796
|
return {
|
|
33686
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
33797
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
33798
|
+
results,
|
|
33799
|
+
count: results.length,
|
|
33800
|
+
query
|
|
33801
|
+
}) }]
|
|
33687
33802
|
};
|
|
33688
33803
|
});
|
|
33689
33804
|
server.registerTool("export_messages", {
|
|
@@ -33760,13 +33875,42 @@ var init_mcp2 = __esm(() => {
|
|
|
33760
33875
|
server.registerTool("list_unread_counts", {
|
|
33761
33876
|
description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
|
|
33762
33877
|
inputSchema: {
|
|
33763
|
-
agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts.")
|
|
33878
|
+
agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts."),
|
|
33879
|
+
include_mentions: exports_external.coerce.boolean().optional().describe("Include mention_count per space (requires agent)")
|
|
33764
33880
|
}
|
|
33765
33881
|
}, async (args) => {
|
|
33882
|
+
if (args.agent && args.include_mentions) {
|
|
33883
|
+
const counts2 = listUnreadCountsWithMentions(args.agent);
|
|
33884
|
+
return { content: [{ type: "text", text: JSON.stringify(counts2) }] };
|
|
33885
|
+
}
|
|
33766
33886
|
const counts = listUnreadCounts(args.agent);
|
|
33767
|
-
return {
|
|
33768
|
-
|
|
33769
|
-
|
|
33887
|
+
return { content: [{ type: "text", text: JSON.stringify(counts) }] };
|
|
33888
|
+
});
|
|
33889
|
+
server.registerTool("get_mentions", {
|
|
33890
|
+
description: "Get messages that @mention a specific agent. Useful for catching up on missed pings.",
|
|
33891
|
+
inputSchema: {
|
|
33892
|
+
agent: exports_external.string().describe("Agent name to find mentions for"),
|
|
33893
|
+
space: exports_external.string().optional().describe("Filter to a specific space"),
|
|
33894
|
+
unread_only: exports_external.coerce.boolean().optional().describe("Only unread (not yet notified) mentions (default: true)"),
|
|
33895
|
+
limit: exports_external.coerce.number().optional().describe("Max results (default: 50)")
|
|
33896
|
+
}
|
|
33897
|
+
}, async (args) => {
|
|
33898
|
+
const results = getMessagesForAgent(args.agent, {
|
|
33899
|
+
space: args.space,
|
|
33900
|
+
unread_only: args.unread_only ?? true,
|
|
33901
|
+
limit: args.limit
|
|
33902
|
+
});
|
|
33903
|
+
return { content: [{ type: "text", text: JSON.stringify({ mentions: results, count: results.length }) }] };
|
|
33904
|
+
});
|
|
33905
|
+
server.registerTool("mark_mentions_read", {
|
|
33906
|
+
description: "Mark @mentions as seen for an agent. Clears unread mention counts.",
|
|
33907
|
+
inputSchema: {
|
|
33908
|
+
agent: exports_external.string().describe("Agent name"),
|
|
33909
|
+
space: exports_external.string().optional().describe("Clear only mentions in this space")
|
|
33910
|
+
}
|
|
33911
|
+
}, async (args) => {
|
|
33912
|
+
const cleared = markMentionsRead(args.agent, args.space);
|
|
33913
|
+
return { content: [{ type: "text", text: JSON.stringify({ cleared }) }] };
|
|
33770
33914
|
});
|
|
33771
33915
|
server.registerTool("send_to_space", {
|
|
33772
33916
|
description: "Post a message to a space.",
|
package/bin/mcp.js
CHANGED
|
@@ -6749,6 +6749,20 @@ function getDb() {
|
|
|
6749
6749
|
if (!presenceColNames.includes("project_id")) {
|
|
6750
6750
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
6751
6751
|
}
|
|
6752
|
+
db.exec(`
|
|
6753
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
6754
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6755
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
6756
|
+
mentioned_agent TEXT NOT NULL,
|
|
6757
|
+
from_agent TEXT NOT NULL,
|
|
6758
|
+
space TEXT,
|
|
6759
|
+
notified_at TEXT,
|
|
6760
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
6761
|
+
)
|
|
6762
|
+
`);
|
|
6763
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
|
|
6764
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
|
|
6765
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
|
|
6752
6766
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
6753
6767
|
if (!ftsExists) {
|
|
6754
6768
|
db.exec(`
|
|
@@ -28839,6 +28853,12 @@ function sendMessage(opts) {
|
|
|
28839
28853
|
db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
|
|
28840
28854
|
message.attachments = attachmentInfos;
|
|
28841
28855
|
}
|
|
28856
|
+
if (opts.space) {
|
|
28857
|
+
const mentions = parseMentions(opts.content);
|
|
28858
|
+
if (mentions.length > 0) {
|
|
28859
|
+
processMentions(message.id, opts.from, opts.space, mentions, db2);
|
|
28860
|
+
}
|
|
28861
|
+
}
|
|
28842
28862
|
fireWebhooks(message);
|
|
28843
28863
|
return message;
|
|
28844
28864
|
}
|
|
@@ -28880,6 +28900,10 @@ function readMessages(opts = {}) {
|
|
|
28880
28900
|
if (opts.threads_only) {
|
|
28881
28901
|
conditions.push("reply_to IS NULL");
|
|
28882
28902
|
}
|
|
28903
|
+
if (opts.mentions_only) {
|
|
28904
|
+
conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
|
|
28905
|
+
params.push(opts.mentions_only.toLowerCase());
|
|
28906
|
+
}
|
|
28883
28907
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
28884
28908
|
const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
28885
28909
|
const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
@@ -29119,6 +29143,14 @@ function searchMessages(opts) {
|
|
|
29119
29143
|
extraWhere += " AND m.to_agent = ?";
|
|
29120
29144
|
ftsParams.push(opts.to);
|
|
29121
29145
|
}
|
|
29146
|
+
if (opts.since) {
|
|
29147
|
+
extraWhere += " AND m.created_at >= ?";
|
|
29148
|
+
ftsParams.push(opts.since);
|
|
29149
|
+
}
|
|
29150
|
+
if (opts.until) {
|
|
29151
|
+
extraWhere += " AND m.created_at <= ?";
|
|
29152
|
+
ftsParams.push(opts.until);
|
|
29153
|
+
}
|
|
29122
29154
|
const orderClause = sortByRelevance ? "ORDER BY rank" : "ORDER BY m.created_at DESC, m.id DESC";
|
|
29123
29155
|
const rows2 = db2.prepare(`SELECT m.*, rank,
|
|
29124
29156
|
snippet(messages_fts, 0, '**', '**', '...', 20) as snippet
|
|
@@ -29151,6 +29183,14 @@ function searchMessages(opts) {
|
|
|
29151
29183
|
conditions.push("to_agent = ?");
|
|
29152
29184
|
params.push(opts.to);
|
|
29153
29185
|
}
|
|
29186
|
+
if (opts.since) {
|
|
29187
|
+
conditions.push("created_at >= ?");
|
|
29188
|
+
params.push(opts.since);
|
|
29189
|
+
}
|
|
29190
|
+
if (opts.until) {
|
|
29191
|
+
conditions.push("created_at <= ?");
|
|
29192
|
+
params.push(opts.until);
|
|
29193
|
+
}
|
|
29154
29194
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
29155
29195
|
const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
|
|
29156
29196
|
return rows.map((row) => {
|
|
@@ -29191,6 +29231,73 @@ function listUnreadCounts(agent) {
|
|
|
29191
29231
|
`).all();
|
|
29192
29232
|
return rows;
|
|
29193
29233
|
}
|
|
29234
|
+
function parseMentions(content) {
|
|
29235
|
+
const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
|
|
29236
|
+
return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
|
|
29237
|
+
}
|
|
29238
|
+
async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
|
|
29239
|
+
const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
|
|
29240
|
+
for (const agent of mentionedAgents) {
|
|
29241
|
+
try {
|
|
29242
|
+
stmt.run(messageId, agent, fromAgent, space);
|
|
29243
|
+
if (agent !== fromAgent.toLowerCase()) {
|
|
29244
|
+
sendMessage({
|
|
29245
|
+
from: fromAgent,
|
|
29246
|
+
to: agent,
|
|
29247
|
+
content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
|
|
29248
|
+
metadata: { type: "mention_notification", source_message_id: messageId, space }
|
|
29249
|
+
});
|
|
29250
|
+
}
|
|
29251
|
+
} catch {}
|
|
29252
|
+
}
|
|
29253
|
+
}
|
|
29254
|
+
function listUnreadCountsWithMentions(agent) {
|
|
29255
|
+
const db2 = getDb();
|
|
29256
|
+
const rows = db2.prepare(`
|
|
29257
|
+
SELECT
|
|
29258
|
+
space,
|
|
29259
|
+
COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
|
|
29260
|
+
(SELECT COUNT(*) FROM message_mentions mm WHERE mm.space = m.space AND mm.mentioned_agent = ? AND mm.notified_at IS NULL) AS mention_count,
|
|
29261
|
+
MAX(created_at) AS latest_message_at
|
|
29262
|
+
FROM messages m
|
|
29263
|
+
WHERE space IN (
|
|
29264
|
+
SELECT DISTINCT space FROM space_members WHERE agent = ?
|
|
29265
|
+
UNION
|
|
29266
|
+
SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
|
|
29267
|
+
)
|
|
29268
|
+
GROUP BY space
|
|
29269
|
+
HAVING COUNT(*) > 0
|
|
29270
|
+
ORDER BY mention_count DESC, unread_count DESC, latest_message_at DESC
|
|
29271
|
+
`).all(agent, agent, agent, agent);
|
|
29272
|
+
return rows;
|
|
29273
|
+
}
|
|
29274
|
+
function getMessagesForAgent(agent, opts) {
|
|
29275
|
+
const db2 = getDb();
|
|
29276
|
+
const conditions = ["mm.mentioned_agent = ?"];
|
|
29277
|
+
const params = [agent.toLowerCase()];
|
|
29278
|
+
if (opts?.space) {
|
|
29279
|
+
conditions.push("m.space = ?");
|
|
29280
|
+
params.push(opts.space);
|
|
29281
|
+
}
|
|
29282
|
+
if (opts?.unread_only) {
|
|
29283
|
+
conditions.push("mm.notified_at IS NULL");
|
|
29284
|
+
}
|
|
29285
|
+
const limit = opts?.limit ?? 50;
|
|
29286
|
+
const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
|
|
29287
|
+
JOIN message_mentions mm ON mm.message_id = m.id
|
|
29288
|
+
WHERE ${conditions.join(" AND ")}
|
|
29289
|
+
ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
|
|
29290
|
+
return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
|
|
29291
|
+
}
|
|
29292
|
+
function markMentionsRead(agent, space) {
|
|
29293
|
+
const db2 = getDb();
|
|
29294
|
+
if (space) {
|
|
29295
|
+
const result2 = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND space = ? AND notified_at IS NULL").run(agent, space);
|
|
29296
|
+
return result2.changes;
|
|
29297
|
+
}
|
|
29298
|
+
const result = db2.prepare("UPDATE message_mentions SET notified_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE mentioned_agent = ? AND notified_at IS NULL").run(agent);
|
|
29299
|
+
return result.changes;
|
|
29300
|
+
}
|
|
29194
29301
|
|
|
29195
29302
|
// src/lib/sessions.ts
|
|
29196
29303
|
init_db();
|
|
@@ -30817,7 +30924,7 @@ function getGraphStats() {
|
|
|
30817
30924
|
// package.json
|
|
30818
30925
|
var package_default = {
|
|
30819
30926
|
name: "@hasna/conversations",
|
|
30820
|
-
version: "0.2.
|
|
30927
|
+
version: "0.2.7",
|
|
30821
30928
|
description: "Real-time CLI messaging for AI agents",
|
|
30822
30929
|
type: "module",
|
|
30823
30930
|
bin: {
|
|
@@ -30951,7 +31058,8 @@ server.registerTool("read_messages", {
|
|
|
30951
31058
|
mark_read: exports_external.coerce.boolean().optional(),
|
|
30952
31059
|
max_content_length: exports_external.coerce.number().optional().describe("Truncate each message content to N chars (adds truncated:true flag)"),
|
|
30953
31060
|
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)")
|
|
31061
|
+
include_reply_counts: exports_external.coerce.boolean().optional().describe("Include reply_count on each message (adds one extra query)"),
|
|
31062
|
+
mentions_only: exports_external.string().optional().describe("Only return messages that @mention this agent")
|
|
30955
31063
|
}
|
|
30956
31064
|
}, async (args) => {
|
|
30957
31065
|
const agent = resolveIdentity(args.from);
|
|
@@ -31034,19 +31142,26 @@ server.registerTool("mark_read", {
|
|
|
31034
31142
|
};
|
|
31035
31143
|
});
|
|
31036
31144
|
server.registerTool("search_messages", {
|
|
31037
|
-
description: "Full-text search across messages.",
|
|
31145
|
+
description: "Full-text search across messages. Uses FTS5 with BM25 ranking if available, falls back to LIKE. Returns messages with snippet and relevance_score.",
|
|
31038
31146
|
inputSchema: {
|
|
31039
|
-
query: exports_external.string(),
|
|
31040
|
-
space: exports_external.string().optional(),
|
|
31041
|
-
from: exports_external.string().optional(),
|
|
31042
|
-
to: exports_external.string().optional(),
|
|
31043
|
-
|
|
31147
|
+
query: exports_external.string().describe(`Search query. Wrap in quotes for exact phrase: '"BUG-005"'`),
|
|
31148
|
+
space: exports_external.string().optional().describe("Limit to a specific space"),
|
|
31149
|
+
from: exports_external.string().optional().describe("Filter by sender"),
|
|
31150
|
+
to: exports_external.string().optional().describe("Filter by recipient"),
|
|
31151
|
+
since: exports_external.string().optional().describe("ISO 8601 date \u2014 only messages after this"),
|
|
31152
|
+
until: exports_external.string().optional().describe("ISO 8601 date \u2014 only messages before this"),
|
|
31153
|
+
sort: exports_external.enum(["relevance", "recent"]).optional().describe("Sort order (default: relevance)"),
|
|
31154
|
+
limit: exports_external.coerce.number().optional().describe("Max results (default: 20)")
|
|
31044
31155
|
}
|
|
31045
31156
|
}, async (args) => {
|
|
31046
|
-
const { query, space, from, to, limit } = args;
|
|
31047
|
-
const
|
|
31157
|
+
const { query, space, from, to, since, until, sort, limit } = args;
|
|
31158
|
+
const results = searchMessages({ query, space, from, to, since, until, sort, limit });
|
|
31048
31159
|
return {
|
|
31049
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
31160
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
31161
|
+
results,
|
|
31162
|
+
count: results.length,
|
|
31163
|
+
query
|
|
31164
|
+
}) }]
|
|
31050
31165
|
};
|
|
31051
31166
|
});
|
|
31052
31167
|
server.registerTool("export_messages", {
|
|
@@ -31123,13 +31238,42 @@ server.registerTool("list_spaces", {
|
|
|
31123
31238
|
server.registerTool("list_unread_counts", {
|
|
31124
31239
|
description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
|
|
31125
31240
|
inputSchema: {
|
|
31126
|
-
agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts.")
|
|
31241
|
+
agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts."),
|
|
31242
|
+
include_mentions: exports_external.coerce.boolean().optional().describe("Include mention_count per space (requires agent)")
|
|
31127
31243
|
}
|
|
31128
31244
|
}, async (args) => {
|
|
31245
|
+
if (args.agent && args.include_mentions) {
|
|
31246
|
+
const counts2 = listUnreadCountsWithMentions(args.agent);
|
|
31247
|
+
return { content: [{ type: "text", text: JSON.stringify(counts2) }] };
|
|
31248
|
+
}
|
|
31129
31249
|
const counts = listUnreadCounts(args.agent);
|
|
31130
|
-
return {
|
|
31131
|
-
|
|
31132
|
-
|
|
31250
|
+
return { content: [{ type: "text", text: JSON.stringify(counts) }] };
|
|
31251
|
+
});
|
|
31252
|
+
server.registerTool("get_mentions", {
|
|
31253
|
+
description: "Get messages that @mention a specific agent. Useful for catching up on missed pings.",
|
|
31254
|
+
inputSchema: {
|
|
31255
|
+
agent: exports_external.string().describe("Agent name to find mentions for"),
|
|
31256
|
+
space: exports_external.string().optional().describe("Filter to a specific space"),
|
|
31257
|
+
unread_only: exports_external.coerce.boolean().optional().describe("Only unread (not yet notified) mentions (default: true)"),
|
|
31258
|
+
limit: exports_external.coerce.number().optional().describe("Max results (default: 50)")
|
|
31259
|
+
}
|
|
31260
|
+
}, async (args) => {
|
|
31261
|
+
const results = getMessagesForAgent(args.agent, {
|
|
31262
|
+
space: args.space,
|
|
31263
|
+
unread_only: args.unread_only ?? true,
|
|
31264
|
+
limit: args.limit
|
|
31265
|
+
});
|
|
31266
|
+
return { content: [{ type: "text", text: JSON.stringify({ mentions: results, count: results.length }) }] };
|
|
31267
|
+
});
|
|
31268
|
+
server.registerTool("mark_mentions_read", {
|
|
31269
|
+
description: "Mark @mentions as seen for an agent. Clears unread mention counts.",
|
|
31270
|
+
inputSchema: {
|
|
31271
|
+
agent: exports_external.string().describe("Agent name"),
|
|
31272
|
+
space: exports_external.string().optional().describe("Clear only mentions in this space")
|
|
31273
|
+
}
|
|
31274
|
+
}, async (args) => {
|
|
31275
|
+
const cleared = markMentionsRead(args.agent, args.space);
|
|
31276
|
+
return { content: [{ type: "text", text: JSON.stringify({ cleared }) }] };
|
|
31133
31277
|
});
|
|
31134
31278
|
server.registerTool("send_to_space", {
|
|
31135
31279
|
description: "Post a message to a space.",
|
package/dist/index.js
CHANGED
|
@@ -276,6 +276,20 @@ function getDb() {
|
|
|
276
276
|
if (!presenceColNames.includes("project_id")) {
|
|
277
277
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
278
278
|
}
|
|
279
|
+
db.exec(`
|
|
280
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
281
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
282
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
283
|
+
mentioned_agent TEXT NOT NULL,
|
|
284
|
+
from_agent TEXT NOT NULL,
|
|
285
|
+
space TEXT,
|
|
286
|
+
notified_at TEXT,
|
|
287
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
288
|
+
)
|
|
289
|
+
`);
|
|
290
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent)");
|
|
291
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id)");
|
|
292
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at)");
|
|
279
293
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
280
294
|
if (!ftsExists) {
|
|
281
295
|
db.exec(`
|
|
@@ -2304,6 +2318,12 @@ function sendMessage(opts) {
|
|
|
2304
2318
|
db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
|
|
2305
2319
|
message.attachments = attachmentInfos;
|
|
2306
2320
|
}
|
|
2321
|
+
if (opts.space) {
|
|
2322
|
+
const mentions = parseMentions(opts.content);
|
|
2323
|
+
if (mentions.length > 0) {
|
|
2324
|
+
processMentions(message.id, opts.from, opts.space, mentions, db2);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2307
2327
|
fireWebhooks(message);
|
|
2308
2328
|
return message;
|
|
2309
2329
|
}
|
|
@@ -2345,6 +2365,10 @@ function readMessages(opts = {}) {
|
|
|
2345
2365
|
if (opts.threads_only) {
|
|
2346
2366
|
conditions.push("reply_to IS NULL");
|
|
2347
2367
|
}
|
|
2368
|
+
if (opts.mentions_only) {
|
|
2369
|
+
conditions.push(`id IN (SELECT message_id FROM message_mentions WHERE mentioned_agent = ?)`);
|
|
2370
|
+
params.push(opts.mentions_only.toLowerCase());
|
|
2371
|
+
}
|
|
2348
2372
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2349
2373
|
const resolvedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
2350
2374
|
const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
@@ -2544,6 +2568,14 @@ function searchMessages(opts) {
|
|
|
2544
2568
|
extraWhere += " AND m.to_agent = ?";
|
|
2545
2569
|
ftsParams.push(opts.to);
|
|
2546
2570
|
}
|
|
2571
|
+
if (opts.since) {
|
|
2572
|
+
extraWhere += " AND m.created_at >= ?";
|
|
2573
|
+
ftsParams.push(opts.since);
|
|
2574
|
+
}
|
|
2575
|
+
if (opts.until) {
|
|
2576
|
+
extraWhere += " AND m.created_at <= ?";
|
|
2577
|
+
ftsParams.push(opts.until);
|
|
2578
|
+
}
|
|
2547
2579
|
const orderClause = sortByRelevance ? "ORDER BY rank" : "ORDER BY m.created_at DESC, m.id DESC";
|
|
2548
2580
|
const rows2 = db2.prepare(`SELECT m.*, rank,
|
|
2549
2581
|
snippet(messages_fts, 0, '**', '**', '...', 20) as snippet
|
|
@@ -2576,6 +2608,14 @@ function searchMessages(opts) {
|
|
|
2576
2608
|
conditions.push("to_agent = ?");
|
|
2577
2609
|
params.push(opts.to);
|
|
2578
2610
|
}
|
|
2611
|
+
if (opts.since) {
|
|
2612
|
+
conditions.push("created_at >= ?");
|
|
2613
|
+
params.push(opts.since);
|
|
2614
|
+
}
|
|
2615
|
+
if (opts.until) {
|
|
2616
|
+
conditions.push("created_at <= ?");
|
|
2617
|
+
params.push(opts.until);
|
|
2618
|
+
}
|
|
2579
2619
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
2580
2620
|
const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
|
|
2581
2621
|
return rows.map((row) => {
|
|
@@ -2583,6 +2623,26 @@ function searchMessages(opts) {
|
|
|
2583
2623
|
return { ...msg, snippet: null, relevance_score: 0 };
|
|
2584
2624
|
});
|
|
2585
2625
|
}
|
|
2626
|
+
function parseMentions(content) {
|
|
2627
|
+
const matches = content.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
|
|
2628
|
+
return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
|
|
2629
|
+
}
|
|
2630
|
+
async function processMentions(messageId, fromAgent, space, mentionedAgents, db2) {
|
|
2631
|
+
const stmt = db2.prepare("INSERT INTO message_mentions (message_id, mentioned_agent, from_agent, space) VALUES (?, ?, ?, ?)");
|
|
2632
|
+
for (const agent of mentionedAgents) {
|
|
2633
|
+
try {
|
|
2634
|
+
stmt.run(messageId, agent, fromAgent, space);
|
|
2635
|
+
if (agent !== fromAgent.toLowerCase()) {
|
|
2636
|
+
sendMessage({
|
|
2637
|
+
from: fromAgent,
|
|
2638
|
+
to: agent,
|
|
2639
|
+
content: `You were mentioned in #${space} by ${fromAgent} (message #${messageId})`,
|
|
2640
|
+
metadata: { type: "mention_notification", source_message_id: messageId, space }
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
} catch {}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2586
2646
|
// src/lib/sessions.ts
|
|
2587
2647
|
init_db();
|
|
2588
2648
|
function listSessions(agent) {
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -67,3 +67,24 @@ export interface UnreadCount {
|
|
|
67
67
|
* If agent is omitted, returns counts for all spaces.
|
|
68
68
|
*/
|
|
69
69
|
export declare function listUnreadCounts(agent?: string): UnreadCount[];
|
|
70
|
+
/** Extract @agentname mentions from message content. Returns unique agent names (lowercase). */
|
|
71
|
+
export declare function parseMentions(content: string): string[];
|
|
72
|
+
export interface MentionCount {
|
|
73
|
+
space: string;
|
|
74
|
+
unread_count: number;
|
|
75
|
+
mention_count: number;
|
|
76
|
+
latest_message_at: string | null;
|
|
77
|
+
}
|
|
78
|
+
/** Get unread counts AND mention counts per space for an agent. */
|
|
79
|
+
export declare function listUnreadCountsWithMentions(agent: string): MentionCount[];
|
|
80
|
+
/** Get messages that mention a specific agent. */
|
|
81
|
+
export declare function getMessagesForAgent(agent: string, opts?: {
|
|
82
|
+
space?: string;
|
|
83
|
+
unread_only?: boolean;
|
|
84
|
+
limit?: number;
|
|
85
|
+
}): Array<{
|
|
86
|
+
message: Message;
|
|
87
|
+
mention_id: number;
|
|
88
|
+
}>;
|
|
89
|
+
/** Mark mentions as notified (agent has seen them). */
|
|
90
|
+
export declare function markMentionsRead(agent: string, space?: string): number;
|
package/dist/types.d.ts
CHANGED
|
@@ -111,14 +111,18 @@ export interface ReadMessagesOptions {
|
|
|
111
111
|
max_content_length?: number;
|
|
112
112
|
threads_only?: boolean;
|
|
113
113
|
include_reply_counts?: boolean;
|
|
114
|
+
mentions_only?: string;
|
|
114
115
|
}
|
|
115
116
|
export interface SearchMessagesOptions {
|
|
116
117
|
query: string;
|
|
117
118
|
space?: string;
|
|
118
119
|
from?: string;
|
|
119
120
|
to?: string;
|
|
121
|
+
since?: string;
|
|
122
|
+
until?: string;
|
|
120
123
|
limit?: number;
|
|
121
124
|
sort?: "relevance" | "recent";
|
|
125
|
+
snippet_length?: number;
|
|
122
126
|
}
|
|
123
127
|
export interface SearchResult extends Message {
|
|
124
128
|
snippet: string | null;
|