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.
|
|
4
|
-
"description": "๐ง Persistent memory + ๐ session search + ๐ก๏ธ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills.
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
58
|
-
.map((name) => ({ 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
|
|
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 (
|
|
117
|
-
output += `\n๐ Project memories scanned: ${
|
|
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
|
+
}
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -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
|
|
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) {
|