pi-hermes-memory 0.7.14 → 0.7.15
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/package.json +1 -1
- package/src/store/fts-query.ts +38 -10
- package/src/store/sqlite-memory-store.ts +65 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "🧠 Persistent memory + 🔍 session search + 🛡️ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills. 368 tests. Ported from Hermes agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/store/fts-query.ts
CHANGED
|
@@ -2,6 +2,23 @@ const FTS5_OPERATOR_PATTERN = /\b(OR|AND|NOT|NEAR)\b/;
|
|
|
2
2
|
const FTS5_TOKEN_PATTERN = /"([^"]*)"|(\S+)/g;
|
|
3
3
|
const NATURAL_LANGUAGE_CONNECTORS = new Set(['and', 'or', 'not', 'near']);
|
|
4
4
|
|
|
5
|
+
function collectNaturalLanguageTerms(query: string): string[] {
|
|
6
|
+
const terms: string[] = [];
|
|
7
|
+
|
|
8
|
+
for (const match of query.matchAll(FTS5_TOKEN_PATTERN)) {
|
|
9
|
+
const phrase = match[1];
|
|
10
|
+
const term = match[2];
|
|
11
|
+
if (phrase === undefined && term && NATURAL_LANGUAGE_CONNECTORS.has(term.toLowerCase())) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rawValue = phrase ?? term ?? '';
|
|
16
|
+
if (rawValue.length > 0) terms.push(rawValue);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return terms;
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
/**
|
|
6
23
|
* Normalize natural-language search input into an FTS5 query.
|
|
7
24
|
* Plain terms become individually quoted for implicit AND matching.
|
|
@@ -16,19 +33,30 @@ export function normalizeFts5Query(query: string): string {
|
|
|
16
33
|
return trimmed;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (phrase === undefined && term && NATURAL_LANGUAGE_CONNECTORS.has(term.toLowerCase())) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
36
|
+
return collectNaturalLanguageTerms(trimmed)
|
|
37
|
+
.map((term) => `"${term.replace(/"/g, '""')}"`)
|
|
38
|
+
.join(' ');
|
|
39
|
+
}
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Build a broader fallback query for natural-language searches.
|
|
43
|
+
* Returns null for explicit operator queries or when the input is already a
|
|
44
|
+
* single searchable term.
|
|
45
|
+
*/
|
|
46
|
+
export function buildFallbackFts5Query(query: string): string | null {
|
|
47
|
+
const trimmed = query.trim();
|
|
48
|
+
if (trimmed.length === 0 || FTS5_OPERATOR_PATTERN.test(trimmed)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const terms = collectNaturalLanguageTerms(trimmed);
|
|
53
|
+
if (terms.length <= 1) {
|
|
54
|
+
return null;
|
|
29
55
|
}
|
|
30
56
|
|
|
31
|
-
return
|
|
57
|
+
return terms
|
|
58
|
+
.map((term) => `"${term.replace(/"/g, '""')}"`)
|
|
59
|
+
.join(' OR ');
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
export function isFts5QueryError(err: unknown): boolean {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
-
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
2
|
+
import { buildFallbackFts5Query, isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
3
3
|
import { normalizeMemoryLookupText } from './memory-lookup.js';
|
|
4
4
|
import type { MemoryCategory } from '../types.js';
|
|
5
5
|
|
|
@@ -579,60 +579,77 @@ export function searchMemories(
|
|
|
579
579
|
if (normalizedQuery.length === 0) {
|
|
580
580
|
return [];
|
|
581
581
|
}
|
|
582
|
-
conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
|
|
583
|
-
params.push(normalizedQuery);
|
|
584
582
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
} else {
|
|
589
|
-
conditions.push('m.project = ?');
|
|
590
|
-
params.push(project);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
583
|
+
const runSearch = (matchQuery: string): SqliteMemoryEntry[] => {
|
|
584
|
+
const conditions: string[] = [];
|
|
585
|
+
const params: unknown[] = [];
|
|
593
586
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
params.push(target);
|
|
597
|
-
}
|
|
587
|
+
conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
|
|
588
|
+
params.push(matchQuery);
|
|
598
589
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
590
|
+
if (project !== undefined) {
|
|
591
|
+
if (project === null) {
|
|
592
|
+
conditions.push('m.project IS NULL');
|
|
593
|
+
} else {
|
|
594
|
+
conditions.push('m.project = ?');
|
|
595
|
+
params.push(project);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
603
598
|
|
|
604
|
-
|
|
599
|
+
if (target) {
|
|
600
|
+
conditions.push('m.target = ?');
|
|
601
|
+
params.push(target);
|
|
602
|
+
}
|
|
605
603
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
604
|
+
if (category) {
|
|
605
|
+
conditions.push('m.category = ?');
|
|
606
|
+
params.push(category);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
610
|
+
|
|
611
|
+
const sql = `
|
|
612
|
+
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
613
|
+
FROM memories m
|
|
614
|
+
${whereClause}
|
|
615
|
+
ORDER BY m.last_referenced DESC
|
|
616
|
+
LIMIT ?
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const rows = db.prepare(sql).all(...params, limit) as Array<{
|
|
621
|
+
id: number;
|
|
622
|
+
project: string | null;
|
|
623
|
+
target: string;
|
|
624
|
+
category: string | null;
|
|
625
|
+
content: string;
|
|
626
|
+
failure_reason: string | null;
|
|
627
|
+
tool_state: string | null;
|
|
628
|
+
corrected_to: string | null;
|
|
629
|
+
created: string;
|
|
630
|
+
last_referenced: string;
|
|
631
|
+
}>;
|
|
632
|
+
|
|
633
|
+
return rows.map(mapRow);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (isFts5QueryError(err)) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
throw err;
|
|
633
639
|
}
|
|
634
|
-
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const exactResults = runSearch(normalizedQuery);
|
|
643
|
+
if (exactResults.length > 0) {
|
|
644
|
+
return exactResults;
|
|
635
645
|
}
|
|
646
|
+
|
|
647
|
+
const fallbackQuery = buildFallbackFts5Query(query);
|
|
648
|
+
if (!fallbackQuery || fallbackQuery === normalizedQuery) {
|
|
649
|
+
return exactResults;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return runSearch(fallbackQuery);
|
|
636
653
|
}
|
|
637
654
|
|
|
638
655
|
/**
|