@hasna/conversations 0.1.33 → 0.2.1

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
@@ -6302,7 +6302,7 @@ var require_formats = __commonJS((exports) => {
6302
6302
  }
6303
6303
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6304
6304
  function getTime(strictTimeZone) {
6305
- return function time(str) {
6305
+ return function time3(str) {
6306
6306
  const matches = TIME.exec(str);
6307
6307
  if (!matches)
6308
6308
  return false;
@@ -27310,6 +27310,62 @@ class ExperimentalServerTasks {
27310
27310
  requestStream(request, resultSchema, options) {
27311
27311
  return this._server.requestStream(request, resultSchema, options);
27312
27312
  }
27313
+ createMessageStream(params, options) {
27314
+ const clientCapabilities = this._server.getClientCapabilities();
27315
+ if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
27316
+ throw new Error("Client does not support sampling tools capability.");
27317
+ }
27318
+ if (params.messages.length > 0) {
27319
+ const lastMessage = params.messages[params.messages.length - 1];
27320
+ const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
27321
+ const hasToolResults = lastContent.some((c) => c.type === "tool_result");
27322
+ const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
27323
+ const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
27324
+ const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
27325
+ if (hasToolResults) {
27326
+ if (lastContent.some((c) => c.type !== "tool_result")) {
27327
+ throw new Error("The last message must contain only tool_result content if any is present");
27328
+ }
27329
+ if (!hasPreviousToolUse) {
27330
+ throw new Error("tool_result blocks are not matching any tool_use from the previous message");
27331
+ }
27332
+ }
27333
+ if (hasPreviousToolUse) {
27334
+ const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
27335
+ const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
27336
+ if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
27337
+ throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
27338
+ }
27339
+ }
27340
+ }
27341
+ return this.requestStream({
27342
+ method: "sampling/createMessage",
27343
+ params
27344
+ }, CreateMessageResultSchema, options);
27345
+ }
27346
+ elicitInputStream(params, options) {
27347
+ const clientCapabilities = this._server.getClientCapabilities();
27348
+ const mode = params.mode ?? "form";
27349
+ switch (mode) {
27350
+ case "url": {
27351
+ if (!clientCapabilities?.elicitation?.url) {
27352
+ throw new Error("Client does not support url elicitation.");
27353
+ }
27354
+ break;
27355
+ }
27356
+ case "form": {
27357
+ if (!clientCapabilities?.elicitation?.form) {
27358
+ throw new Error("Client does not support form elicitation.");
27359
+ }
27360
+ break;
27361
+ }
27362
+ }
27363
+ const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
27364
+ return this.requestStream({
27365
+ method: "elicitation/create",
27366
+ params: normalizedParams
27367
+ }, ElicitResultSchema, options);
27368
+ }
27313
27369
  async getTask(taskId, options) {
27314
27370
  return this._server.getTask({ taskId }, options);
27315
27371
  }
@@ -28947,12 +29003,18 @@ function getThreadReplies(messageId) {
28947
29003
  function searchMessages(opts) {
28948
29004
  const db2 = getDb();
28949
29005
  const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
29006
+ const sortByRelevance = opts.sort !== "recent";
29007
+ const priorityWeights = { urgent: 10, high: 5, normal: 1, low: 0.5 };
28950
29008
  try {
28951
- const ftsConditions = [];
28952
29009
  const ftsParams = [];
28953
- const words = opts.query.trim().split(/\s+/).filter(Boolean);
28954
- const ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
28955
- ftsConditions.push("messages_fts MATCH ?");
29010
+ const query = opts.query.trim();
29011
+ let ftsQuery;
29012
+ if (query.startsWith('"') && query.endsWith('"')) {
29013
+ ftsQuery = query;
29014
+ } else {
29015
+ const words = query.split(/\s+/).filter(Boolean);
29016
+ ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
29017
+ }
28956
29018
  ftsParams.push(ftsQuery);
28957
29019
  let extraWhere = "";
28958
29020
  if (opts.space) {
@@ -28967,11 +29029,23 @@ function searchMessages(opts) {
28967
29029
  extraWhere += " AND m.to_agent = ?";
28968
29030
  ftsParams.push(opts.to);
28969
29031
  }
28970
- const rows2 = db2.prepare(`SELECT m.* FROM messages m
29032
+ const orderClause = sortByRelevance ? "ORDER BY rank" : "ORDER BY m.created_at DESC, m.id DESC";
29033
+ const rows2 = db2.prepare(`SELECT m.*, rank,
29034
+ snippet(messages_fts, 0, '**', '**', '...', 20) as snippet
29035
+ FROM messages m
28971
29036
  JOIN messages_fts ON messages_fts.rowid = m.id
28972
- WHERE ${ftsConditions.join(" AND ")}${extraWhere}
28973
- ORDER BY m.created_at DESC, m.id DESC LIMIT ${limit}`).all(...ftsParams);
28974
- return rows2.map(parseMessage);
29037
+ WHERE messages_fts MATCH ?${extraWhere}
29038
+ ${orderClause} LIMIT ${limit}`).all(...ftsParams);
29039
+ const maxRank = rows2.reduce((max, r) => Math.max(max, Math.abs(r.rank || 0)), 0) || 1;
29040
+ return rows2.map((row) => {
29041
+ const msg = parseMessage(row);
29042
+ const ftsScore = maxRank > 0 ? Math.abs(row.rank || 0) / maxRank * 100 : 50;
29043
+ const priorityBoost = priorityWeights[msg.priority] || 1;
29044
+ const pinnedBoost = msg.pinned_at ? 20 : 0;
29045
+ const blockingBoost = msg.blocking ? 15 : 0;
29046
+ const relevance_score = Math.round((ftsScore * priorityBoost + pinnedBoost + blockingBoost) * 100) / 100;
29047
+ return { ...msg, snippet: row.snippet || null, relevance_score };
29048
+ });
28975
29049
  } catch {}
28976
29050
  const conditions = ["content LIKE ?"];
28977
29051
  const params = [`%${opts.query}%`];
@@ -28989,7 +29063,10 @@ function searchMessages(opts) {
28989
29063
  }
28990
29064
  const where = `WHERE ${conditions.join(" AND ")}`;
28991
29065
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
28992
- return rows.map(parseMessage);
29066
+ return rows.map((row) => {
29067
+ const msg = parseMessage(row);
29068
+ return { ...msg, snippet: null, relevance_score: 0 };
29069
+ });
28993
29070
  }
28994
29071
 
28995
29072
  // src/lib/sessions.ts
@@ -29022,6 +29099,32 @@ function listSessions(agent) {
29022
29099
  };
29023
29100
  });
29024
29101
  }
29102
+ function getSessionActivity(sessionId) {
29103
+ const db2 = getDb();
29104
+ const exists = db2.prepare("SELECT 1 FROM messages WHERE session_id = ? LIMIT 1").get(sessionId);
29105
+ if (!exists)
29106
+ return null;
29107
+ const msgsLast1h = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-1 hour')").get(sessionId).c;
29108
+ const msgsLast24h = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-24 hours')").get(sessionId).c;
29109
+ const uniqueAgents = db2.prepare("SELECT COUNT(DISTINCT from_agent) as c FROM messages WHERE session_id = ?").get(sessionId).c;
29110
+ const totalMsgs = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ?").get(sessionId).c;
29111
+ const replyCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND reply_to IS NOT NULL").get(sessionId).c;
29112
+ const replyRatio = totalMsgs > 0 ? Math.round(replyCount / totalMsgs * 100) / 100 : 0;
29113
+ const priorityRow = db2.prepare("SELECT priority, COUNT(*) as c FROM messages WHERE session_id = ? GROUP BY priority ORDER BY c DESC LIMIT 1").get(sessionId);
29114
+ const reactionCount = db2.prepare("SELECT COUNT(*) as c FROM reactions r JOIN messages m ON r.message_id = m.id WHERE m.session_id = ?").get(sessionId).c;
29115
+ const agentsLast1h = db2.prepare("SELECT COUNT(DISTINCT from_agent) as c FROM messages WHERE session_id = ? AND created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-1 hour')").get(sessionId).c;
29116
+ const isTrending = msgsLast1h >= 5 || agentsLast1h >= 3;
29117
+ return {
29118
+ session_id: sessionId,
29119
+ msgs_last_1h: msgsLast1h,
29120
+ msgs_last_24h: msgsLast24h,
29121
+ unique_agents: uniqueAgents,
29122
+ reply_ratio: replyRatio,
29123
+ avg_priority: priorityRow?.priority ?? "normal",
29124
+ reaction_count: reactionCount,
29125
+ is_trending: isTrending
29126
+ };
29127
+ }
29025
29128
 
29026
29129
  // src/lib/spaces.ts
29027
29130
  init_db();
@@ -30018,10 +30121,491 @@ function listLocks(opts) {
30018
30121
  query += " ORDER BY locked_at ASC";
30019
30122
  return db2.prepare(query).all(...params);
30020
30123
  }
30124
+
30125
+ // src/lib/hot.ts
30126
+ init_db();
30127
+ function computeHotness(sessionId) {
30128
+ const db2 = getDb();
30129
+ const base = db2.prepare(`
30130
+ SELECT session_id,
30131
+ GROUP_CONCAT(DISTINCT from_agent) as agents,
30132
+ MAX(space) as space,
30133
+ MAX(created_at) as last_message_at,
30134
+ COUNT(*) as message_count
30135
+ FROM messages WHERE session_id = ?
30136
+ GROUP BY session_id
30137
+ `).get(sessionId);
30138
+ if (!base)
30139
+ return null;
30140
+ const msgsLast1h = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-1 hour')").get(sessionId).c;
30141
+ const msgsLast24h = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-24 hours')").get(sessionId).c;
30142
+ const uniqueAgents = db2.prepare("SELECT COUNT(DISTINCT from_agent) as c FROM messages WHERE session_id = ?").get(sessionId).c;
30143
+ const reactionCount = db2.prepare("SELECT COUNT(*) as c FROM reactions r JOIN messages m ON r.message_id = m.id WHERE m.session_id = ?").get(sessionId).c;
30144
+ const replyCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND reply_to IS NOT NULL").get(sessionId).c;
30145
+ const highPriorityCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND priority IN ('high', 'urgent')").get(sessionId).c;
30146
+ const blockerCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND blocking = 1").get(sessionId).c;
30147
+ const lastMsgMs = new Date(base.last_message_at + "Z").getTime();
30148
+ const hoursSinceLast = Math.max(0, (Date.now() - lastMsgMs) / 3600000);
30149
+ const hotness_score = Math.round(msgsLast1h * 3 + uniqueAgents * 5 + reactionCount * 2 + replyCount * 4 + highPriorityCount * 10 + blockerCount * 20 - hoursSinceLast * 2);
30150
+ return {
30151
+ session_id: base.session_id,
30152
+ participants: base.agents.split(","),
30153
+ space: base.space,
30154
+ last_message_at: base.last_message_at,
30155
+ message_count: base.message_count,
30156
+ hotness_score,
30157
+ metrics: {
30158
+ msgs_last_1h: msgsLast1h,
30159
+ msgs_last_24h: msgsLast24h,
30160
+ unique_agents: uniqueAgents,
30161
+ reaction_count: reactionCount,
30162
+ reply_count: replyCount,
30163
+ high_priority_count: highPriorityCount,
30164
+ blocker_count: blockerCount,
30165
+ hours_since_last: Math.round(hoursSinceLast * 10) / 10
30166
+ }
30167
+ };
30168
+ }
30169
+ function listHotSessions(opts) {
30170
+ const db2 = getDb();
30171
+ const limit = opts?.limit ?? 20;
30172
+ const minScore = opts?.min_score ?? 0;
30173
+ let where = "";
30174
+ const params = [];
30175
+ if (opts?.space) {
30176
+ where = " WHERE space = ?";
30177
+ params.push(opts.space);
30178
+ } else if (opts?.project_id) {
30179
+ where = " WHERE project_id = ?";
30180
+ params.push(opts.project_id);
30181
+ }
30182
+ const sessions = db2.prepare(`SELECT session_id, MAX(created_at) as last_at FROM messages${where} GROUP BY session_id ORDER BY last_at DESC LIMIT 100`).all(...params);
30183
+ const hotSessions = [];
30184
+ for (const { session_id } of sessions) {
30185
+ const hot = computeHotness(session_id);
30186
+ if (hot && hot.hotness_score >= minScore) {
30187
+ hotSessions.push(hot);
30188
+ }
30189
+ }
30190
+ hotSessions.sort((a, b) => b.hotness_score - a.hotness_score);
30191
+ return hotSessions.slice(0, limit);
30192
+ }
30193
+
30194
+ // src/lib/topics.ts
30195
+ init_db();
30196
+ var STOPWORDS = new Set([
30197
+ "a",
30198
+ "an",
30199
+ "the",
30200
+ "and",
30201
+ "or",
30202
+ "but",
30203
+ "in",
30204
+ "on",
30205
+ "at",
30206
+ "to",
30207
+ "for",
30208
+ "of",
30209
+ "with",
30210
+ "by",
30211
+ "from",
30212
+ "is",
30213
+ "it",
30214
+ "this",
30215
+ "that",
30216
+ "are",
30217
+ "was",
30218
+ "were",
30219
+ "be",
30220
+ "been",
30221
+ "being",
30222
+ "have",
30223
+ "has",
30224
+ "had",
30225
+ "do",
30226
+ "does",
30227
+ "did",
30228
+ "will",
30229
+ "would",
30230
+ "could",
30231
+ "should",
30232
+ "may",
30233
+ "might",
30234
+ "shall",
30235
+ "can",
30236
+ "need",
30237
+ "not",
30238
+ "no",
30239
+ "so",
30240
+ "if",
30241
+ "then",
30242
+ "than",
30243
+ "too",
30244
+ "very",
30245
+ "just",
30246
+ "about",
30247
+ "up",
30248
+ "out",
30249
+ "all",
30250
+ "also",
30251
+ "as",
30252
+ "into",
30253
+ "only",
30254
+ "other",
30255
+ "each",
30256
+ "every",
30257
+ "both",
30258
+ "few",
30259
+ "more",
30260
+ "most",
30261
+ "some",
30262
+ "such",
30263
+ "any",
30264
+ "over",
30265
+ "after",
30266
+ "before",
30267
+ "between",
30268
+ "under",
30269
+ "above",
30270
+ "here",
30271
+ "there",
30272
+ "when",
30273
+ "where",
30274
+ "how",
30275
+ "what",
30276
+ "which",
30277
+ "who",
30278
+ "whom",
30279
+ "why",
30280
+ "its",
30281
+ "my",
30282
+ "your",
30283
+ "his",
30284
+ "her",
30285
+ "our",
30286
+ "their",
30287
+ "we",
30288
+ "you",
30289
+ "he",
30290
+ "she",
30291
+ "they",
30292
+ "i",
30293
+ "me",
30294
+ "him",
30295
+ "us",
30296
+ "them",
30297
+ "now",
30298
+ "new",
30299
+ "get",
30300
+ "got",
30301
+ "go",
30302
+ "going",
30303
+ "done",
30304
+ "make",
30305
+ "made",
30306
+ "see",
30307
+ "know",
30308
+ "think",
30309
+ "want",
30310
+ "one",
30311
+ "two",
30312
+ "like",
30313
+ "still",
30314
+ "back",
30315
+ "even"
30316
+ ]);
30317
+ function extractTopics(text, topN = 10) {
30318
+ const cleaned = text.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]+`/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[#*_~|>\[\](){}]/g, " ").replace(/\d+/g, " ").toLowerCase();
30319
+ const words = cleaned.split(/\s+/).filter((w) => w.length >= 3 && !STOPWORDS.has(w) && /^[a-z]/.test(w));
30320
+ const freq = new Map;
30321
+ for (const w of words) {
30322
+ freq.set(w, (freq.get(w) || 0) + 1);
30323
+ }
30324
+ const totalWords = words.length || 1;
30325
+ return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN).map(([topic, count]) => ({
30326
+ topic,
30327
+ weight: Math.round(count / totalWords * 1000) / 1000,
30328
+ count
30329
+ }));
30330
+ }
30331
+ function getSpaceTopics(spaceName, opts) {
30332
+ const db2 = getDb();
30333
+ const limit = opts?.limit ?? 100;
30334
+ const sinceClause = opts?.since ? "AND created_at > ?" : "";
30335
+ const params = [spaceName];
30336
+ if (opts?.since)
30337
+ params.push(opts.since);
30338
+ const rows = db2.prepare(`SELECT content FROM messages WHERE space = ? ${sinceClause} ORDER BY created_at DESC LIMIT ${limit}`).all(...params);
30339
+ const combined = rows.map((r) => r.content).join(`
30340
+ `);
30341
+ return extractTopics(combined, 15);
30342
+ }
30343
+ function getSessionTopics(sessionId, opts) {
30344
+ const db2 = getDb();
30345
+ const limit = opts?.limit ?? 100;
30346
+ const rows = db2.prepare(`SELECT content FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ${limit}`).all(sessionId);
30347
+ const combined = rows.map((r) => r.content).join(`
30348
+ `);
30349
+ return extractTopics(combined, 15);
30350
+ }
30351
+ function getTrendingTopics(opts) {
30352
+ const db2 = getDb();
30353
+ const hours = opts?.hours ?? 24;
30354
+ const topN = opts?.top_n ?? 20;
30355
+ let where = `WHERE created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${hours} hours')`;
30356
+ const params = [];
30357
+ if (opts?.project_id) {
30358
+ where += " AND project_id = ?";
30359
+ params.push(opts.project_id);
30360
+ }
30361
+ const rows = db2.prepare(`SELECT content FROM messages ${where} ORDER BY created_at DESC LIMIT 500`).all(...params);
30362
+ const combined = rows.map((r) => r.content).join(`
30363
+ `);
30364
+ return extractTopics(combined, topN);
30365
+ }
30366
+
30367
+ // src/lib/summary.ts
30368
+ init_db();
30369
+ function getConversationSummary(sessionOrSpace, opts) {
30370
+ const db2 = getDb();
30371
+ const limit = opts?.limit ?? 50;
30372
+ const isSpace = sessionOrSpace.startsWith("space:") || db2.prepare("SELECT 1 FROM spaces WHERE name = ?").get(sessionOrSpace);
30373
+ const filterCol = isSpace ? "space" : "session_id";
30374
+ const filterVal = isSpace && !sessionOrSpace.startsWith("space:") ? sessionOrSpace : sessionOrSpace;
30375
+ const messages = db2.prepare(`SELECT * FROM messages WHERE ${filterCol} = ? ORDER BY created_at DESC LIMIT ${limit}`).all(filterVal);
30376
+ if (messages.length === 0)
30377
+ return null;
30378
+ const agents = new Set;
30379
+ for (const m of messages) {
30380
+ agents.add(m.from_agent);
30381
+ if (m.to_agent)
30382
+ agents.add(m.to_agent);
30383
+ }
30384
+ const dates = messages.map((m) => m.created_at).sort();
30385
+ const dateRange = { first: dates[0], last: dates[dates.length - 1] };
30386
+ const allContent = messages.map((m) => m.content).join(`
30387
+ `);
30388
+ const topics = extractTopics(allContent, 10);
30389
+ const keyMessages = [];
30390
+ for (const m of messages) {
30391
+ const priority = m.priority;
30392
+ if (priority === "high" || priority === "urgent") {
30393
+ keyMessages.push({
30394
+ id: m.id,
30395
+ from: m.from_agent,
30396
+ content: m.content.slice(0, 200),
30397
+ reason: `${priority} priority`
30398
+ });
30399
+ }
30400
+ if (m.blocking) {
30401
+ keyMessages.push({
30402
+ id: m.id,
30403
+ from: m.from_agent,
30404
+ content: m.content.slice(0, 200),
30405
+ reason: "blocking message"
30406
+ });
30407
+ }
30408
+ }
30409
+ for (const m of messages) {
30410
+ if (m.pinned_at) {
30411
+ keyMessages.push({
30412
+ id: m.id,
30413
+ from: m.from_agent,
30414
+ content: m.content.slice(0, 200),
30415
+ reason: "pinned"
30416
+ });
30417
+ }
30418
+ }
30419
+ const msgIds = messages.map((m) => m.id);
30420
+ if (msgIds.length > 0) {
30421
+ const placeholders = msgIds.map(() => "?").join(",");
30422
+ const reacted = db2.prepare(`SELECT message_id, COUNT(*) as c FROM reactions WHERE message_id IN (${placeholders}) GROUP BY message_id ORDER BY c DESC LIMIT 3`).all(...msgIds);
30423
+ for (const r of reacted) {
30424
+ const m = messages.find((msg) => msg.id === r.message_id);
30425
+ if (m) {
30426
+ keyMessages.push({
30427
+ id: r.message_id,
30428
+ from: m.from_agent,
30429
+ content: m.content.slice(0, 200),
30430
+ reason: `${r.c} reaction(s)`
30431
+ });
30432
+ }
30433
+ }
30434
+ }
30435
+ const seen = new Set;
30436
+ const uniqueKey = keyMessages.filter((k) => {
30437
+ if (seen.has(k.id))
30438
+ return false;
30439
+ seen.add(k.id);
30440
+ return true;
30441
+ }).slice(0, 10);
30442
+ const blockers = messages.filter((m) => m.blocking && !m.read_at).map((m) => ({
30443
+ id: m.id,
30444
+ from: m.from_agent,
30445
+ content: m.content.slice(0, 200),
30446
+ created_at: m.created_at
30447
+ }));
30448
+ const replyCount = messages.filter((m) => m.reply_to).length;
30449
+ const reactionCount = msgIds.length > 0 ? db2.prepare(`SELECT COUNT(*) as c FROM reactions WHERE message_id IN (${msgIds.map(() => "?").join(",")})`).get(...msgIds).c : 0;
30450
+ const priorityCounts = {};
30451
+ for (const m of messages) {
30452
+ const p = m.priority;
30453
+ priorityCounts[p] = (priorityCounts[p] || 0) + 1;
30454
+ }
30455
+ const avgPriority = Object.entries(priorityCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "normal";
30456
+ return {
30457
+ session_id: sessionOrSpace,
30458
+ participants: [...agents].filter((a) => a !== sessionOrSpace),
30459
+ message_count: messages.length,
30460
+ date_range: dateRange,
30461
+ topics,
30462
+ key_messages: uniqueKey,
30463
+ unresolved_blockers: blockers,
30464
+ activity: {
30465
+ reply_count: replyCount,
30466
+ reaction_count: reactionCount,
30467
+ avg_priority: avgPriority
30468
+ }
30469
+ };
30470
+ }
30471
+
30472
+ // src/lib/graph.ts
30473
+ init_db();
30474
+ function ensureGraphTable() {
30475
+ const db2 = getDb();
30476
+ db2.exec(`
30477
+ CREATE TABLE IF NOT EXISTS graph_edges (
30478
+ from_type TEXT NOT NULL,
30479
+ from_id TEXT NOT NULL,
30480
+ to_type TEXT NOT NULL,
30481
+ to_id TEXT NOT NULL,
30482
+ relation TEXT NOT NULL,
30483
+ weight REAL NOT NULL DEFAULT 1,
30484
+ metadata TEXT,
30485
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
30486
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
30487
+ UNIQUE(from_type, from_id, to_type, to_id, relation)
30488
+ )
30489
+ `);
30490
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_from ON graph_edges(from_type, from_id)");
30491
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_to ON graph_edges(to_type, to_id)");
30492
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_relation ON graph_edges(relation)");
30493
+ }
30494
+ function buildGraph() {
30495
+ const db2 = getDb();
30496
+ ensureGraphTable();
30497
+ let created = 0;
30498
+ let updated = 0;
30499
+ const upsert = db2.prepare(`
30500
+ INSERT INTO graph_edges (from_type, from_id, to_type, to_id, relation, weight, updated_at)
30501
+ VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))
30502
+ ON CONFLICT(from_type, from_id, to_type, to_id, relation) DO UPDATE SET
30503
+ weight = excluded.weight,
30504
+ updated_at = excluded.updated_at
30505
+ `);
30506
+ const insertOrUpdate = db2.transaction(() => {
30507
+ const dmPairs = db2.prepare(`
30508
+ SELECT from_agent, to_agent, COUNT(*) as cnt, MAX(created_at) as last_at
30509
+ FROM messages WHERE space IS NULL AND from_agent != to_agent
30510
+ GROUP BY from_agent, to_agent
30511
+ `).all();
30512
+ for (const pair of dmPairs) {
30513
+ const existing = db2.prepare("SELECT 1 FROM graph_edges WHERE from_type='agent' AND from_id=? AND to_type='agent' AND to_id=? AND relation='communicates_with'").get(pair.from_agent, pair.to_agent);
30514
+ upsert.run("agent", pair.from_agent, "agent", pair.to_agent, "communicates_with", pair.cnt);
30515
+ if (existing)
30516
+ updated++;
30517
+ else
30518
+ created++;
30519
+ }
30520
+ const spacePosts = db2.prepare(`
30521
+ SELECT from_agent, space, COUNT(*) as cnt
30522
+ FROM messages WHERE space IS NOT NULL
30523
+ GROUP BY from_agent, space
30524
+ `).all();
30525
+ for (const sp of spacePosts) {
30526
+ const existing = db2.prepare("SELECT 1 FROM graph_edges WHERE from_type='agent' AND from_id=? AND to_type='space' AND to_id=? AND relation='posts_in'").get(sp.from_agent, sp.space);
30527
+ upsert.run("agent", sp.from_agent, "space", sp.space, "posts_in", sp.cnt);
30528
+ if (existing)
30529
+ updated++;
30530
+ else
30531
+ created++;
30532
+ }
30533
+ const members = db2.prepare("SELECT agent, space FROM space_members").all();
30534
+ for (const m of members) {
30535
+ const existing = db2.prepare("SELECT 1 FROM graph_edges WHERE from_type='agent' AND from_id=? AND to_type='space' AND to_id=? AND relation='member_of'").get(m.agent, m.space);
30536
+ upsert.run("agent", m.agent, "space", m.space, "member_of", 1);
30537
+ if (existing)
30538
+ updated++;
30539
+ else
30540
+ created++;
30541
+ }
30542
+ const spaceProjects = db2.prepare("SELECT name, project_id FROM spaces WHERE project_id IS NOT NULL").all();
30543
+ for (const sp of spaceProjects) {
30544
+ const existing = db2.prepare("SELECT 1 FROM graph_edges WHERE from_type='space' AND from_id=? AND to_type='project' AND to_id=? AND relation='belongs_to'").get(sp.name, sp.project_id);
30545
+ upsert.run("space", sp.name, "project", sp.project_id, "belongs_to", 1);
30546
+ if (existing)
30547
+ updated++;
30548
+ else
30549
+ created++;
30550
+ }
30551
+ });
30552
+ insertOrUpdate();
30553
+ return { edges_created: created, edges_updated: updated };
30554
+ }
30555
+ function getRelated(entityType, entityId) {
30556
+ const db2 = getDb();
30557
+ ensureGraphTable();
30558
+ const outgoing = db2.prepare(`
30559
+ SELECT to_type as type, to_id as id, relation, weight FROM graph_edges
30560
+ WHERE from_type = ? AND from_id = ? ORDER BY weight DESC
30561
+ `).all(entityType, entityId);
30562
+ const incoming = db2.prepare(`
30563
+ SELECT from_type as type, from_id as id, relation, weight FROM graph_edges
30564
+ WHERE to_type = ? AND to_id = ? ORDER BY weight DESC
30565
+ `).all(entityType, entityId);
30566
+ return [...outgoing, ...incoming];
30567
+ }
30568
+ function getAgentNetwork(agent) {
30569
+ const db2 = getDb();
30570
+ ensureGraphTable();
30571
+ const comms = db2.prepare(`
30572
+ SELECT to_id as agent, weight as message_count,
30573
+ (SELECT MAX(created_at) FROM messages WHERE from_agent = ? AND to_agent = ge.to_id AND space IS NULL) as last_at
30574
+ FROM graph_edges ge
30575
+ WHERE from_type = 'agent' AND from_id = ? AND relation = 'communicates_with'
30576
+ ORDER BY weight DESC LIMIT 20
30577
+ `).all(agent, agent);
30578
+ const spaces = db2.prepare(`
30579
+ SELECT to_id as space, weight as message_count FROM graph_edges
30580
+ WHERE from_type = 'agent' AND from_id = ? AND relation = 'posts_in'
30581
+ ORDER BY weight DESC LIMIT 20
30582
+ `).all(agent);
30583
+ const projects = db2.prepare(`
30584
+ SELECT DISTINCT g2.to_id FROM graph_edges g1
30585
+ JOIN graph_edges g2 ON g1.to_type = 'space' AND g1.to_id = g2.from_id AND g2.relation = 'belongs_to'
30586
+ WHERE g1.from_type = 'agent' AND g1.from_id = ? AND g1.relation IN ('member_of', 'posts_in')
30587
+ `).all(agent);
30588
+ return {
30589
+ agent,
30590
+ communicates_with: comms,
30591
+ spaces,
30592
+ projects: projects.map((p) => p.to_id)
30593
+ };
30594
+ }
30595
+ function getGraphStats() {
30596
+ const db2 = getDb();
30597
+ ensureGraphTable();
30598
+ const total = db2.prepare("SELECT COUNT(*) as c FROM graph_edges").get().c;
30599
+ const byRelation = db2.prepare("SELECT relation, COUNT(*) as c FROM graph_edges GROUP BY relation ORDER BY c DESC").all();
30600
+ const map3 = {};
30601
+ for (const r of byRelation)
30602
+ map3[r.relation] = r.c;
30603
+ return { total_edges: total, by_relation: map3 };
30604
+ }
30021
30605
  // package.json
30022
30606
  var package_default = {
30023
30607
  name: "@hasna/conversations",
30024
- version: "0.1.33",
30608
+ version: "0.2.1",
30025
30609
  description: "Real-time CLI messaging for AI agents",
30026
30610
  type: "module",
30027
30611
  bin: {
@@ -30750,6 +31334,106 @@ server.registerTool("get_pinned_messages", {
30750
31334
  content: [{ type: "text", text: JSON.stringify(messages) }]
30751
31335
  };
30752
31336
  });
31337
+ server.registerTool("build_graph", {
31338
+ description: "Build/rebuild the knowledge graph from messages, spaces, and projects. Creates relationship edges between agents, spaces, and projects.",
31339
+ inputSchema: {}
31340
+ }, async () => {
31341
+ const result = buildGraph();
31342
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
31343
+ });
31344
+ server.registerTool("get_related", {
31345
+ description: "Find all entities related to a given entity in the knowledge graph.",
31346
+ inputSchema: {
31347
+ entity_type: exports_external.string(),
31348
+ entity_id: exports_external.string()
31349
+ }
31350
+ }, async (args) => {
31351
+ const related = getRelated(args.entity_type, args.entity_id);
31352
+ return { content: [{ type: "text", text: JSON.stringify(related) }] };
31353
+ });
31354
+ server.registerTool("get_agent_network", {
31355
+ description: "Get an agent's communication network: who they talk to, spaces, projects.",
31356
+ inputSchema: {
31357
+ agent: exports_external.string()
31358
+ }
31359
+ }, async (args) => {
31360
+ const network = getAgentNetwork(args.agent);
31361
+ return { content: [{ type: "text", text: JSON.stringify(network) }] };
31362
+ });
31363
+ server.registerTool("graph_stats", {
31364
+ description: "Get knowledge graph statistics: total edges and counts by relation type.",
31365
+ inputSchema: {}
31366
+ }, async () => {
31367
+ const stats = getGraphStats();
31368
+ return { content: [{ type: "text", text: JSON.stringify(stats) }] };
31369
+ });
31370
+ server.registerTool("get_summary", {
31371
+ description: "Get a structured summary of a conversation (session or space): participants, topics, key messages, blockers, activity.",
31372
+ inputSchema: {
31373
+ session_id: exports_external.string().optional(),
31374
+ space: exports_external.string().optional(),
31375
+ limit: exports_external.coerce.number().optional()
31376
+ }
31377
+ }, async (args) => {
31378
+ const target = args.space || args.session_id;
31379
+ if (!target)
31380
+ return { content: [{ type: "text", text: "session_id or space required" }], isError: true };
31381
+ const summary = getConversationSummary(target, { limit: args.limit });
31382
+ if (!summary)
31383
+ return { content: [{ type: "text", text: `No messages found for "${target}"` }], isError: true };
31384
+ return { content: [{ type: "text", text: JSON.stringify(summary) }] };
31385
+ });
31386
+ server.registerTool("get_topics", {
31387
+ description: "Extract topics from a space or session. Returns weighted keyword list.",
31388
+ inputSchema: {
31389
+ space: exports_external.string().optional(),
31390
+ session_id: exports_external.string().optional(),
31391
+ limit: exports_external.coerce.number().optional()
31392
+ }
31393
+ }, async (args) => {
31394
+ const topics = args.space ? getSpaceTopics(args.space, { limit: args.limit }) : args.session_id ? getSessionTopics(args.session_id, { limit: args.limit }) : getTrendingTopics({ top_n: args.limit });
31395
+ return { content: [{ type: "text", text: JSON.stringify(topics) }] };
31396
+ });
31397
+ server.registerTool("trending_topics", {
31398
+ description: "Get trending topics across all messages in the last N hours.",
31399
+ inputSchema: {
31400
+ hours: exports_external.coerce.number().optional(),
31401
+ project_id: exports_external.string().optional(),
31402
+ top_n: exports_external.coerce.number().optional()
31403
+ }
31404
+ }, async (args) => {
31405
+ const topics = getTrendingTopics({ hours: args.hours, project_id: args.project_id, top_n: args.top_n });
31406
+ return { content: [{ type: "text", text: JSON.stringify(topics) }] };
31407
+ });
31408
+ server.registerTool("get_session_activity", {
31409
+ description: "Get activity metrics for a session: message velocity, unique agents, reply ratio, reaction count, trending status.",
31410
+ inputSchema: {
31411
+ session_id: exports_external.string()
31412
+ }
31413
+ }, async (args) => {
31414
+ const activity = getSessionActivity(args.session_id);
31415
+ if (!activity) {
31416
+ return { content: [{ type: "text", text: `session "${args.session_id}" not found` }], isError: true };
31417
+ }
31418
+ return { content: [{ type: "text", text: JSON.stringify(activity) }] };
31419
+ });
31420
+ server.registerTool("hot_sessions", {
31421
+ description: "List conversations ranked by activity hotness (message velocity, reactions, replies, priority, blockers).",
31422
+ inputSchema: {
31423
+ limit: exports_external.coerce.number().optional(),
31424
+ min_score: exports_external.coerce.number().optional(),
31425
+ space: exports_external.string().optional(),
31426
+ project_id: exports_external.string().optional()
31427
+ }
31428
+ }, async (args) => {
31429
+ const sessions = listHotSessions({
31430
+ limit: args.limit,
31431
+ min_score: args.min_score,
31432
+ space: args.space,
31433
+ project_id: args.project_id
31434
+ });
31435
+ return { content: [{ type: "text", text: JSON.stringify(sessions) }] };
31436
+ });
30753
31437
  server.registerTool("add_reaction", {
30754
31438
  description: "Add an emoji reaction to a message.",
30755
31439
  inputSchema: {
@@ -31049,6 +31733,15 @@ server.registerTool("search_tools", {
31049
31733
  "pin_message",
31050
31734
  "unpin_message",
31051
31735
  "get_pinned_messages",
31736
+ "build_graph",
31737
+ "get_related",
31738
+ "get_agent_network",
31739
+ "graph_stats",
31740
+ "get_summary",
31741
+ "get_topics",
31742
+ "trending_topics",
31743
+ "get_session_activity",
31744
+ "hot_sessions",
31052
31745
  "add_reaction",
31053
31746
  "remove_reaction",
31054
31747
  "get_reactions",
@@ -31107,6 +31800,15 @@ server.registerTool("describe_tools", {
31107
31800
  pin_message: "Pin a message. Required: id",
31108
31801
  unpin_message: "Unpin a message. Required: id",
31109
31802
  get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
31803
+ build_graph: "Build/rebuild knowledge graph from messages, spaces, projects. Returns edge counts.",
31804
+ get_related: "Find entities related to a given entity. Required: entity_type, entity_id",
31805
+ get_agent_network: "Agent's communication network: contacts, spaces, projects. Required: agent",
31806
+ graph_stats: "Knowledge graph stats: total edges, by relation type",
31807
+ get_summary: "Structured conversation summary: participants, topics, key messages, blockers. Required: session_id? or space?. Optional: limit?",
31808
+ get_topics: "Extract topics from space or session. Optional: space?, session_id?, limit?",
31809
+ trending_topics: "Trending topics across all messages. Optional: hours?, project_id?, top_n?",
31810
+ get_session_activity: "Get activity metrics for a session: velocity, agents, reply ratio, reactions, trending. Required: session_id",
31811
+ hot_sessions: "List conversations by hotness score (velocity, reactions, replies, priority, blockers). Optional: limit?, min_score?, space?, project_id?",
31110
31812
  add_reaction: "Add emoji reaction to a message. Required: message_id, emoji. Optional: from?",
31111
31813
  remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
31112
31814
  get_reactions: "Get all reactions for a message. Required: message_id",