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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hermes-memory",
3
- "version": "0.7.14",
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",
@@ -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
- const normalizedTerms: string[] = [];
20
- for (const match of trimmed.matchAll(FTS5_TOKEN_PATTERN)) {
21
- const phrase = match[1];
22
- const term = match[2];
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
- const rawValue = phrase ?? term ?? '';
28
- normalizedTerms.push(`"${rawValue.replace(/"/g, '""')}"`);
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 normalizedTerms.join(' ');
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
- if (project !== undefined) {
586
- if (project === null) {
587
- conditions.push('m.project IS NULL');
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
- if (target) {
595
- conditions.push('m.target = ?');
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
- if (category) {
600
- conditions.push('m.category = ?');
601
- params.push(category);
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
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
599
+ if (target) {
600
+ conditions.push('m.target = ?');
601
+ params.push(target);
602
+ }
605
603
 
606
- const sql = `
607
- SELECT ${MEMORY_SELECT_COLUMNS}
608
- FROM memories m
609
- ${whereClause}
610
- ORDER BY m.last_referenced DESC
611
- LIMIT ?
612
- `;
613
- params.push(limit);
614
-
615
- try {
616
- const rows = db.prepare(sql).all(...params) as Array<{
617
- id: number;
618
- project: string | null;
619
- target: string;
620
- category: string | null;
621
- content: string;
622
- failure_reason: string | null;
623
- tool_state: string | null;
624
- corrected_to: string | null;
625
- created: string;
626
- last_referenced: string;
627
- }>;
628
-
629
- return rows.map(mapRow);
630
- } catch (err) {
631
- if (isFts5QueryError(err)) {
632
- return [];
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
- throw err;
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
  /**