pi-hermes-memory 0.3.3 → 0.4.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/README.md +42 -9
- package/docs/0.4/PLAN.md +160 -0
- package/docs/0.4/TASKS.md +113 -0
- package/docs/ROADMAP.md +47 -29
- package/package.json +5 -1
- package/src/constants.ts +3 -3
- package/src/handlers/index-sessions.ts +64 -0
- package/src/index.ts +39 -0
- package/src/skills/learn-memory-tool/SKILL.md +125 -0
- package/src/store/db.ts +84 -0
- package/src/store/schema.ts +94 -0
- package/src/store/session-indexer.ts +153 -0
- package/src/store/session-parser.ts +214 -0
- package/src/store/session-search.ts +134 -0
- package/src/store/sqlite-memory-store.ts +215 -0
- package/src/tools/memory-search-tool.ts +74 -0
- package/src/tools/session-search-tool.ts +79 -0
- package/src/types.ts +7 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { DatabaseManager } from './db.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape a string for FTS5 query syntax.
|
|
5
|
+
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
6
|
+
*/
|
|
7
|
+
function escapeFts5Query(query: string): string {
|
|
8
|
+
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
9
|
+
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
10
|
+
return query;
|
|
11
|
+
}
|
|
12
|
+
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
13
|
+
return `"${query.replace(/"/g, '""')}"`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Search result from session history.
|
|
18
|
+
*/
|
|
19
|
+
export interface SessionSearchResult {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
project: string;
|
|
22
|
+
role: string;
|
|
23
|
+
content: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
snippet: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Search options for session search.
|
|
30
|
+
*/
|
|
31
|
+
export interface SessionSearchOptions {
|
|
32
|
+
/** Maximum number of results (default: 10) */
|
|
33
|
+
limit?: number;
|
|
34
|
+
/** Filter by project name */
|
|
35
|
+
project?: string;
|
|
36
|
+
/** Filter by role: 'user', 'assistant', 'system' */
|
|
37
|
+
role?: string;
|
|
38
|
+
/** Only return messages after this date (ISO string) */
|
|
39
|
+
since?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Search across indexed session messages using FTS5.
|
|
44
|
+
*
|
|
45
|
+
* @param dbManager — Database manager instance
|
|
46
|
+
* @param query — FTS5 search query
|
|
47
|
+
* @param options — Search options
|
|
48
|
+
* @returns Array of search results with snippets
|
|
49
|
+
*/
|
|
50
|
+
export function searchSessions(
|
|
51
|
+
dbManager: DatabaseManager,
|
|
52
|
+
query: string,
|
|
53
|
+
options: SessionSearchOptions = {}
|
|
54
|
+
): SessionSearchResult[] {
|
|
55
|
+
const db = dbManager.getDb();
|
|
56
|
+
const { limit = 10, project, role, since } = options;
|
|
57
|
+
|
|
58
|
+
// Build the query dynamically based on filters
|
|
59
|
+
const conditions: string[] = [];
|
|
60
|
+
const params: unknown[] = [];
|
|
61
|
+
|
|
62
|
+
// FTS5 match condition — use subquery for reliable rowid matching
|
|
63
|
+
conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
|
|
64
|
+
params.push(escapeFts5Query(query));
|
|
65
|
+
|
|
66
|
+
// Project filter
|
|
67
|
+
if (project) {
|
|
68
|
+
conditions.push('s.project = ?');
|
|
69
|
+
params.push(project);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Role filter
|
|
73
|
+
if (role) {
|
|
74
|
+
conditions.push('m.role = ?');
|
|
75
|
+
params.push(role);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Date filter
|
|
79
|
+
if (since) {
|
|
80
|
+
conditions.push('m.timestamp >= ?');
|
|
81
|
+
params.push(since);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
85
|
+
|
|
86
|
+
const sql = `
|
|
87
|
+
SELECT
|
|
88
|
+
m.session_id,
|
|
89
|
+
s.project,
|
|
90
|
+
m.role,
|
|
91
|
+
m.content,
|
|
92
|
+
m.timestamp,
|
|
93
|
+
m.content as snippet
|
|
94
|
+
FROM messages m
|
|
95
|
+
JOIN sessions s ON s.id = m.session_id
|
|
96
|
+
${whereClause}
|
|
97
|
+
ORDER BY m.timestamp DESC
|
|
98
|
+
LIMIT ?
|
|
99
|
+
`;
|
|
100
|
+
params.push(limit);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
104
|
+
session_id: string;
|
|
105
|
+
project: string;
|
|
106
|
+
role: string;
|
|
107
|
+
content: string;
|
|
108
|
+
timestamp: string;
|
|
109
|
+
snippet: string;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
// Map snake_case column names to camelCase
|
|
113
|
+
return rows.map(row => ({
|
|
114
|
+
sessionId: row.session_id,
|
|
115
|
+
project: row.project,
|
|
116
|
+
role: row.role,
|
|
117
|
+
content: row.content,
|
|
118
|
+
timestamp: row.timestamp,
|
|
119
|
+
snippet: row.snippet,
|
|
120
|
+
}));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
// FTS5 can throw on malformed queries — return empty results
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the total number of indexed messages.
|
|
129
|
+
*/
|
|
130
|
+
export function getIndexedMessageCount(dbManager: DatabaseManager): number {
|
|
131
|
+
const db = dbManager.getDb();
|
|
132
|
+
const result = db.prepare('SELECT COUNT(*) as count FROM messages').get() as { count: number };
|
|
133
|
+
return result.count;
|
|
134
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { DatabaseManager } from './db.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A memory entry stored in SQLite.
|
|
5
|
+
*/
|
|
6
|
+
export interface SqliteMemoryEntry {
|
|
7
|
+
id: number;
|
|
8
|
+
project: string | null;
|
|
9
|
+
target: 'memory' | 'user';
|
|
10
|
+
content: string;
|
|
11
|
+
created: string;
|
|
12
|
+
lastReferenced: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a memory entry to the SQLite store.
|
|
17
|
+
*/
|
|
18
|
+
export function addMemory(
|
|
19
|
+
dbManager: DatabaseManager,
|
|
20
|
+
content: string,
|
|
21
|
+
target: 'memory' | 'user' = 'memory',
|
|
22
|
+
project: string | null = null
|
|
23
|
+
): SqliteMemoryEntry {
|
|
24
|
+
const db = dbManager.getDb();
|
|
25
|
+
const today = new Date().toISOString().split('T')[0];
|
|
26
|
+
|
|
27
|
+
const result = db.prepare(`
|
|
28
|
+
INSERT INTO memories (project, target, content, created, last_referenced)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?)
|
|
30
|
+
`).run(project, target, content, today, today);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: Number(result.lastInsertRowid),
|
|
34
|
+
project,
|
|
35
|
+
target,
|
|
36
|
+
content,
|
|
37
|
+
created: today,
|
|
38
|
+
lastReferenced: today,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Escape a string for FTS5 query syntax.
|
|
44
|
+
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
45
|
+
*/
|
|
46
|
+
function escapeFts5Query(query: string): string {
|
|
47
|
+
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
48
|
+
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
49
|
+
return query;
|
|
50
|
+
}
|
|
51
|
+
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
52
|
+
return `"${query.replace(/"/g, '""')}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Search memories using FTS5.
|
|
57
|
+
*/
|
|
58
|
+
export function searchMemories(
|
|
59
|
+
dbManager: DatabaseManager,
|
|
60
|
+
query: string,
|
|
61
|
+
options: { project?: string; target?: string; limit?: number } = {}
|
|
62
|
+
): SqliteMemoryEntry[] {
|
|
63
|
+
const db = dbManager.getDb();
|
|
64
|
+
const { project, target, limit = 10 } = options;
|
|
65
|
+
|
|
66
|
+
const conditions: string[] = [];
|
|
67
|
+
const params: unknown[] = [];
|
|
68
|
+
|
|
69
|
+
// FTS5 match via subquery with escaped query
|
|
70
|
+
conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
|
|
71
|
+
params.push(escapeFts5Query(query));
|
|
72
|
+
|
|
73
|
+
if (project !== undefined) {
|
|
74
|
+
if (project === null) {
|
|
75
|
+
conditions.push('m.project IS NULL');
|
|
76
|
+
} else {
|
|
77
|
+
conditions.push('m.project = ?');
|
|
78
|
+
params.push(project);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (target) {
|
|
83
|
+
conditions.push('m.target = ?');
|
|
84
|
+
params.push(target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
88
|
+
|
|
89
|
+
const sql = `
|
|
90
|
+
SELECT id, project, target, content, created, last_referenced
|
|
91
|
+
FROM memories m
|
|
92
|
+
${whereClause}
|
|
93
|
+
ORDER BY m.last_referenced DESC
|
|
94
|
+
LIMIT ?
|
|
95
|
+
`;
|
|
96
|
+
params.push(limit);
|
|
97
|
+
|
|
98
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
99
|
+
id: number;
|
|
100
|
+
project: string | null;
|
|
101
|
+
target: string;
|
|
102
|
+
content: string;
|
|
103
|
+
created: string;
|
|
104
|
+
last_referenced: string;
|
|
105
|
+
}>;
|
|
106
|
+
|
|
107
|
+
return rows.map(row => ({
|
|
108
|
+
id: row.id,
|
|
109
|
+
project: row.project,
|
|
110
|
+
target: row.target as 'memory' | 'user',
|
|
111
|
+
content: row.content,
|
|
112
|
+
created: row.created,
|
|
113
|
+
lastReferenced: row.last_referenced,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all memories, optionally filtered.
|
|
119
|
+
*/
|
|
120
|
+
export function getMemories(
|
|
121
|
+
dbManager: DatabaseManager,
|
|
122
|
+
options: { project?: string | null; target?: string } = {}
|
|
123
|
+
): SqliteMemoryEntry[] {
|
|
124
|
+
const db = dbManager.getDb();
|
|
125
|
+
const { project, target } = options;
|
|
126
|
+
|
|
127
|
+
const conditions: string[] = [];
|
|
128
|
+
const params: unknown[] = [];
|
|
129
|
+
|
|
130
|
+
if (project !== undefined) {
|
|
131
|
+
if (project === null) {
|
|
132
|
+
conditions.push('project IS NULL');
|
|
133
|
+
} else {
|
|
134
|
+
conditions.push('project = ?');
|
|
135
|
+
params.push(project);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (target) {
|
|
140
|
+
conditions.push('target = ?');
|
|
141
|
+
params.push(target);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
145
|
+
|
|
146
|
+
const rows = db.prepare(`
|
|
147
|
+
SELECT id, project, target, content, created, last_referenced
|
|
148
|
+
FROM memories
|
|
149
|
+
${whereClause}
|
|
150
|
+
ORDER BY last_referenced DESC
|
|
151
|
+
`).all(...params) as Array<{
|
|
152
|
+
id: number;
|
|
153
|
+
project: string | null;
|
|
154
|
+
target: string;
|
|
155
|
+
content: string;
|
|
156
|
+
created: string;
|
|
157
|
+
last_referenced: string;
|
|
158
|
+
}>;
|
|
159
|
+
|
|
160
|
+
return rows.map(row => ({
|
|
161
|
+
id: row.id,
|
|
162
|
+
project: row.project,
|
|
163
|
+
target: row.target as 'memory' | 'user',
|
|
164
|
+
content: row.content,
|
|
165
|
+
created: row.created,
|
|
166
|
+
lastReferenced: row.last_referenced,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove a memory by ID.
|
|
172
|
+
*/
|
|
173
|
+
export function removeMemory(dbManager: DatabaseManager, id: number): boolean {
|
|
174
|
+
const db = dbManager.getDb();
|
|
175
|
+
const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
176
|
+
return result.changes > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Update a memory's last_referenced date.
|
|
181
|
+
*/
|
|
182
|
+
export function touchMemory(dbManager: DatabaseManager, id: number): void {
|
|
183
|
+
const db = dbManager.getDb();
|
|
184
|
+
const today = new Date().toISOString().split('T')[0];
|
|
185
|
+
db.prepare('UPDATE memories SET last_referenced = ? WHERE id = ?').run(today, id);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get memory statistics.
|
|
190
|
+
*/
|
|
191
|
+
export function getMemoryStats(dbManager: DatabaseManager): {
|
|
192
|
+
total: number;
|
|
193
|
+
byProject: { project: string | null; count: number }[];
|
|
194
|
+
byTarget: { target: string; count: number }[];
|
|
195
|
+
} {
|
|
196
|
+
const db = dbManager.getDb();
|
|
197
|
+
|
|
198
|
+
const total = (db.prepare('SELECT COUNT(*) as count FROM memories').get() as { count: number }).count;
|
|
199
|
+
|
|
200
|
+
const byProject = db.prepare(`
|
|
201
|
+
SELECT project, COUNT(*) as count
|
|
202
|
+
FROM memories
|
|
203
|
+
GROUP BY project
|
|
204
|
+
ORDER BY count DESC
|
|
205
|
+
`).all() as { project: string | null; count: number }[];
|
|
206
|
+
|
|
207
|
+
const byTarget = db.prepare(`
|
|
208
|
+
SELECT target, COUNT(*) as count
|
|
209
|
+
FROM memories
|
|
210
|
+
GROUP BY target
|
|
211
|
+
ORDER BY count DESC
|
|
212
|
+
`).all() as { target: string; count: number }[];
|
|
213
|
+
|
|
214
|
+
return { total, byProject, byTarget };
|
|
215
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
4
|
+
import { DatabaseManager } from '../store/db.js';
|
|
5
|
+
import { searchMemories, getMemoryStats } from '../store/sqlite-memory-store.js';
|
|
6
|
+
|
|
7
|
+
interface SearchResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
count?: number;
|
|
10
|
+
message?: string;
|
|
11
|
+
output?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerMemorySearchTool(pi: ExtensionAPI, dbManager: DatabaseManager): void {
|
|
15
|
+
pi.registerTool({
|
|
16
|
+
name: 'memory_search',
|
|
17
|
+
label: 'Memory Search',
|
|
18
|
+
description: `Search extended memory store for relevant entries. Use this when you need context beyond what's in the system prompt — the extended store has unlimited capacity and is searchable.
|
|
19
|
+
|
|
20
|
+
Use cases:
|
|
21
|
+
- Find memories about a specific topic: "What do I know about auth setup?"
|
|
22
|
+
- Search project-specific memories: "What conventions does project X follow?"
|
|
23
|
+
- Find user preferences: "What are the user's testing preferences?"
|
|
24
|
+
|
|
25
|
+
Returns matching memory entries with project context and dates.`,
|
|
26
|
+
promptSnippet: 'Search extended memory store (unlimited capacity)',
|
|
27
|
+
promptGuidelines: [
|
|
28
|
+
'Use memory_search when you need context beyond what is in the system prompt.',
|
|
29
|
+
'Use memory_search to find project-specific memories or user preferences.',
|
|
30
|
+
],
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
query: Type.String({ description: 'Search query. Use natural language or specific terms.' }),
|
|
33
|
+
project: Type.Optional(Type.String({ description: 'Filter by project name. Pass null for global memories only.' })),
|
|
34
|
+
target: Type.Optional(StringEnum(['memory', 'user'] as const, { description: 'Filter by target type (memory or user).' })),
|
|
35
|
+
limit: Type.Optional(Type.Number({ description: 'Maximum results to return (default: 10, max: 20).' })),
|
|
36
|
+
}),
|
|
37
|
+
execute: async (_id: string, args: { query: string; project?: string; target?: string; limit?: number }) => {
|
|
38
|
+
const query = args.query;
|
|
39
|
+
const project = args.project;
|
|
40
|
+
const target = args.target;
|
|
41
|
+
const limit = Math.min(args.limit || 10, 20);
|
|
42
|
+
|
|
43
|
+
if (!query || query.trim().length === 0) {
|
|
44
|
+
const result: SearchResult = { success: false, message: 'query is required' };
|
|
45
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stats = getMemoryStats(dbManager);
|
|
49
|
+
if (stats.total === 0) {
|
|
50
|
+
const result: SearchResult = { success: false, message: 'No memories in extended store yet. Use the memory tool with add action to store memories.' };
|
|
51
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const results = searchMemories(dbManager, query, { project, target, limit });
|
|
55
|
+
|
|
56
|
+
if (results.length === 0) {
|
|
57
|
+
const result: SearchResult = { success: true, count: 0, message: `No memories found matching "${query}". Try a different search term or broader query.` };
|
|
58
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let output = `Found ${results.length} memories matching "${query}":\n\n`;
|
|
62
|
+
|
|
63
|
+
for (const entry of results) {
|
|
64
|
+
const projectLabel = entry.project ? `[${entry.project}]` : '[global]';
|
|
65
|
+
const targetLabel = entry.target === 'user' ? '👤' : '🧠';
|
|
66
|
+
output += `${targetLabel} ${projectLabel} ${entry.content}\n`;
|
|
67
|
+
output += ` Created: ${entry.created} | Last used: ${entry.lastReferenced}\n\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const finalResult: SearchResult = { success: true, count: results.length, output: output.trim() };
|
|
71
|
+
return { content: [{ type: 'text' as const, text: output.trim() }], details: finalResult };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
4
|
+
import { DatabaseManager } from '../store/db.js';
|
|
5
|
+
import { searchSessions, getIndexedMessageCount } from '../store/session-search.js';
|
|
6
|
+
|
|
7
|
+
interface SearchResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
count?: number;
|
|
10
|
+
message?: string;
|
|
11
|
+
output?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerSessionSearchTool(pi: ExtensionAPI, dbManager: DatabaseManager): void {
|
|
15
|
+
pi.registerTool({
|
|
16
|
+
name: 'session_search',
|
|
17
|
+
label: 'Session Search',
|
|
18
|
+
description: `Search across past Pi coding sessions for relevant conversation context. Use this when the user asks about previous discussions, past work, or when you need context from earlier sessions.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
- "What did we discuss about auth last week?"
|
|
22
|
+
- "Find the PR where we fixed the test hang"
|
|
23
|
+
- "What approach did we take for the database migration?"
|
|
24
|
+
|
|
25
|
+
Returns conversation snippets with session dates and project context.`,
|
|
26
|
+
promptSnippet: 'Search past conversations for relevant context',
|
|
27
|
+
promptGuidelines: [
|
|
28
|
+
'Use session_search when the user asks about previous discussions or past work.',
|
|
29
|
+
'Use session_search when you need context from earlier sessions.',
|
|
30
|
+
],
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
query: Type.String({ description: 'Search query. Use natural language or specific terms.' }),
|
|
33
|
+
project: Type.Optional(Type.String({ description: 'Filter by project name (optional).' })),
|
|
34
|
+
role: Type.Optional(StringEnum(['user', 'assistant'] as const, { description: 'Filter by message role (optional).' })),
|
|
35
|
+
limit: Type.Optional(Type.Number({ description: 'Maximum results to return (default: 10, max: 20).' })),
|
|
36
|
+
}),
|
|
37
|
+
execute: async (_id: string, args: { query: string; project?: string; role?: string; limit?: number }) => {
|
|
38
|
+
const query = args.query;
|
|
39
|
+
const project = args.project;
|
|
40
|
+
const role = args.role;
|
|
41
|
+
const limit = Math.min(args.limit || 10, 20);
|
|
42
|
+
|
|
43
|
+
if (!query || query.trim().length === 0) {
|
|
44
|
+
const result: SearchResult = { success: false, message: 'query is required' };
|
|
45
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const totalMessages = getIndexedMessageCount(dbManager);
|
|
49
|
+
if (totalMessages === 0) {
|
|
50
|
+
const result: SearchResult = { success: false, message: 'No sessions indexed yet. Run /memory-index-sessions to import past sessions.' };
|
|
51
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const results = searchSessions(dbManager, query, { project, role, limit });
|
|
55
|
+
|
|
56
|
+
if (results.length === 0) {
|
|
57
|
+
const result: SearchResult = { success: true, count: 0, message: `No results found for "${query}". Try a different search term or broader query.` };
|
|
58
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let output = `Found ${results.length} results for "${query}":\n\n`;
|
|
62
|
+
|
|
63
|
+
for (const r of results) {
|
|
64
|
+
const date = new Date(r.timestamp).toLocaleDateString('en-US', {
|
|
65
|
+
year: 'numeric',
|
|
66
|
+
month: 'short',
|
|
67
|
+
day: 'numeric',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
output += `---\n`;
|
|
71
|
+
output += `📅 ${date} | 📁 ${r.project} | ${r.role === 'user' ? '👤 User' : '🤖 Assistant'}\n`;
|
|
72
|
+
output += `${r.snippet}\n\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const finalResult: SearchResult = { success: true, count: results.length, output: output.trim() };
|
|
76
|
+
return { content: [{ type: 'text' as const, text: output.trim() }], details: finalResult };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import type { TextContent } from "@mariozechner/pi-ai";
|
|
6
6
|
|
|
7
7
|
export interface MemoryConfig {
|
|
8
|
-
/** Max chars for MEMORY.md (agent notes). Default:
|
|
8
|
+
/** Max chars for MEMORY.md (agent notes). Default: 5000 */
|
|
9
9
|
memoryCharLimit: number;
|
|
10
|
-
/** Max chars for USER.md (user profile). Default:
|
|
10
|
+
/** Max chars for USER.md (user profile). Default: 5000 */
|
|
11
11
|
userCharLimit: number;
|
|
12
|
-
/** Max chars for project-level MEMORY.md. Default:
|
|
12
|
+
/** Max chars for project-level MEMORY.md. Default: 5000 */
|
|
13
13
|
projectCharLimit: number;
|
|
14
14
|
/** Turns between background auto-reviews. Default: 10 */
|
|
15
15
|
nudgeInterval: number;
|
|
@@ -29,6 +29,10 @@ export interface MemoryConfig {
|
|
|
29
29
|
correctionDetection: boolean;
|
|
30
30
|
/** Tool calls before triggering background review (in addition to turn count). Default: 15 */
|
|
31
31
|
nudgeToolCalls: number;
|
|
32
|
+
/** Enable session history search via SQLite FTS5. Default: true */
|
|
33
|
+
sessionSearchEnabled?: boolean;
|
|
34
|
+
/** Days to retain session history. Default: 90 */
|
|
35
|
+
sessionRetentionDays?: number;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export interface MemoryResult {
|