pi-hermes-memory 0.7.0 โ†’ 0.7.2

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 CHANGED
@@ -412,6 +412,8 @@ Create `~/.pi/agent/hermes-memory-config.json`:
412
412
 
413
413
  These are plain markdown files. You can read and edit them directly if you want to curate what the agent remembers. Memory entries are separated by `ยง` (section sign). Skills use standard SKILL.md format with frontmatter.
414
414
 
415
+ If you are upgrading from a version that stored project memory directly at `~/.pi/agent/<project>/MEMORY.md`, the extension copies or merges those entries into `~/.pi/agent/projects-memory/<project>/MEMORY.md` on startup. The old folders are left in place as a backup.
416
+
415
417
  The `sessions.db` SQLite database stores session history and extended memory entries. It's searchable via FTS5 full-text search.
416
418
 
417
419
  ## Known Limitations
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-hermes-memory",
3
- "version": "0.7.0",
4
- "description": "๐Ÿง  Persistent memory + ๐Ÿ” session search + ๐Ÿ›ก๏ธ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills. 362 tests. Ported from Hermes agent.",
3
+ "version": "0.7.2",
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",
7
7
  "files": [
@@ -78,6 +78,7 @@ export function setupCorrectionDetector(
78
78
  projectStore: MemoryStore | null,
79
79
  config: MemoryConfig,
80
80
  dbManager: DatabaseManager | null = null,
81
+ projectName?: string | null,
81
82
  ): void {
82
83
  if (!config.correctionDetection) return;
83
84
 
@@ -174,11 +175,11 @@ export function setupCorrectionDetector(
174
175
  if (correctionText) {
175
176
  const directive = extractCorrectionDirective(correctionText);
176
177
  const failureReason = "User corrected the agent";
177
- const projectMarker = projectStore ? "project" : undefined;
178
+ const scopedProjectName = projectStore ? projectName?.trim() || null : null;
178
179
  const addResult = await store.addFailure(directive, {
179
180
  category: "correction",
180
181
  failureReason,
181
- project: projectMarker,
182
+ project: scopedProjectName ?? undefined,
182
183
  });
183
184
 
184
185
  if (addResult.success && dbManager) {
@@ -187,9 +188,10 @@ export function setupCorrectionDetector(
187
188
  content: formatFailureMemoryContent(directive, {
188
189
  category: "correction",
189
190
  failureReason,
190
- project: projectMarker,
191
+ project: scopedProjectName,
191
192
  }),
192
193
  target: "failure",
194
+ project: scopedProjectName,
193
195
  category: "correction",
194
196
  failureReason,
195
197
  });
@@ -13,7 +13,7 @@ import {
13
13
  } from '../store/sqlite-memory-store.js';
14
14
  import { ENTRY_DELIMITER, MEMORY_FILE, USER_FILE } from '../constants.js';
15
15
 
16
- interface BackfillCounters {
16
+ export interface BackfillCounters {
17
17
  filesScanned: number;
18
18
  entriesScanned: number;
19
19
  imported: number;
@@ -52,15 +52,77 @@ function importEntries(
52
52
 
53
53
  function scanProjectDirs(agentRoot: string, globalDir: string, projectsMemoryDir = "projects-memory"): Array<{ name: string; memoryFile: string }> {
54
54
  const projectsRoot = path.join(agentRoot, projectsMemoryDir);
55
- if (!fs.existsSync(projectsRoot)) return [];
55
+ const projects = new Map<string, string>();
56
+
57
+ if (fs.existsSync(projectsRoot)) {
58
+ for (const name of fs.readdirSync(projectsRoot)) {
59
+ const dir = path.join(projectsRoot, name);
60
+ const memoryFile = path.join(dir, MEMORY_FILE);
61
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
62
+ projects.set(name, memoryFile);
63
+ }
64
+ }
65
+ }
66
+
67
+ const globalDirName = path.basename(globalDir);
68
+ if (fs.existsSync(agentRoot)) {
69
+ for (const name of fs.readdirSync(agentRoot)) {
70
+ if (name === globalDirName || name === projectsMemoryDir || name === 'skills' || name.startsWith('.')) continue;
71
+ if (projects.has(name)) continue;
72
+ const dir = path.join(agentRoot, name);
73
+ const memoryFile = path.join(dir, MEMORY_FILE);
74
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
75
+ projects.set(name, memoryFile);
76
+ }
77
+ }
78
+ }
56
79
 
57
- return fs.readdirSync(projectsRoot)
58
- .map((name) => ({ name, dir: path.join(projectsRoot, name) }))
59
- .filter(({ dir }) => fs.existsSync(dir) && fs.statSync(dir).isDirectory())
60
- .map(({ name, dir }) => ({ name, memoryFile: path.join(dir, MEMORY_FILE) }))
80
+ return [...projects.entries()]
81
+ .map(([name, memoryFile]) => ({ name, memoryFile }))
61
82
  .filter(({ memoryFile }) => fs.existsSync(memoryFile));
62
83
  }
63
84
 
85
+ export function syncMarkdownMemoriesToSqlite(
86
+ dbManager: DatabaseManager,
87
+ globalDir: string,
88
+ projectsMemoryDir?: string,
89
+ ): BackfillCounters & { projectCount: number } {
90
+ const counters: BackfillCounters = {
91
+ filesScanned: 0,
92
+ entriesScanned: 0,
93
+ imported: 0,
94
+ skipped: 0,
95
+ warnings: [],
96
+ };
97
+
98
+ const globalMemoryFile = path.join(globalDir, MEMORY_FILE);
99
+ const globalUserFile = path.join(globalDir, USER_FILE);
100
+ const globalFailureFile = path.join(globalDir, 'failures.md');
101
+
102
+ const importFile = (
103
+ filePath: string,
104
+ target: 'memory' | 'user' | 'failure',
105
+ project: string | null = null,
106
+ ) => {
107
+ if (!fs.existsSync(filePath)) return;
108
+ counters.filesScanned++;
109
+ const entries = readEntries(filePath);
110
+ importEntries(dbManager, counters, entries, target, project);
111
+ };
112
+
113
+ importFile(globalMemoryFile, 'memory');
114
+ importFile(globalUserFile, 'user');
115
+ importFile(globalFailureFile, 'failure');
116
+
117
+ const agentRoot = path.dirname(globalDir);
118
+ const projects = scanProjectDirs(agentRoot, globalDir, projectsMemoryDir);
119
+ for (const project of projects) {
120
+ importFile(project.memoryFile, 'memory', project.name);
121
+ }
122
+
123
+ return { ...counters, projectCount: projects.length };
124
+ }
125
+
64
126
  export function registerSyncMarkdownMemoriesCommand(
65
127
  pi: ExtensionAPI,
66
128
  dbManager: DatabaseManager,
@@ -70,41 +132,10 @@ export function registerSyncMarkdownMemoriesCommand(
70
132
  pi.registerCommand('memory-sync-markdown', {
71
133
  description: 'Backfill Markdown memories into the SQLite search store',
72
134
  handler: async (_args, ctx: ExtensionCommandContext) => {
73
- const counters: BackfillCounters = {
74
- filesScanned: 0,
75
- entriesScanned: 0,
76
- imported: 0,
77
- skipped: 0,
78
- warnings: [],
79
- };
80
-
81
135
  ctx.ui.notify('๐Ÿ”„ Scanning Markdown memory files for SQLite backfill...', 'info');
82
136
 
83
137
  try {
84
- const globalMemoryFile = path.join(globalDir, MEMORY_FILE);
85
- const globalUserFile = path.join(globalDir, USER_FILE);
86
- const globalFailureFile = path.join(globalDir, 'failures.md');
87
-
88
- const importFile = (
89
- filePath: string,
90
- target: 'memory' | 'user' | 'failure',
91
- project: string | null = null,
92
- ) => {
93
- if (!fs.existsSync(filePath)) return;
94
- counters.filesScanned++;
95
- const entries = readEntries(filePath);
96
- importEntries(dbManager, counters, entries, target, project);
97
- };
98
-
99
- importFile(globalMemoryFile, 'memory');
100
- importFile(globalUserFile, 'user');
101
- importFile(globalFailureFile, 'failure');
102
-
103
- const agentRoot = path.dirname(globalDir);
104
- const projects = scanProjectDirs(agentRoot, globalDir, projectsMemoryDir);
105
- for (const project of projects) {
106
- importFile(project.memoryFile, 'memory', project.name);
107
- }
138
+ const counters = syncMarkdownMemoriesToSqlite(dbManager, globalDir, projectsMemoryDir);
108
139
 
109
140
  let output = `\nโœ… Markdown โ†’ SQLite sync complete!\n\n`;
110
141
  output += `๐Ÿ“Š Results:\n`;
@@ -113,8 +144,8 @@ export function registerSyncMarkdownMemoriesCommand(
113
144
  output += `โ”œโ”€ Imported into SQLite: ${counters.imported}\n`;
114
145
  output += `โ””โ”€ Skipped as duplicates: ${counters.skipped}\n`;
115
146
 
116
- if (projects.length > 0) {
117
- output += `\n๐Ÿ“ Project memories scanned: ${projects.length}\n`;
147
+ if (counters.projectCount > 0) {
148
+ output += `\n๐Ÿ“ Project memories scanned: ${counters.projectCount}\n`;
118
149
  }
119
150
 
120
151
  if (counters.warnings.length > 0) {
package/src/index.ts CHANGED
@@ -45,11 +45,12 @@ import { registerInterviewCommand } from "./handlers/interview.js";
45
45
  import { registerSwitchProjectCommand } from "./handlers/switch-project.js";
46
46
  import { registerIndexSessionsCommand } from "./handlers/index-sessions.js";
47
47
  import { registerLearnMemoryCommand } from "./handlers/learn-memory.js";
48
- import { registerSyncMarkdownMemoriesCommand } from "./handlers/sync-markdown-memories.js";
48
+ import { registerSyncMarkdownMemoriesCommand, syncMarkdownMemoriesToSqlite } from "./handlers/sync-markdown-memories.js";
49
49
  import { registerPreviewContextCommand } from "./handlers/preview-context.js";
50
50
  import { loadConfig } from "./config.js";
51
51
  import { detectProject } from "./project.js";
52
52
  import { buildPromptContext } from "./prompt-context.js";
53
+ import { migrateLegacyProjectMemoryDirs } from "./project-memory-migration.js";
53
54
 
54
55
  export default function (pi: ExtensionAPI) {
55
56
  const config = loadConfig();
@@ -59,6 +60,16 @@ export default function (pi: ExtensionAPI) {
59
60
  const skillStore = new SkillStore(path.join(globalDir, "skills"));
60
61
  const dbManager = new DatabaseManager(globalDir);
61
62
 
63
+ // Keep project memory available for users upgrading from the old
64
+ // ~/.pi/agent/<project>/ layout. This is non-destructive: legacy folders
65
+ // remain in place while entries are copied/merged into projects-memory/.
66
+ migrateLegacyProjectMemoryDirs(globalDir, config.projectsMemoryDir);
67
+ try {
68
+ syncMarkdownMemoriesToSqlite(dbManager, globalDir, config.projectsMemoryDir);
69
+ } catch {
70
+ // Best-effort only: failed SQLite backfill should not block extension startup.
71
+ }
72
+
62
73
  // Detect project from cwd using shared helper
63
74
  const project = detectProject(config.projectsMemoryDir);
64
75
 
@@ -105,7 +116,7 @@ export default function (pi: ExtensionAPI) {
105
116
  registerConsolidateCommand(pi, store);
106
117
 
107
118
  // โ”€โ”€ 8. Setup correction detection โ”€โ”€
108
- setupCorrectionDetector(pi, store, projectStore, config, dbManager);
119
+ setupCorrectionDetector(pi, store, projectStore, config, dbManager, projectName);
109
120
 
110
121
  // โ”€โ”€ 9. Setup skill auto-trigger โ”€โ”€
111
122
  setupSkillAutoTrigger(pi, store, skillStore, config);
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ENTRY_DELIMITER, MEMORY_FILE } from "./constants.js";
4
+
5
+ export interface ProjectMemoryMigrationResult {
6
+ scanned: number;
7
+ copied: number;
8
+ merged: number;
9
+ skipped: number;
10
+ warnings: string[];
11
+ }
12
+
13
+ function readEntries(filePath: string): string[] {
14
+ if (!fs.existsSync(filePath)) return [];
15
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
16
+ if (!raw) return [];
17
+ return raw.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
18
+ }
19
+
20
+ function writeEntries(filePath: string, entries: string[]): void {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, entries.join(ENTRY_DELIMITER), "utf-8");
23
+ }
24
+
25
+ function isLegacyProjectDir(agentRoot: string, projectsMemoryDir: string, name: string): boolean {
26
+ if (name === "memory" || name === "skills" || name === projectsMemoryDir) return false;
27
+ if (name.startsWith(".")) return false;
28
+
29
+ const dir = path.join(agentRoot, name);
30
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(path.join(dir, MEMORY_FILE));
31
+ }
32
+
33
+ export function migrateLegacyProjectMemoryDirs(
34
+ globalDir: string,
35
+ projectsMemoryDir = "projects-memory",
36
+ ): ProjectMemoryMigrationResult {
37
+ const result: ProjectMemoryMigrationResult = {
38
+ scanned: 0,
39
+ copied: 0,
40
+ merged: 0,
41
+ skipped: 0,
42
+ warnings: [],
43
+ };
44
+
45
+ const agentRoot = path.dirname(globalDir);
46
+ if (!fs.existsSync(agentRoot)) return result;
47
+
48
+ const projectsRoot = path.join(agentRoot, projectsMemoryDir);
49
+
50
+ for (const name of fs.readdirSync(agentRoot)) {
51
+ if (!isLegacyProjectDir(agentRoot, projectsMemoryDir, name)) continue;
52
+ result.scanned++;
53
+
54
+ const legacyFile = path.join(agentRoot, name, MEMORY_FILE);
55
+ const targetFile = path.join(projectsRoot, name, MEMORY_FILE);
56
+
57
+ try {
58
+ const legacyEntries = readEntries(legacyFile);
59
+ if (legacyEntries.length === 0) {
60
+ result.skipped++;
61
+ continue;
62
+ }
63
+
64
+ if (!fs.existsSync(targetFile)) {
65
+ writeEntries(targetFile, legacyEntries);
66
+ result.copied++;
67
+ continue;
68
+ }
69
+
70
+ const targetEntries = readEntries(targetFile);
71
+ const mergedEntries = [...targetEntries];
72
+ const seen = new Set(targetEntries);
73
+
74
+ for (const entry of legacyEntries) {
75
+ if (!seen.has(entry)) {
76
+ seen.add(entry);
77
+ mergedEntries.push(entry);
78
+ }
79
+ }
80
+
81
+ if (mergedEntries.length === targetEntries.length) {
82
+ result.skipped++;
83
+ continue;
84
+ }
85
+
86
+ writeEntries(targetFile, mergedEntries);
87
+ result.merged++;
88
+ } catch (err) {
89
+ result.warnings.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
91
+ }
92
+
93
+ return result;
94
+ }
@@ -179,8 +179,8 @@ export function registerMemoryTool(
179
179
  async execute(toolCallId, params, signal, onUpdate, ctx) {
180
180
  const { action, target: rawTarget, content, old_text, category, failure_reason } = params;
181
181
 
182
- // Route 'project' to projectStore (internal target 'memory')
183
- const target = rawTarget as "memory" | "user" | "failure";
182
+ // Route 'project' to projectStore using the normal MEMORY.md target.
183
+ const target = rawTarget === "project" ? "memory" : rawTarget as "memory" | "user" | "failure";
184
184
  const activeStore = rawTarget === "project" ? projectStore : store;
185
185
 
186
186
  if (rawTarget === "project" && !projectStore) {