pi-hermes-memory 0.6.7 → 0.6.9

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Preview context command — /memory-preview-context shows the memory/skill blocks
3
+ * that are injected into the system prompt.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import { MemoryStore } from "../store/memory-store.js";
8
+ import { SkillStore } from "../store/skill-store.js";
9
+
10
+ export function registerPreviewContextCommand(
11
+ pi: ExtensionAPI,
12
+ store: MemoryStore,
13
+ projectStore: MemoryStore | null,
14
+ skillStore: SkillStore,
15
+ projectName: string,
16
+ ): void {
17
+ pi.registerCommand("memory-preview-context", {
18
+ description: "Preview the memory/skill context blocks injected into the system prompt",
19
+ handler: async (_args, ctx) => {
20
+ const memoryBlock = store.formatForSystemPrompt();
21
+ const projectBlock = projectStore ? projectStore.formatProjectBlock(projectName) : "";
22
+ const skillIndex = await skillStore.formatIndexForSystemPrompt();
23
+
24
+ const lines: string[] = [];
25
+ lines.push("");
26
+ lines.push(" ╔══════════════════════════════════════════════╗");
27
+ lines.push(" ║ 👀 Injected Context Preview ║");
28
+ lines.push(" ╚══════════════════════════════════════════════╝");
29
+ lines.push("");
30
+ lines.push(" This is the memory/skill context appended to the system prompt.");
31
+ lines.push(" (Core hidden system instructions are NOT shown.)");
32
+ lines.push("");
33
+
34
+ let blockCount = 0;
35
+
36
+ if (memoryBlock) {
37
+ blockCount++;
38
+ lines.push(" ── MEMORY + USER + RECENT FAILURES ─────────────────────────");
39
+ lines.push(memoryBlock);
40
+ lines.push("");
41
+ }
42
+
43
+ if (projectBlock) {
44
+ blockCount++;
45
+ lines.push(` ── PROJECT MEMORY (${projectName}) ─────────────────────────`);
46
+ lines.push(projectBlock);
47
+ lines.push("");
48
+ }
49
+
50
+ if (skillIndex) {
51
+ blockCount++;
52
+ lines.push(" ── SKILL INDEX ─────────────────────────────────────────────");
53
+ lines.push(skillIndex);
54
+ lines.push("");
55
+ }
56
+
57
+ if (blockCount === 0) {
58
+ lines.push(" No memory context blocks are currently injected.");
59
+ lines.push(" Add memory entries or skills, then run this command again.");
60
+ lines.push("");
61
+ }
62
+
63
+ lines.push(` Blocks shown: ${blockCount}`);
64
+ ctx.ui.notify(lines.join("\n"), "info");
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Markdown memory sync command — /memory-sync-markdown imports existing
3
+ * Markdown-backed memories into the SQLite search store.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
9
+ import { DatabaseManager } from '../store/db.js';
10
+ import {
11
+ parseMarkdownMemoryEntry,
12
+ syncMemoryEntry,
13
+ } from '../store/sqlite-memory-store.js';
14
+ import { ENTRY_DELIMITER, MEMORY_FILE, USER_FILE } from '../constants.js';
15
+
16
+ interface BackfillCounters {
17
+ filesScanned: number;
18
+ entriesScanned: number;
19
+ imported: number;
20
+ skipped: number;
21
+ warnings: string[];
22
+ }
23
+
24
+ function readEntries(filePath: string): string[] {
25
+ if (!fs.existsSync(filePath)) return [];
26
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
27
+ if (!raw) return [];
28
+ return raw.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
29
+ }
30
+
31
+ function importEntries(
32
+ dbManager: DatabaseManager,
33
+ counters: BackfillCounters,
34
+ entries: string[],
35
+ target: 'memory' | 'user' | 'failure',
36
+ project: string | null = null,
37
+ ): void {
38
+ for (const rawEntry of entries) {
39
+ counters.entriesScanned++;
40
+ try {
41
+ const parsed = parseMarkdownMemoryEntry(rawEntry, target, project);
42
+ const result = syncMemoryEntry(dbManager, parsed);
43
+ if (result.action === 'inserted') counters.imported++;
44
+ else counters.skipped++;
45
+ } catch (err) {
46
+ counters.warnings.push(
47
+ `${path.basename(project ?? 'global')}/${target}: ${err instanceof Error ? err.message : String(err)}`,
48
+ );
49
+ }
50
+ }
51
+ }
52
+
53
+ function scanProjectDirs(agentRoot: string, globalDir: string): Array<{ name: string; memoryFile: string }> {
54
+ if (!fs.existsSync(agentRoot)) return [];
55
+ const globalDirName = path.basename(globalDir);
56
+
57
+ return fs.readdirSync(agentRoot)
58
+ .map((name) => ({ name, dir: path.join(agentRoot, name) }))
59
+ .filter(({ name, dir }) => name !== globalDirName && fs.existsSync(dir) && fs.statSync(dir).isDirectory())
60
+ .map(({ name, dir }) => ({ name, memoryFile: path.join(dir, MEMORY_FILE) }))
61
+ .filter(({ memoryFile }) => fs.existsSync(memoryFile));
62
+ }
63
+
64
+ export function registerSyncMarkdownMemoriesCommand(
65
+ pi: ExtensionAPI,
66
+ dbManager: DatabaseManager,
67
+ globalDir: string,
68
+ ): void {
69
+ pi.registerCommand('memory-sync-markdown', {
70
+ description: 'Backfill Markdown memories into the SQLite search store',
71
+ handler: async (_args, ctx: ExtensionCommandContext) => {
72
+ const counters: BackfillCounters = {
73
+ filesScanned: 0,
74
+ entriesScanned: 0,
75
+ imported: 0,
76
+ skipped: 0,
77
+ warnings: [],
78
+ };
79
+
80
+ ctx.ui.notify('🔄 Scanning Markdown memory files for SQLite backfill...', 'info');
81
+
82
+ try {
83
+ const globalMemoryFile = path.join(globalDir, MEMORY_FILE);
84
+ const globalUserFile = path.join(globalDir, USER_FILE);
85
+ const globalFailureFile = path.join(globalDir, 'failures.md');
86
+
87
+ const importFile = (
88
+ filePath: string,
89
+ target: 'memory' | 'user' | 'failure',
90
+ project: string | null = null,
91
+ ) => {
92
+ if (!fs.existsSync(filePath)) return;
93
+ counters.filesScanned++;
94
+ const entries = readEntries(filePath);
95
+ importEntries(dbManager, counters, entries, target, project);
96
+ };
97
+
98
+ importFile(globalMemoryFile, 'memory');
99
+ importFile(globalUserFile, 'user');
100
+ importFile(globalFailureFile, 'failure');
101
+
102
+ const agentRoot = path.dirname(globalDir);
103
+ const projects = scanProjectDirs(agentRoot, globalDir);
104
+ for (const project of projects) {
105
+ importFile(project.memoryFile, 'memory', project.name);
106
+ }
107
+
108
+ let output = `\n✅ Markdown → SQLite sync complete!\n\n`;
109
+ output += `📊 Results:\n`;
110
+ output += `├─ Files scanned: ${counters.filesScanned}\n`;
111
+ output += `├─ Entries scanned: ${counters.entriesScanned}\n`;
112
+ output += `├─ Imported into SQLite: ${counters.imported}\n`;
113
+ output += `└─ Skipped as duplicates: ${counters.skipped}\n`;
114
+
115
+ if (projects.length > 0) {
116
+ output += `\n📁 Project memories scanned: ${projects.length}\n`;
117
+ }
118
+
119
+ if (counters.warnings.length > 0) {
120
+ output += `\n⚠️ Warnings (${counters.warnings.length}):\n`;
121
+ for (const warning of counters.warnings.slice(0, 5)) {
122
+ output += `├─ ${warning}\n`;
123
+ }
124
+ if (counters.warnings.length > 5) {
125
+ output += `└─ ... and ${counters.warnings.length - 5} more\n`;
126
+ }
127
+ }
128
+
129
+ output += `\n💡 Re-running this command is safe — existing SQLite rows are de-duplicated.`;
130
+ ctx.ui.notify(output, 'info');
131
+ } catch (err) {
132
+ ctx.ui.notify(`❌ Markdown sync failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
133
+ }
134
+ },
135
+ });
136
+ }
package/src/index.ts CHANGED
@@ -45,6 +45,8 @@ 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";
49
+ import { registerPreviewContextCommand } from "./handlers/preview-context.js";
48
50
  import { loadConfig } from "./config.js";
49
51
  import { detectProject } from "./project.js";
50
52
 
@@ -90,8 +92,8 @@ export default function (pi: ExtensionAPI) {
90
92
  }
91
93
  });
92
94
 
93
- // ── 3. Register the memory tool (with project store) ──
94
- registerMemoryTool(pi, store, projectStore);
95
+ // ── 3. Register the memory tool (with project store + SQLite sync) ──
96
+ registerMemoryTool(pi, store, projectStore, dbManager, projectName);
95
97
 
96
98
  // ── 4. Register the skill tool ──
97
99
  registerSkillTool(pi, skillStore);
@@ -109,7 +111,7 @@ export default function (pi: ExtensionAPI) {
109
111
  registerConsolidateCommand(pi, store);
110
112
 
111
113
  // ── 8. Setup correction detection ──
112
- setupCorrectionDetector(pi, store, projectStore, config);
114
+ setupCorrectionDetector(pi, store, projectStore, config, dbManager);
113
115
 
114
116
  // ── 9. Setup skill auto-trigger ──
115
117
  setupSkillAutoTrigger(pi, store, skillStore, config);
@@ -120,6 +122,8 @@ export default function (pi: ExtensionAPI) {
120
122
  registerInterviewCommand(pi, store);
121
123
  registerSwitchProjectCommand(pi);
122
124
  registerLearnMemoryCommand(pi);
125
+ registerSyncMarkdownMemoriesCommand(pi, dbManager, globalDir);
126
+ registerPreviewContextCommand(pi, store, projectStore, skillStore, projectName);
123
127
 
124
128
  // ── 11. SQLite session search + extended memory ──
125
129
  registerSessionSearchTool(pi, dbManager);
package/src/store/db.ts CHANGED
@@ -51,9 +51,11 @@ export class DatabaseManager {
51
51
  db.exec(SCHEMA_SQL);
52
52
  }
53
53
 
54
- // Extra safety: always ensure the legacy memories columns exist, even when
55
- // schema execution succeeds (idempotent on upgraded DBs).
54
+ // Extra safety: always ensure legacy memories columns exist, then migrate
55
+ // legacy CHECK(target IN ('memory','user')) constraints to include 'failure'.
56
56
  this.ensureMemoriesColumns(db);
57
+ this.migrateLegacyMemoriesTargetConstraint(db);
58
+ this.rebuildMemoryFts(db);
57
59
 
58
60
  return db;
59
61
  }
@@ -85,6 +87,56 @@ export class DatabaseManager {
85
87
  }
86
88
  }
87
89
 
90
+ private migrateLegacyMemoriesTargetConstraint(db: Database.Database): void {
91
+ const tableSqlRow = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='memories'").get() as { sql?: string } | undefined;
92
+ const tableSql = tableSqlRow?.sql ?? '';
93
+ if (!tableSql) return;
94
+
95
+ // Legacy schema allowed only memory/user. New schema must allow failure too.
96
+ const hasLegacyTargetCheck = /target\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*target\s+IN\s*\(\s*'memory'\s*,\s*'user'\s*\)\s*\)/i.test(tableSql);
97
+ if (!hasLegacyTargetCheck) return;
98
+
99
+ const tx = db.transaction(() => {
100
+ db.exec('PRAGMA foreign_keys = OFF');
101
+
102
+ db.exec(`
103
+ CREATE TABLE memories_new (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ project TEXT,
106
+ target TEXT NOT NULL CHECK (target IN ('memory', 'user', 'failure')),
107
+ category TEXT CHECK (category IN ('failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk')),
108
+ content TEXT NOT NULL,
109
+ failure_reason TEXT,
110
+ tool_state TEXT,
111
+ corrected_to TEXT,
112
+ created DATE NOT NULL,
113
+ last_referenced DATE NOT NULL
114
+ );
115
+ `);
116
+
117
+ db.exec(`
118
+ INSERT INTO memories_new (id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced)
119
+ SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
120
+ FROM memories;
121
+ `);
122
+
123
+ db.exec('DROP TABLE memories');
124
+ db.exec('ALTER TABLE memories_new RENAME TO memories');
125
+
126
+ db.exec('PRAGMA foreign_keys = ON');
127
+ });
128
+
129
+ tx();
130
+ }
131
+
132
+ private rebuildMemoryFts(db: Database.Database): void {
133
+ const ftsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_fts'").get() as { name?: string } | undefined;
134
+ if (!ftsTable) return;
135
+
136
+ // Keep FTS index consistent after table rebuild/migrations.
137
+ db.exec("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')");
138
+ }
139
+
88
140
  /**
89
141
  * Close the database connection.
90
142
  */