@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/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * conversations
11
11
  */
12
12
  export { sendMessage, readMessages, markRead, markSessionRead, markSpaceRead, markAllRead, getMessageById, searchMessages, exportMessages, deleteMessage, editMessage, pinMessage, unpinMessage, getPinnedMessages, getUnreadBlockers, getThreadReplies, } from "./lib/messages.js";
13
- export { listSessions, getSession, } from "./lib/sessions.js";
13
+ export { listSessions, getSession, getSessionActivity, } from "./lib/sessions.js";
14
+ export type { SessionActivity } from "./lib/sessions.js";
14
15
  export { createSpace, updateSpace, archiveSpace, unarchiveSpace, listSpaces, getSpace, joinSpace, leaveSpace, getSpaceMembers, isSpaceMember, getSpaceDepth, } from "./lib/spaces.js";
15
16
  export { createProject, listProjects, getProject, getProjectByName, updateProject, deleteProject, } from "./lib/projects.js";
16
17
  export { getDb, getDbPath, closeDb, } from "./lib/db.js";
@@ -22,4 +23,12 @@ export { fireWebhooks, } from "./lib/webhooks.js";
22
23
  export { heartbeat, registerAgent, isAgentConflict, getPresence, listAgents, removePresence, renameAgent, } from "./lib/presence.js";
23
24
  export { acquireLock, releaseLock, checkLock, cleanExpiredLocks, listLocks, } from "./lib/locks.js";
24
25
  export type { ResourceLock } from "./lib/locks.js";
25
- export type { Message, Session, Space, SpaceInfo, SpaceMember, Project, ProjectInfo, Priority, SendMessageOptions, ReadMessagesOptions, SearchMessagesOptions, AgentPresence, AgentConflictError, RegisterAgentResult, Reaction, Attachment, } from "./types.js";
26
+ export { computeHotness, listHotSessions, } from "./lib/hot.js";
27
+ export type { HotSession, HotSessionsOptions } from "./lib/hot.js";
28
+ export { extractTopics, getSpaceTopics, getSessionTopics, getTrendingTopics, } from "./lib/topics.js";
29
+ export type { TopicWeight } from "./lib/topics.js";
30
+ export { getConversationSummary } from "./lib/summary.js";
31
+ export { buildGraph, getRelated, getAgentNetwork, getGraphStats, } from "./lib/graph.js";
32
+ export type { GraphEdge, RelatedEntity, AgentNetwork } from "./lib/graph.js";
33
+ export type { ConversationSummary, SummaryOptions } from "./lib/summary.js";
34
+ export type { Message, Session, Space, SpaceInfo, SpaceMember, Project, ProjectInfo, Priority, SendMessageOptions, ReadMessagesOptions, SearchMessagesOptions, SearchResult, AgentPresence, AgentConflictError, RegisterAgentResult, Reaction, Attachment, } from "./types.js";
package/dist/index.js CHANGED
@@ -2480,12 +2480,18 @@ function getThreadReplies(messageId) {
2480
2480
  function searchMessages(opts) {
2481
2481
  const db2 = getDb();
2482
2482
  const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
2483
+ const sortByRelevance = opts.sort !== "recent";
2484
+ const priorityWeights = { urgent: 10, high: 5, normal: 1, low: 0.5 };
2483
2485
  try {
2484
- const ftsConditions = [];
2485
2486
  const ftsParams = [];
2486
- const words = opts.query.trim().split(/\s+/).filter(Boolean);
2487
- const ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
2488
- ftsConditions.push("messages_fts MATCH ?");
2487
+ const query = opts.query.trim();
2488
+ let ftsQuery;
2489
+ if (query.startsWith('"') && query.endsWith('"')) {
2490
+ ftsQuery = query;
2491
+ } else {
2492
+ const words = query.split(/\s+/).filter(Boolean);
2493
+ ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
2494
+ }
2489
2495
  ftsParams.push(ftsQuery);
2490
2496
  let extraWhere = "";
2491
2497
  if (opts.space) {
@@ -2500,11 +2506,23 @@ function searchMessages(opts) {
2500
2506
  extraWhere += " AND m.to_agent = ?";
2501
2507
  ftsParams.push(opts.to);
2502
2508
  }
2503
- const rows2 = db2.prepare(`SELECT m.* FROM messages m
2509
+ const orderClause = sortByRelevance ? "ORDER BY rank" : "ORDER BY m.created_at DESC, m.id DESC";
2510
+ const rows2 = db2.prepare(`SELECT m.*, rank,
2511
+ snippet(messages_fts, 0, '**', '**', '...', 20) as snippet
2512
+ FROM messages m
2504
2513
  JOIN messages_fts ON messages_fts.rowid = m.id
2505
- WHERE ${ftsConditions.join(" AND ")}${extraWhere}
2506
- ORDER BY m.created_at DESC, m.id DESC LIMIT ${limit}`).all(...ftsParams);
2507
- return rows2.map(parseMessage);
2514
+ WHERE messages_fts MATCH ?${extraWhere}
2515
+ ${orderClause} LIMIT ${limit}`).all(...ftsParams);
2516
+ const maxRank = rows2.reduce((max, r) => Math.max(max, Math.abs(r.rank || 0)), 0) || 1;
2517
+ return rows2.map((row) => {
2518
+ const msg = parseMessage(row);
2519
+ const ftsScore = maxRank > 0 ? Math.abs(row.rank || 0) / maxRank * 100 : 50;
2520
+ const priorityBoost = priorityWeights[msg.priority] || 1;
2521
+ const pinnedBoost = msg.pinned_at ? 20 : 0;
2522
+ const blockingBoost = msg.blocking ? 15 : 0;
2523
+ const relevance_score = Math.round((ftsScore * priorityBoost + pinnedBoost + blockingBoost) * 100) / 100;
2524
+ return { ...msg, snippet: row.snippet || null, relevance_score };
2525
+ });
2508
2526
  } catch {}
2509
2527
  const conditions = ["content LIKE ?"];
2510
2528
  const params = [`%${opts.query}%`];
@@ -2522,7 +2540,10 @@ function searchMessages(opts) {
2522
2540
  }
2523
2541
  const where = `WHERE ${conditions.join(" AND ")}`;
2524
2542
  const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
2525
- return rows.map(parseMessage);
2543
+ return rows.map((row) => {
2544
+ const msg = parseMessage(row);
2545
+ return { ...msg, snippet: null, relevance_score: 0 };
2546
+ });
2526
2547
  }
2527
2548
  // src/lib/sessions.ts
2528
2549
  init_db();
@@ -2579,6 +2600,32 @@ function getSession(sessionId) {
2579
2600
  unread_count: row.unread_count
2580
2601
  };
2581
2602
  }
2603
+ function getSessionActivity(sessionId) {
2604
+ const db2 = getDb();
2605
+ const exists = db2.prepare("SELECT 1 FROM messages WHERE session_id = ? LIMIT 1").get(sessionId);
2606
+ if (!exists)
2607
+ return null;
2608
+ 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;
2609
+ 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;
2610
+ const uniqueAgents = db2.prepare("SELECT COUNT(DISTINCT from_agent) as c FROM messages WHERE session_id = ?").get(sessionId).c;
2611
+ const totalMsgs = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ?").get(sessionId).c;
2612
+ const replyCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND reply_to IS NOT NULL").get(sessionId).c;
2613
+ const replyRatio = totalMsgs > 0 ? Math.round(replyCount / totalMsgs * 100) / 100 : 0;
2614
+ 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);
2615
+ 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;
2616
+ 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;
2617
+ const isTrending = msgsLast1h >= 5 || agentsLast1h >= 3;
2618
+ return {
2619
+ session_id: sessionId,
2620
+ msgs_last_1h: msgsLast1h,
2621
+ msgs_last_24h: msgsLast24h,
2622
+ unique_agents: uniqueAgents,
2623
+ reply_ratio: replyRatio,
2624
+ avg_priority: priorityRow?.priority ?? "normal",
2625
+ reaction_count: reactionCount,
2626
+ is_trending: isTrending
2627
+ };
2628
+ }
2582
2629
  // src/lib/spaces.ts
2583
2630
  init_db();
2584
2631
  function getSpaceDepth(spaceName) {
@@ -3670,6 +3717,483 @@ function listLocks(opts) {
3670
3717
  query += " ORDER BY locked_at ASC";
3671
3718
  return db2.prepare(query).all(...params);
3672
3719
  }
3720
+ // src/lib/hot.ts
3721
+ init_db();
3722
+ function computeHotness(sessionId) {
3723
+ const db2 = getDb();
3724
+ const base = db2.prepare(`
3725
+ SELECT session_id,
3726
+ GROUP_CONCAT(DISTINCT from_agent) as agents,
3727
+ MAX(space) as space,
3728
+ MAX(created_at) as last_message_at,
3729
+ COUNT(*) as message_count
3730
+ FROM messages WHERE session_id = ?
3731
+ GROUP BY session_id
3732
+ `).get(sessionId);
3733
+ if (!base)
3734
+ return null;
3735
+ 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;
3736
+ 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;
3737
+ const uniqueAgents = db2.prepare("SELECT COUNT(DISTINCT from_agent) as c FROM messages WHERE session_id = ?").get(sessionId).c;
3738
+ 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;
3739
+ const replyCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND reply_to IS NOT NULL").get(sessionId).c;
3740
+ const highPriorityCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND priority IN ('high', 'urgent')").get(sessionId).c;
3741
+ const blockerCount = db2.prepare("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND blocking = 1").get(sessionId).c;
3742
+ const lastMsgMs = new Date(base.last_message_at + "Z").getTime();
3743
+ const hoursSinceLast = Math.max(0, (Date.now() - lastMsgMs) / 3600000);
3744
+ const hotness_score = Math.round(msgsLast1h * 3 + uniqueAgents * 5 + reactionCount * 2 + replyCount * 4 + highPriorityCount * 10 + blockerCount * 20 - hoursSinceLast * 2);
3745
+ return {
3746
+ session_id: base.session_id,
3747
+ participants: base.agents.split(","),
3748
+ space: base.space,
3749
+ last_message_at: base.last_message_at,
3750
+ message_count: base.message_count,
3751
+ hotness_score,
3752
+ metrics: {
3753
+ msgs_last_1h: msgsLast1h,
3754
+ msgs_last_24h: msgsLast24h,
3755
+ unique_agents: uniqueAgents,
3756
+ reaction_count: reactionCount,
3757
+ reply_count: replyCount,
3758
+ high_priority_count: highPriorityCount,
3759
+ blocker_count: blockerCount,
3760
+ hours_since_last: Math.round(hoursSinceLast * 10) / 10
3761
+ }
3762
+ };
3763
+ }
3764
+ function listHotSessions(opts) {
3765
+ const db2 = getDb();
3766
+ const limit = opts?.limit ?? 20;
3767
+ const minScore = opts?.min_score ?? 0;
3768
+ let where = "";
3769
+ const params = [];
3770
+ if (opts?.space) {
3771
+ where = " WHERE space = ?";
3772
+ params.push(opts.space);
3773
+ } else if (opts?.project_id) {
3774
+ where = " WHERE project_id = ?";
3775
+ params.push(opts.project_id);
3776
+ }
3777
+ 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);
3778
+ const hotSessions = [];
3779
+ for (const { session_id } of sessions) {
3780
+ const hot = computeHotness(session_id);
3781
+ if (hot && hot.hotness_score >= minScore) {
3782
+ hotSessions.push(hot);
3783
+ }
3784
+ }
3785
+ hotSessions.sort((a, b) => b.hotness_score - a.hotness_score);
3786
+ return hotSessions.slice(0, limit);
3787
+ }
3788
+ // src/lib/topics.ts
3789
+ init_db();
3790
+ var STOPWORDS = new Set([
3791
+ "a",
3792
+ "an",
3793
+ "the",
3794
+ "and",
3795
+ "or",
3796
+ "but",
3797
+ "in",
3798
+ "on",
3799
+ "at",
3800
+ "to",
3801
+ "for",
3802
+ "of",
3803
+ "with",
3804
+ "by",
3805
+ "from",
3806
+ "is",
3807
+ "it",
3808
+ "this",
3809
+ "that",
3810
+ "are",
3811
+ "was",
3812
+ "were",
3813
+ "be",
3814
+ "been",
3815
+ "being",
3816
+ "have",
3817
+ "has",
3818
+ "had",
3819
+ "do",
3820
+ "does",
3821
+ "did",
3822
+ "will",
3823
+ "would",
3824
+ "could",
3825
+ "should",
3826
+ "may",
3827
+ "might",
3828
+ "shall",
3829
+ "can",
3830
+ "need",
3831
+ "not",
3832
+ "no",
3833
+ "so",
3834
+ "if",
3835
+ "then",
3836
+ "than",
3837
+ "too",
3838
+ "very",
3839
+ "just",
3840
+ "about",
3841
+ "up",
3842
+ "out",
3843
+ "all",
3844
+ "also",
3845
+ "as",
3846
+ "into",
3847
+ "only",
3848
+ "other",
3849
+ "each",
3850
+ "every",
3851
+ "both",
3852
+ "few",
3853
+ "more",
3854
+ "most",
3855
+ "some",
3856
+ "such",
3857
+ "any",
3858
+ "over",
3859
+ "after",
3860
+ "before",
3861
+ "between",
3862
+ "under",
3863
+ "above",
3864
+ "here",
3865
+ "there",
3866
+ "when",
3867
+ "where",
3868
+ "how",
3869
+ "what",
3870
+ "which",
3871
+ "who",
3872
+ "whom",
3873
+ "why",
3874
+ "its",
3875
+ "my",
3876
+ "your",
3877
+ "his",
3878
+ "her",
3879
+ "our",
3880
+ "their",
3881
+ "we",
3882
+ "you",
3883
+ "he",
3884
+ "she",
3885
+ "they",
3886
+ "i",
3887
+ "me",
3888
+ "him",
3889
+ "us",
3890
+ "them",
3891
+ "now",
3892
+ "new",
3893
+ "get",
3894
+ "got",
3895
+ "go",
3896
+ "going",
3897
+ "done",
3898
+ "make",
3899
+ "made",
3900
+ "see",
3901
+ "know",
3902
+ "think",
3903
+ "want",
3904
+ "one",
3905
+ "two",
3906
+ "like",
3907
+ "still",
3908
+ "back",
3909
+ "even"
3910
+ ]);
3911
+ function extractTopics(text, topN = 10) {
3912
+ const cleaned = text.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]+`/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[#*_~|>\[\](){}]/g, " ").replace(/\d+/g, " ").toLowerCase();
3913
+ const words = cleaned.split(/\s+/).filter((w) => w.length >= 3 && !STOPWORDS.has(w) && /^[a-z]/.test(w));
3914
+ const freq = new Map;
3915
+ for (const w of words) {
3916
+ freq.set(w, (freq.get(w) || 0) + 1);
3917
+ }
3918
+ const totalWords = words.length || 1;
3919
+ return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN).map(([topic, count]) => ({
3920
+ topic,
3921
+ weight: Math.round(count / totalWords * 1000) / 1000,
3922
+ count
3923
+ }));
3924
+ }
3925
+ function getSpaceTopics(spaceName, opts) {
3926
+ const db2 = getDb();
3927
+ const limit = opts?.limit ?? 100;
3928
+ const sinceClause = opts?.since ? "AND created_at > ?" : "";
3929
+ const params = [spaceName];
3930
+ if (opts?.since)
3931
+ params.push(opts.since);
3932
+ const rows = db2.prepare(`SELECT content FROM messages WHERE space = ? ${sinceClause} ORDER BY created_at DESC LIMIT ${limit}`).all(...params);
3933
+ const combined = rows.map((r) => r.content).join(`
3934
+ `);
3935
+ return extractTopics(combined, 15);
3936
+ }
3937
+ function getSessionTopics(sessionId, opts) {
3938
+ const db2 = getDb();
3939
+ const limit = opts?.limit ?? 100;
3940
+ const rows = db2.prepare(`SELECT content FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ${limit}`).all(sessionId);
3941
+ const combined = rows.map((r) => r.content).join(`
3942
+ `);
3943
+ return extractTopics(combined, 15);
3944
+ }
3945
+ function getTrendingTopics(opts) {
3946
+ const db2 = getDb();
3947
+ const hours = opts?.hours ?? 24;
3948
+ const topN = opts?.top_n ?? 20;
3949
+ let where = `WHERE created_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${hours} hours')`;
3950
+ const params = [];
3951
+ if (opts?.project_id) {
3952
+ where += " AND project_id = ?";
3953
+ params.push(opts.project_id);
3954
+ }
3955
+ const rows = db2.prepare(`SELECT content FROM messages ${where} ORDER BY created_at DESC LIMIT 500`).all(...params);
3956
+ const combined = rows.map((r) => r.content).join(`
3957
+ `);
3958
+ return extractTopics(combined, topN);
3959
+ }
3960
+ // src/lib/summary.ts
3961
+ init_db();
3962
+ function getConversationSummary(sessionOrSpace, opts) {
3963
+ const db2 = getDb();
3964
+ const limit = opts?.limit ?? 50;
3965
+ const isSpace = sessionOrSpace.startsWith("space:") || db2.prepare("SELECT 1 FROM spaces WHERE name = ?").get(sessionOrSpace);
3966
+ const filterCol = isSpace ? "space" : "session_id";
3967
+ const filterVal = isSpace && !sessionOrSpace.startsWith("space:") ? sessionOrSpace : sessionOrSpace;
3968
+ const messages = db2.prepare(`SELECT * FROM messages WHERE ${filterCol} = ? ORDER BY created_at DESC LIMIT ${limit}`).all(filterVal);
3969
+ if (messages.length === 0)
3970
+ return null;
3971
+ const agents = new Set;
3972
+ for (const m of messages) {
3973
+ agents.add(m.from_agent);
3974
+ if (m.to_agent)
3975
+ agents.add(m.to_agent);
3976
+ }
3977
+ const dates = messages.map((m) => m.created_at).sort();
3978
+ const dateRange = { first: dates[0], last: dates[dates.length - 1] };
3979
+ const allContent = messages.map((m) => m.content).join(`
3980
+ `);
3981
+ const topics = extractTopics(allContent, 10);
3982
+ const keyMessages = [];
3983
+ for (const m of messages) {
3984
+ const priority = m.priority;
3985
+ if (priority === "high" || priority === "urgent") {
3986
+ keyMessages.push({
3987
+ id: m.id,
3988
+ from: m.from_agent,
3989
+ content: m.content.slice(0, 200),
3990
+ reason: `${priority} priority`
3991
+ });
3992
+ }
3993
+ if (m.blocking) {
3994
+ keyMessages.push({
3995
+ id: m.id,
3996
+ from: m.from_agent,
3997
+ content: m.content.slice(0, 200),
3998
+ reason: "blocking message"
3999
+ });
4000
+ }
4001
+ }
4002
+ for (const m of messages) {
4003
+ if (m.pinned_at) {
4004
+ keyMessages.push({
4005
+ id: m.id,
4006
+ from: m.from_agent,
4007
+ content: m.content.slice(0, 200),
4008
+ reason: "pinned"
4009
+ });
4010
+ }
4011
+ }
4012
+ const msgIds = messages.map((m) => m.id);
4013
+ if (msgIds.length > 0) {
4014
+ const placeholders = msgIds.map(() => "?").join(",");
4015
+ 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);
4016
+ for (const r of reacted) {
4017
+ const m = messages.find((msg) => msg.id === r.message_id);
4018
+ if (m) {
4019
+ keyMessages.push({
4020
+ id: r.message_id,
4021
+ from: m.from_agent,
4022
+ content: m.content.slice(0, 200),
4023
+ reason: `${r.c} reaction(s)`
4024
+ });
4025
+ }
4026
+ }
4027
+ }
4028
+ const seen = new Set;
4029
+ const uniqueKey = keyMessages.filter((k) => {
4030
+ if (seen.has(k.id))
4031
+ return false;
4032
+ seen.add(k.id);
4033
+ return true;
4034
+ }).slice(0, 10);
4035
+ const blockers = messages.filter((m) => m.blocking && !m.read_at).map((m) => ({
4036
+ id: m.id,
4037
+ from: m.from_agent,
4038
+ content: m.content.slice(0, 200),
4039
+ created_at: m.created_at
4040
+ }));
4041
+ const replyCount = messages.filter((m) => m.reply_to).length;
4042
+ const reactionCount = msgIds.length > 0 ? db2.prepare(`SELECT COUNT(*) as c FROM reactions WHERE message_id IN (${msgIds.map(() => "?").join(",")})`).get(...msgIds).c : 0;
4043
+ const priorityCounts = {};
4044
+ for (const m of messages) {
4045
+ const p = m.priority;
4046
+ priorityCounts[p] = (priorityCounts[p] || 0) + 1;
4047
+ }
4048
+ const avgPriority = Object.entries(priorityCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "normal";
4049
+ return {
4050
+ session_id: sessionOrSpace,
4051
+ participants: [...agents].filter((a) => a !== sessionOrSpace),
4052
+ message_count: messages.length,
4053
+ date_range: dateRange,
4054
+ topics,
4055
+ key_messages: uniqueKey,
4056
+ unresolved_blockers: blockers,
4057
+ activity: {
4058
+ reply_count: replyCount,
4059
+ reaction_count: reactionCount,
4060
+ avg_priority: avgPriority
4061
+ }
4062
+ };
4063
+ }
4064
+ // src/lib/graph.ts
4065
+ init_db();
4066
+ function ensureGraphTable() {
4067
+ const db2 = getDb();
4068
+ db2.exec(`
4069
+ CREATE TABLE IF NOT EXISTS graph_edges (
4070
+ from_type TEXT NOT NULL,
4071
+ from_id TEXT NOT NULL,
4072
+ to_type TEXT NOT NULL,
4073
+ to_id TEXT NOT NULL,
4074
+ relation TEXT NOT NULL,
4075
+ weight REAL NOT NULL DEFAULT 1,
4076
+ metadata TEXT,
4077
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
4078
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
4079
+ UNIQUE(from_type, from_id, to_type, to_id, relation)
4080
+ )
4081
+ `);
4082
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_from ON graph_edges(from_type, from_id)");
4083
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_to ON graph_edges(to_type, to_id)");
4084
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_graph_relation ON graph_edges(relation)");
4085
+ }
4086
+ function buildGraph() {
4087
+ const db2 = getDb();
4088
+ ensureGraphTable();
4089
+ let created = 0;
4090
+ let updated = 0;
4091
+ const upsert = db2.prepare(`
4092
+ INSERT INTO graph_edges (from_type, from_id, to_type, to_id, relation, weight, updated_at)
4093
+ VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))
4094
+ ON CONFLICT(from_type, from_id, to_type, to_id, relation) DO UPDATE SET
4095
+ weight = excluded.weight,
4096
+ updated_at = excluded.updated_at
4097
+ `);
4098
+ const insertOrUpdate = db2.transaction(() => {
4099
+ const dmPairs = db2.prepare(`
4100
+ SELECT from_agent, to_agent, COUNT(*) as cnt, MAX(created_at) as last_at
4101
+ FROM messages WHERE space IS NULL AND from_agent != to_agent
4102
+ GROUP BY from_agent, to_agent
4103
+ `).all();
4104
+ for (const pair of dmPairs) {
4105
+ 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);
4106
+ upsert.run("agent", pair.from_agent, "agent", pair.to_agent, "communicates_with", pair.cnt);
4107
+ if (existing)
4108
+ updated++;
4109
+ else
4110
+ created++;
4111
+ }
4112
+ const spacePosts = db2.prepare(`
4113
+ SELECT from_agent, space, COUNT(*) as cnt
4114
+ FROM messages WHERE space IS NOT NULL
4115
+ GROUP BY from_agent, space
4116
+ `).all();
4117
+ for (const sp of spacePosts) {
4118
+ 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);
4119
+ upsert.run("agent", sp.from_agent, "space", sp.space, "posts_in", sp.cnt);
4120
+ if (existing)
4121
+ updated++;
4122
+ else
4123
+ created++;
4124
+ }
4125
+ const members = db2.prepare("SELECT agent, space FROM space_members").all();
4126
+ for (const m of members) {
4127
+ 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);
4128
+ upsert.run("agent", m.agent, "space", m.space, "member_of", 1);
4129
+ if (existing)
4130
+ updated++;
4131
+ else
4132
+ created++;
4133
+ }
4134
+ const spaceProjects = db2.prepare("SELECT name, project_id FROM spaces WHERE project_id IS NOT NULL").all();
4135
+ for (const sp of spaceProjects) {
4136
+ 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);
4137
+ upsert.run("space", sp.name, "project", sp.project_id, "belongs_to", 1);
4138
+ if (existing)
4139
+ updated++;
4140
+ else
4141
+ created++;
4142
+ }
4143
+ });
4144
+ insertOrUpdate();
4145
+ return { edges_created: created, edges_updated: updated };
4146
+ }
4147
+ function getRelated(entityType, entityId) {
4148
+ const db2 = getDb();
4149
+ ensureGraphTable();
4150
+ const outgoing = db2.prepare(`
4151
+ SELECT to_type as type, to_id as id, relation, weight FROM graph_edges
4152
+ WHERE from_type = ? AND from_id = ? ORDER BY weight DESC
4153
+ `).all(entityType, entityId);
4154
+ const incoming = db2.prepare(`
4155
+ SELECT from_type as type, from_id as id, relation, weight FROM graph_edges
4156
+ WHERE to_type = ? AND to_id = ? ORDER BY weight DESC
4157
+ `).all(entityType, entityId);
4158
+ return [...outgoing, ...incoming];
4159
+ }
4160
+ function getAgentNetwork(agent) {
4161
+ const db2 = getDb();
4162
+ ensureGraphTable();
4163
+ const comms = db2.prepare(`
4164
+ SELECT to_id as agent, weight as message_count,
4165
+ (SELECT MAX(created_at) FROM messages WHERE from_agent = ? AND to_agent = ge.to_id AND space IS NULL) as last_at
4166
+ FROM graph_edges ge
4167
+ WHERE from_type = 'agent' AND from_id = ? AND relation = 'communicates_with'
4168
+ ORDER BY weight DESC LIMIT 20
4169
+ `).all(agent, agent);
4170
+ const spaces = db2.prepare(`
4171
+ SELECT to_id as space, weight as message_count FROM graph_edges
4172
+ WHERE from_type = 'agent' AND from_id = ? AND relation = 'posts_in'
4173
+ ORDER BY weight DESC LIMIT 20
4174
+ `).all(agent);
4175
+ const projects = db2.prepare(`
4176
+ SELECT DISTINCT g2.to_id FROM graph_edges g1
4177
+ JOIN graph_edges g2 ON g1.to_type = 'space' AND g1.to_id = g2.from_id AND g2.relation = 'belongs_to'
4178
+ WHERE g1.from_type = 'agent' AND g1.from_id = ? AND g1.relation IN ('member_of', 'posts_in')
4179
+ `).all(agent);
4180
+ return {
4181
+ agent,
4182
+ communicates_with: comms,
4183
+ spaces,
4184
+ projects: projects.map((p) => p.to_id)
4185
+ };
4186
+ }
4187
+ function getGraphStats() {
4188
+ const db2 = getDb();
4189
+ ensureGraphTable();
4190
+ const total = db2.prepare("SELECT COUNT(*) as c FROM graph_edges").get().c;
4191
+ const byRelation = db2.prepare("SELECT relation, COUNT(*) as c FROM graph_edges GROUP BY relation ORDER BY c DESC").all();
4192
+ const map = {};
4193
+ for (const r of byRelation)
4194
+ map[r.relation] = r.c;
4195
+ return { total_edges: total, by_relation: map };
4196
+ }
3673
4197
  export {
3674
4198
  useSpaceMessages,
3675
4199
  useMessages,
@@ -3697,6 +4221,7 @@ export {
3697
4221
  listSessions,
3698
4222
  listProjects,
3699
4223
  listLocks,
4224
+ listHotSessions,
3700
4225
  listAgents,
3701
4226
  leaveSpace,
3702
4227
  joinSpace,
@@ -3704,11 +4229,16 @@ export {
3704
4229
  isAgentConflict,
3705
4230
  heartbeat,
3706
4231
  getUnreadBlockers,
4232
+ getTrendingTopics,
3707
4233
  getThreadReplies,
4234
+ getSpaceTopics,
3708
4235
  getSpaceMembers,
3709
4236
  getSpaceDepth,
3710
4237
  getSpace,
4238
+ getSessionTopics,
4239
+ getSessionActivity,
3711
4240
  getSession,
4241
+ getRelated,
3712
4242
  getReactions,
3713
4243
  getReactionSummary,
3714
4244
  getProjectByName,
@@ -3716,18 +4246,24 @@ export {
3716
4246
  getPresence,
3717
4247
  getPinnedMessages,
3718
4248
  getMessageById,
4249
+ getGraphStats,
3719
4250
  getDbPath,
3720
4251
  getDb,
4252
+ getConversationSummary,
4253
+ getAgentNetwork,
3721
4254
  fireWebhooks,
4255
+ extractTopics,
3722
4256
  exportMessages,
3723
4257
  editMessage,
3724
4258
  deleteProject,
3725
4259
  deleteMessage,
3726
4260
  createSpace,
3727
4261
  createProject,
4262
+ computeHotness,
3728
4263
  closeDb,
3729
4264
  cleanExpiredLocks,
3730
4265
  checkLock,
4266
+ buildGraph,
3731
4267
  archiveSpace,
3732
4268
  addReaction,
3733
4269
  acquireLock