pi-hermes-memory 0.7.15 → 0.7.18

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.
@@ -1,5 +1,5 @@
1
1
  import { DatabaseManager } from './db.js';
2
- import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
2
+ import { buildFallbackFts5Query, hasExplicitFts5Operator, isFts5QueryError, normalizeFts5Query } from './fts-query.js';
3
3
 
4
4
  /**
5
5
  * Search result from session history.
@@ -27,6 +27,52 @@ export interface SessionSearchOptions {
27
27
  since?: string;
28
28
  }
29
29
 
30
+ type SearchMatch =
31
+ | { type: 'fts'; query: string }
32
+ | { type: 'like'; terms: string[] };
33
+
34
+ const QUERY_TOKEN_PATTERN = /"([^"]*)"|(\S+)/g;
35
+ const NATURAL_LANGUAGE_CONNECTORS = new Set(['and', 'or', 'not', 'near']);
36
+
37
+ function escapeLikePattern(text: string): string {
38
+ return text.replace(/[\\%_]/g, '\\$&');
39
+ }
40
+
41
+ function collectLikeTerms(query: string): string[] {
42
+ const terms: string[] = [];
43
+
44
+ for (const match of query.matchAll(QUERY_TOKEN_PATTERN)) {
45
+ const phrase = match[1];
46
+ const term = match[2];
47
+ if (phrase === undefined && term && NATURAL_LANGUAGE_CONNECTORS.has(term.toLowerCase())) {
48
+ continue;
49
+ }
50
+
51
+ const rawValue = phrase ?? term ?? '';
52
+ if (rawValue.length > 0) terms.push(rawValue);
53
+ }
54
+
55
+ return terms;
56
+ }
57
+
58
+ function mapRows(rows: Array<{
59
+ session_id: string;
60
+ project: string;
61
+ role: string;
62
+ content: string;
63
+ timestamp: string;
64
+ snippet: string;
65
+ }>): SessionSearchResult[] {
66
+ return rows.map(row => ({
67
+ sessionId: row.session_id,
68
+ project: row.project,
69
+ role: row.role,
70
+ content: row.content,
71
+ timestamp: row.timestamp,
72
+ snippet: row.snippet,
73
+ }));
74
+ }
75
+
30
76
  /**
31
77
  * Search across indexed session messages using FTS5.
32
78
  *
@@ -47,79 +93,104 @@ export function searchSessions(
47
93
  const db = dbManager.getDb();
48
94
  const { limit = 10, project, role, since } = options;
49
95
 
50
- // Build the query dynamically based on filters
51
- const conditions: string[] = [];
52
- const params: unknown[] = [];
96
+ const executeSearch = (match: SearchMatch): SessionSearchResult[] => {
97
+ const conditions: string[] = [];
98
+ const params: unknown[] = [];
99
+
100
+ if (match.type === 'fts') {
101
+ // FTS5 match condition — use subquery for reliable rowid matching
102
+ conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
103
+ params.push(match.query);
104
+ } else {
105
+ if (match.terms.length === 0) {
106
+ return [];
107
+ }
108
+ const likeConditions = match.terms.map(() => `m.content LIKE ? ESCAPE '\\'`);
109
+ conditions.push(`(${likeConditions.join(' OR ')})`);
110
+ for (const term of match.terms) {
111
+ params.push(`%${escapeLikePattern(term)}%`);
112
+ }
113
+ }
114
+
115
+ // Project filter
116
+ if (project) {
117
+ conditions.push('s.project = ?');
118
+ params.push(project);
119
+ }
120
+
121
+ // Role filter
122
+ if (role) {
123
+ conditions.push('m.role = ?');
124
+ params.push(role);
125
+ }
126
+
127
+ // Date filter
128
+ if (since) {
129
+ conditions.push('m.timestamp >= ?');
130
+ params.push(since);
131
+ }
132
+
133
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
134
+
135
+ const sql = `
136
+ SELECT
137
+ m.session_id,
138
+ s.project,
139
+ m.role,
140
+ m.content,
141
+ m.timestamp,
142
+ m.content as snippet
143
+ FROM messages m
144
+ JOIN sessions s ON s.id = m.session_id
145
+ ${whereClause}
146
+ ORDER BY m.timestamp DESC
147
+ LIMIT ?
148
+ `;
149
+
150
+ try {
151
+ const rows = db.prepare(sql).all(...params, limit) as Array<{
152
+ session_id: string;
153
+ project: string;
154
+ role: string;
155
+ content: string;
156
+ timestamp: string;
157
+ snippet: string;
158
+ }>;
159
+
160
+ return mapRows(rows);
161
+ } catch (err) {
162
+ if (match.type === 'fts' && isFts5QueryError(err)) {
163
+ return [];
164
+ }
165
+ throw err;
166
+ }
167
+ };
53
168
 
54
- // FTS5 match condition — use subquery for reliable rowid matching
55
169
  const normalizedQuery = normalizeFts5Query(query);
56
170
  if (normalizedQuery.length === 0) {
57
171
  return [];
58
172
  }
59
- conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
60
- params.push(normalizedQuery);
61
-
62
- // Project filter
63
- if (project) {
64
- conditions.push('s.project = ?');
65
- params.push(project);
66
- }
67
173
 
68
- // Role filter
69
- if (role) {
70
- conditions.push('m.role = ?');
71
- params.push(role);
174
+ const exactResults = executeSearch({ type: 'fts', query: normalizedQuery });
175
+ if (exactResults.length > 0) {
176
+ return exactResults;
72
177
  }
73
178
 
74
- // Date filter
75
- if (since) {
76
- conditions.push('m.timestamp >= ?');
77
- params.push(since);
179
+ const explicitOperatorQuery = hasExplicitFts5Operator(query);
180
+ if (explicitOperatorQuery) {
181
+ return exactResults;
78
182
  }
79
183
 
80
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
81
-
82
- const sql = `
83
- SELECT
84
- m.session_id,
85
- s.project,
86
- m.role,
87
- m.content,
88
- m.timestamp,
89
- m.content as snippet
90
- FROM messages m
91
- JOIN sessions s ON s.id = m.session_id
92
- ${whereClause}
93
- ORDER BY m.timestamp DESC
94
- LIMIT ?
95
- `;
96
- params.push(limit);
97
-
98
- try {
99
- const rows = db.prepare(sql).all(...params) as Array<{
100
- session_id: string;
101
- project: string;
102
- role: string;
103
- content: string;
104
- timestamp: string;
105
- snippet: string;
106
- }>;
107
-
108
- // Map snake_case column names to camelCase
109
- return rows.map(row => ({
110
- sessionId: row.session_id,
111
- project: row.project,
112
- role: row.role,
113
- content: row.content,
114
- timestamp: row.timestamp,
115
- snippet: row.snippet,
116
- }));
117
- } catch (err) {
118
- if (isFts5QueryError(err)) {
119
- return [];
184
+ const fallbackQuery = buildFallbackFts5Query(query);
185
+ if (fallbackQuery && fallbackQuery !== normalizedQuery) {
186
+ const fallbackResults = executeSearch({ type: 'fts', query: fallbackQuery });
187
+ if (fallbackResults.length > 0) {
188
+ return fallbackResults;
120
189
  }
121
- throw err;
122
190
  }
191
+
192
+ const likeTerms = collectLikeTerms(query);
193
+ return executeSearch({ type: 'like', terms: likeTerms });
123
194
  }
124
195
 
125
196
  /**
@@ -7,7 +7,6 @@
7
7
 
8
8
  import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
- import * as os from "node:os";
11
10
  import { scanContent } from "./content-scanner.js";
12
11
  import {
13
12
  buildSkillId,
@@ -21,6 +20,7 @@ import {
21
20
  tokenizeForSimilarity,
22
21
  } from "./skill-utils.js";
23
22
  import type { SkillDocument, SkillIndex, SkillResult, SkillScope } from "../types.js";
23
+ import { AGENT_ROOT } from "../paths.js";
24
24
 
25
25
  interface SkillStoreOptions {
26
26
  globalSkillsDir?: string;
@@ -55,7 +55,7 @@ export class SkillStore {
55
55
  private migrationSentinelPath: string;
56
56
 
57
57
  constructor(options: SkillStoreOptions = {}) {
58
- const agentRoot = path.join(os.homedir(), ".pi", "agent");
58
+ const agentRoot = AGENT_ROOT;
59
59
  this.globalSkillsDir = options.globalSkillsDir ?? path.join(agentRoot, "skills");
60
60
  this.projectSkillsDir = options.projectSkillsDir ?? null;
61
61
  this.projectName = options.projectName ?? null;
@@ -1,5 +1,4 @@
1
1
  import * as path from 'node:path';
2
- import * as os from 'node:os';
3
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
3
  import { Type } from "typebox";
5
4
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -8,6 +7,7 @@ import { searchSessions, getIndexedMessageCount } from '../store/session-search.
8
7
  import { searchSessionAnchors } from '../store/session-anchor-search.js';
9
8
  import type { SessionAnchorRange, SessionAnchorSearchResult } from '../store/session-anchor-search.js';
10
9
  import type { SessionSearchConfig } from '../types.js';
10
+ import { AGENT_ROOT } from '../paths.js';
11
11
 
12
12
  interface SearchResult {
13
13
  success: boolean;
@@ -21,7 +21,7 @@ interface SessionSearchToolOptions {
21
21
  sessionsDir?: string;
22
22
  }
23
23
 
24
- const DEFAULT_SESSIONS_DIR = path.join(os.homedir(), '.pi', 'agent', 'sessions');
24
+ const DEFAULT_SESSIONS_DIR = path.join(AGENT_ROOT, 'sessions');
25
25
 
26
26
  export function registerSessionSearchTool(
27
27
  pi: ExtensionAPI,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Skill tool — registers the LLM-callable `skill` tool for procedural memory.
2
+ * Skill manager tool — registers the LLM-callable `skill_manage` tool for procedural memory.
3
3
  * Complements the `memory` tool (declarative knowledge) with procedural knowledge.
4
4
  */
5
5
 
@@ -85,14 +85,16 @@ const SKILL_TOOL_PARAMETERS = Type.Object({
85
85
  })),
86
86
  }, { additionalProperties: false });
87
87
 
88
+ export const SKILL_MANAGE_TOOL_NAME = "skill_manage";
89
+
88
90
  export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
89
91
  pi.registerTool({
90
- name: "skill",
91
- label: "Skill",
92
+ name: SKILL_MANAGE_TOOL_NAME,
93
+ label: "Skill Manager",
92
94
  description: SKILL_TOOL_DESCRIPTION,
93
- promptSnippet: "Save or manage reusable procedures and patterns",
95
+ promptSnippet: "Create, inspect, and update reusable procedures and patterns",
94
96
  promptGuidelines: [
95
- "Use the skill tool after completing complex tasks that required trial and error or multiple tool calls.",
97
+ "Use the skill_manage tool after completing complex tasks that required trial and error or multiple tool calls.",
96
98
  "Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id, and 'update' for a full rewrite.",
97
99
  "Scope is required on create: choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
98
100
  "Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool will render valid SKILL.md sections for you.",