@agi-cli/server 0.1.124 → 0.1.125

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.
@@ -0,0 +1,135 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { getDb } from '@agi-cli/database';
4
+ import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
+ import { eq, desc, asc, like, and, sql } from 'drizzle-orm';
6
+
7
+ const inputSchema = z.object({
8
+ query: z.string().min(1).describe('Search term to find in message content'),
9
+ limit: z
10
+ .number()
11
+ .min(1)
12
+ .max(50)
13
+ .default(20)
14
+ .describe('Max results to return'),
15
+ });
16
+
17
+ export function buildSearchHistoryTool(projectRoot: string) {
18
+ return {
19
+ name: 'search_history',
20
+ tool: tool({
21
+ description:
22
+ 'Full-text search across all message content in session history. Find past conversations, solutions, or discussions about specific topics.',
23
+ inputSchema,
24
+ async execute(input) {
25
+ const db = await getDb(projectRoot);
26
+
27
+ const projectSessionIds = await db
28
+ .select({ id: sessions.id })
29
+ .from(sessions)
30
+ .where(eq(sessions.projectPath, projectRoot));
31
+
32
+ const sessionIdSet = new Set(projectSessionIds.map((s) => s.id));
33
+
34
+ if (sessionIdSet.size === 0) {
35
+ return {
36
+ ok: true,
37
+ results: [],
38
+ total: 0,
39
+ };
40
+ }
41
+
42
+ const searchPattern = `%${input.query}%`;
43
+
44
+ const matchingParts = await db
45
+ .select({
46
+ id: messageParts.id,
47
+ messageId: messageParts.messageId,
48
+ content: messageParts.content,
49
+ type: messageParts.type,
50
+ })
51
+ .from(messageParts)
52
+ .where(
53
+ and(
54
+ eq(messageParts.type, 'text'),
55
+ like(messageParts.content, searchPattern),
56
+ ),
57
+ )
58
+ .limit(input.limit * 3);
59
+
60
+ const results: Array<{
61
+ sessionId: string;
62
+ sessionTitle: string | null;
63
+ messageId: string;
64
+ role: string;
65
+ matchedContent: string;
66
+ createdAt: number;
67
+ }> = [];
68
+
69
+ for (const part of matchingParts) {
70
+ if (results.length >= input.limit) break;
71
+
72
+ const msgRows = await db
73
+ .select({
74
+ id: messages.id,
75
+ sessionId: messages.sessionId,
76
+ role: messages.role,
77
+ createdAt: messages.createdAt,
78
+ })
79
+ .from(messages)
80
+ .where(eq(messages.id, part.messageId))
81
+ .limit(1);
82
+
83
+ if (msgRows.length === 0) continue;
84
+
85
+ const msg = msgRows[0];
86
+
87
+ if (!sessionIdSet.has(msg.sessionId)) continue;
88
+
89
+ const sessionRows = await db
90
+ .select({ title: sessions.title })
91
+ .from(sessions)
92
+ .where(eq(sessions.id, msg.sessionId))
93
+ .limit(1);
94
+
95
+ const content = part.content ?? '';
96
+ const queryLower = input.query.toLowerCase();
97
+ const contentLower = content.toLowerCase();
98
+ const matchIndex = contentLower.indexOf(queryLower);
99
+
100
+ let matchedContent: string;
101
+ if (matchIndex >= 0) {
102
+ const start = Math.max(0, matchIndex - 50);
103
+ const end = Math.min(
104
+ content.length,
105
+ matchIndex + input.query.length + 50,
106
+ );
107
+ const prefix = start > 0 ? '...' : '';
108
+ const suffix = end < content.length ? '...' : '';
109
+ matchedContent = prefix + content.slice(start, end) + suffix;
110
+ } else {
111
+ matchedContent =
112
+ content.slice(0, 150) + (content.length > 150 ? '...' : '');
113
+ }
114
+
115
+ results.push({
116
+ sessionId: msg.sessionId,
117
+ sessionTitle: sessionRows[0]?.title ?? null,
118
+ messageId: msg.id,
119
+ role: msg.role,
120
+ matchedContent,
121
+ createdAt: msg.createdAt,
122
+ });
123
+ }
124
+
125
+ results.sort((a, b) => b.createdAt - a.createdAt);
126
+
127
+ return {
128
+ ok: true,
129
+ results,
130
+ total: results.length,
131
+ };
132
+ },
133
+ }),
134
+ };
135
+ }