pi-hermes-memory 0.3.3 → 0.4.0

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/src/index.ts CHANGED
@@ -27,8 +27,13 @@ import * as os from "node:os";
27
27
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
28
28
  import { MemoryStore } from "./store/memory-store.js";
29
29
  import { SkillStore } from "./store/skill-store.js";
30
+ import { DatabaseManager } from "./store/db.js";
31
+ import { indexSession } from "./store/session-indexer.js";
32
+ import { parseSessionFile } from "./store/session-parser.js";
30
33
  import { registerMemoryTool } from "./tools/memory-tool.js";
31
34
  import { registerSkillTool } from "./tools/skill-tool.js";
35
+ import { registerSessionSearchTool } from "./tools/session-search-tool.js";
36
+ import { registerMemorySearchTool } from "./tools/memory-search-tool.js";
32
37
  import { setupBackgroundReview } from "./handlers/background-review.js";
33
38
  import { setupSessionFlush } from "./handlers/session-flush.js";
34
39
  import { registerInsightsCommand } from "./handlers/insights.js";
@@ -38,6 +43,7 @@ import { setupSkillAutoTrigger } from "./handlers/skill-auto-trigger.js";
38
43
  import { registerSkillsCommand } from "./handlers/skills-command.js";
39
44
  import { registerInterviewCommand } from "./handlers/interview.js";
40
45
  import { registerSwitchProjectCommand } from "./handlers/switch-project.js";
46
+ import { registerIndexSessionsCommand } from "./handlers/index-sessions.js";
41
47
  import { loadConfig } from "./config.js";
42
48
  import { detectProject } from "./project.js";
43
49
 
@@ -47,6 +53,7 @@ export default function (pi: ExtensionAPI) {
47
53
  const globalDir = config.memoryDir ?? path.join(os.homedir(), ".pi", "agent", "memory");
48
54
  const store = new MemoryStore(config);
49
55
  const skillStore = new SkillStore(path.join(globalDir, "skills"));
56
+ const dbManager = new DatabaseManager(globalDir);
50
57
 
51
58
  // Detect project from cwd using shared helper
52
59
  const project = detectProject();
@@ -111,4 +118,39 @@ export default function (pi: ExtensionAPI) {
111
118
  registerSkillsCommand(pi, skillStore);
112
119
  registerInterviewCommand(pi, store);
113
120
  registerSwitchProjectCommand(pi);
121
+
122
+ // ── 11. SQLite session search + extended memory ──
123
+ registerSessionSearchTool(pi, dbManager);
124
+ registerMemorySearchTool(pi, dbManager);
125
+ registerIndexSessionsCommand(pi);
126
+
127
+ // ── 12. Auto-index session on shutdown ──
128
+ let currentSessionId: string | null = null;
129
+ let currentSessionCwd: string | null = null;
130
+
131
+ pi.on("session_start", async (event, _ctx) => {
132
+ // Capture session metadata for indexing
133
+ currentSessionId = (event as Record<string, unknown>).sessionId as string ?? null;
134
+ currentSessionCwd = process.cwd();
135
+ });
136
+
137
+ pi.on("session_shutdown", async (_event, _ctx) => {
138
+ // Index the current session to SQLite
139
+ if (currentSessionId && currentSessionCwd) {
140
+ try {
141
+ const sessionsDir = path.join(os.homedir(), ".pi", "agent", "sessions");
142
+ const encodedCwd = currentSessionCwd.replace(/\//g, "-");
143
+ const sessionFiles = require("node:fs").readdirSync(path.join(sessionsDir, encodedCwd))
144
+ .filter((f: string) => f.includes(currentSessionId!) && f.endsWith(".jsonl"));
145
+ if (sessionFiles.length > 0) {
146
+ const sessionData = parseSessionFile(path.join(sessionsDir, encodedCwd, sessionFiles[0]));
147
+ if (sessionData) {
148
+ indexSession(dbManager, sessionData);
149
+ }
150
+ }
151
+ } catch {
152
+ // Silent fail — don't block shutdown
153
+ }
154
+ }
155
+ });
114
156
  }
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: learn-memory-tool
3
+ description: Learn how to use the pi-hermes-memory extension effectively — when to save memories, how to search, and best practices for persistent memory.
4
+ version: 1
5
+ created: 2026-05-03
6
+ updated: 2026-05-03
7
+ ---
8
+
9
+ ## When to Use
10
+
11
+ When a user asks about the memory system, how to use it, or when they seem confused about what gets remembered. Also useful for onboarding new users to the extension.
12
+
13
+ ## Overview
14
+
15
+ Pi Hermes Memory gives your AI agent persistent memory across sessions. Here's what it does:
16
+
17
+ ### What Gets Saved
18
+
19
+ | Type | File | What Goes Here | Limit |
20
+ |---|---|---|---|
21
+ | **Memory** | `MEMORY.md` | Facts — env details, project conventions, tool quirks | 5,000 chars |
22
+ | **User Profile** | `USER.md` | Who you are — name, preferences, communication style | 5,000 chars |
23
+ | **Skills** | `skills/*.md` | Procedures — *how* to debug, deploy, test | Unlimited |
24
+ | **Extended Memory** | `sessions.db` | Searchable memories beyond the core limit | Unlimited |
25
+
26
+ ### The `memory` Tool
27
+
28
+ The agent has a `memory` tool with these actions:
29
+
30
+ | Action | Target | What It Does |
31
+ |---|---|---|
32
+ | `add` | `memory` or `user` | Append a new entry |
33
+ | `replace` | `memory` or `user` | Update an existing entry (matched by substring) |
34
+ | `remove` | `memory` or `user` | Delete an entry (matched by substring) |
35
+
36
+ ### The `skill` Tool
37
+
38
+ For saving reusable procedures:
39
+
40
+ | Action | What It Does |
41
+ |---|---|
42
+ | `create` | Save a new skill |
43
+ | `view` | Read a skill or list all skills |
44
+ | `patch` | Update one section of a skill |
45
+ | `edit` | Replace description and/or body |
46
+ | `delete` | Remove a skill |
47
+
48
+ ### Search Tools
49
+
50
+ | Tool | What It Does |
51
+ |---|---|
52
+ | `session_search` | Search past conversations across all sessions |
53
+ | `memory_search` | Search extended memory store (unlimited capacity) |
54
+
55
+ ### Commands
56
+
57
+ | Command | What It Does |
58
+ |---|---|
59
+ | `/memory-insights` | Shows everything stored in memory and user profile |
60
+ | `/memory-skills` | Lists all agent-created skills |
61
+ | `/memory-consolidate` | Manually trigger memory consolidation |
62
+ | `/memory-interview` | Answer questions to pre-fill your user profile |
63
+ | `/memory-switch-project` | List all project memories |
64
+ | `/memory-index-sessions` | Import past sessions for search |
65
+
66
+ ## Best Practices
67
+
68
+ ### What TO Save
69
+
70
+ - **User preferences**: "prefers pnpm over npm", "uses vim", "likes concise answers"
71
+ - **Environment facts**: "macOS M1", "Node 20", "project uses Prisma"
72
+ - **Corrections**: "don't use npm — use pnpm", "always run tests first"
73
+ - **Project conventions**: "monorepo with turborepo", "conventional commits"
74
+ - **Tool quirks**: "CI needs `--frozen-lockfile`", "deploy script is in scripts/deploy.sh"
75
+
76
+ ### What NOT to Save
77
+
78
+ - **Task progress**: "finished implementing auth" — this is temporary
79
+ - **Session outcomes**: "PR #42 was merged" — this belongs in git history
80
+ - **Temporary state**: "currently debugging the test failure" — will be irrelevant soon
81
+ - **Large code blocks**: Use skills instead for procedures
82
+
83
+ ### How Memory Flows
84
+
85
+ 1. **Session starts**: Core memory (MEMORY.md + USER.md) is injected into the system prompt
86
+ 2. **During conversation**: Agent saves memories via the `memory` tool
87
+ 3. **Every 10 turns or 15 tool calls**: Background review saves anything noteworthy
88
+ 4. **When you correct the agent**: Immediate save — no waiting
89
+ 5. **When memory is full**: Auto-consolidation merges and prunes entries
90
+ 6. **Session ends**: One last flush before shutdown
91
+
92
+ ### Two-Tier Architecture
93
+
94
+ - **Global memory** (`~/.pi/agent/memory/`): Always injected — your name, preferences, tools
95
+ - **Project memory** (`~/.pi/agent/<project>/`): Injected when cwd matches — project-specific facts
96
+
97
+ ### Memory Aging
98
+
99
+ Entries carry timestamps. When consolidating, the agent knows which entries are stale (created long ago, never referenced) and which are fresh.
100
+
101
+ ### Context Fencing
102
+
103
+ Memory is wrapped in `<memory-context>` XML tags so the LLM never treats stored facts as user instructions. This prevents injection attacks through stored memory.
104
+
105
+ ## Troubleshooting
106
+
107
+ ### "Memory is full"
108
+ Run `/memory-consolidate` to manually merge entries. Or let auto-consolidation handle it.
109
+
110
+ ### "I can't find what I'm looking for"
111
+ Use `memory_search` to search the extended store, or `session_search` to search past conversations.
112
+
113
+ ### "The agent forgot something"
114
+ Check `/memory-insights` to see what's stored. If it's not there, the agent may not have saved it yet. You can tell the agent: "remember that X".
115
+
116
+ ### "I want to edit memory manually"
117
+ Memory files are plain markdown at `~/.pi/agent/memory/MEMORY.md` and `USER.md`. Edit them directly if you want.
118
+
119
+ ## Verification
120
+
121
+ After reading this skill, the user should understand:
122
+ 1. What the memory tool does and when to use it
123
+ 2. The difference between memory, user profile, skills, and extended memory
124
+ 3. How to search across sessions and extended memory
125
+ 4. Best practices for what to save and what not to save
@@ -0,0 +1,84 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { SCHEMA_SQL } from './schema.js';
5
+
6
+ export class DatabaseManager {
7
+ private db: Database.Database | null = null;
8
+ private readonly dbPath: string;
9
+
10
+ constructor(memoryDir: string) {
11
+ this.dbPath = path.join(memoryDir, 'sessions.db');
12
+ }
13
+
14
+ /**
15
+ * Get the database instance. Creates/opens on first call.
16
+ */
17
+ getDb(): Database.Database {
18
+ if (!this.db) {
19
+ this.db = this.open();
20
+ }
21
+ return this.db;
22
+ }
23
+
24
+ /**
25
+ * Open the database and initialize schema.
26
+ */
27
+ private open(): Database.Database {
28
+ // Ensure directory exists
29
+ const dir = path.dirname(this.dbPath);
30
+ if (!fs.existsSync(dir)) {
31
+ fs.mkdirSync(dir, { recursive: true });
32
+ }
33
+
34
+ const db = new Database(this.dbPath);
35
+
36
+ // Enable WAL mode for concurrent reads
37
+ db.pragma('journal_mode = WAL');
38
+ db.pragma('foreign_keys = ON');
39
+
40
+ // Create tables and triggers
41
+ db.exec(SCHEMA_SQL);
42
+
43
+ return db;
44
+ }
45
+
46
+ /**
47
+ * Close the database connection.
48
+ */
49
+ close(): void {
50
+ if (this.db) {
51
+ this.db.close();
52
+ this.db = null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get the database file path.
58
+ */
59
+ getPath(): string {
60
+ return this.dbPath;
61
+ }
62
+
63
+ /**
64
+ * Check if the database file exists.
65
+ */
66
+ exists(): boolean {
67
+ return fs.existsSync(this.dbPath);
68
+ }
69
+
70
+ /**
71
+ * Get stats about the database.
72
+ */
73
+ getStats(): { sessions: number; messages: number; memories: number } {
74
+ const db = this.getDb();
75
+ const sessions = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };
76
+ const messages = db.prepare('SELECT COUNT(*) as count FROM messages').get() as { count: number };
77
+ const memories = db.prepare('SELECT COUNT(*) as count FROM memories').get() as { count: number };
78
+ return {
79
+ sessions: sessions.count,
80
+ messages: messages.count,
81
+ memories: memories.count,
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SQLite schema for pi-hermes-memory v0.4
3
+ *
4
+ * Tables:
5
+ * - sessions — Pi session metadata
6
+ * - messages — all conversation messages
7
+ * - message_fts — FTS5 index for full-text search across messages
8
+ * - memories — extended memory entries (unlimited, searchable)
9
+ * - memory_fts — FTS5 index for memory search
10
+ */
11
+
12
+ export const SCHEMA_SQL = `
13
+ -- Session metadata
14
+ CREATE TABLE IF NOT EXISTS sessions (
15
+ id TEXT PRIMARY KEY,
16
+ project TEXT NOT NULL,
17
+ cwd TEXT NOT NULL,
18
+ started_at TEXT NOT NULL,
19
+ ended_at TEXT,
20
+ message_count INTEGER DEFAULT 0
21
+ );
22
+
23
+ -- All messages from all sessions
24
+ CREATE TABLE IF NOT EXISTS messages (
25
+ id TEXT PRIMARY KEY,
26
+ session_id TEXT NOT NULL REFERENCES sessions(id),
27
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
28
+ content TEXT NOT NULL,
29
+ timestamp TEXT NOT NULL,
30
+ tool_calls TEXT
31
+ );
32
+
33
+ -- FTS5 index for full-text search across messages
34
+ -- content='messages' + content_rowid='rowid' keeps FTS in sync with the content table
35
+ CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts5(
36
+ content,
37
+ content='messages',
38
+ content_rowid='rowid'
39
+ );
40
+
41
+ -- Triggers to keep message_fts in sync with messages table
42
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
43
+ INSERT INTO message_fts(rowid, content) VALUES (new.rowid, new.content);
44
+ END;
45
+
46
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
47
+ INSERT INTO message_fts(message_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
48
+ END;
49
+
50
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
51
+ INSERT INTO message_fts(message_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
52
+ INSERT INTO message_fts(rowid, content) VALUES (new.rowid, new.content);
53
+ END;
54
+
55
+ -- Extended memory entries (beyond MEMORY.md limit)
56
+ CREATE TABLE IF NOT EXISTS memories (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ project TEXT,
59
+ target TEXT NOT NULL CHECK (target IN ('memory', 'user')),
60
+ content TEXT NOT NULL,
61
+ created DATE NOT NULL,
62
+ last_referenced DATE NOT NULL
63
+ );
64
+
65
+ -- FTS5 index for memory search
66
+ -- content='memories' + content_rowid='id' keeps FTS in sync
67
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
68
+ content,
69
+ content='memories',
70
+ content_rowid='id'
71
+ );
72
+
73
+ -- Triggers to keep memory_fts in sync with memories table
74
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
75
+ INSERT INTO memory_fts(rowid, content) VALUES (new.id, new.content);
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
79
+ INSERT INTO memory_fts(memory_fts, rowid, content) VALUES ('delete', old.id, old.content);
80
+ END;
81
+
82
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
83
+ INSERT INTO memory_fts(memory_fts, rowid, content) VALUES ('delete', old.id, old.content);
84
+ INSERT INTO memory_fts(rowid, content) VALUES (new.id, new.content);
85
+ END;
86
+
87
+ -- Indexes for common queries
88
+ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
89
+ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
90
+ CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
91
+ CREATE INDEX IF NOT EXISTS idx_memories_target ON memories(target);
92
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
93
+ CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at);
94
+ `;
@@ -0,0 +1,153 @@
1
+ import { DatabaseManager } from './db.js';
2
+ import { parseSessionFile, getSessionFiles, type ParsedSession } from './session-parser.js';
3
+
4
+ /**
5
+ * Index result for a single session.
6
+ */
7
+ export interface IndexResult {
8
+ sessionId: string;
9
+ messagesIndexed: number;
10
+ skipped: boolean; // true if already indexed
11
+ }
12
+
13
+ /**
14
+ * Bulk index result.
15
+ */
16
+ export interface BulkIndexResult {
17
+ sessionsProcessed: number;
18
+ sessionsIndexed: number;
19
+ sessionsSkipped: number;
20
+ messagesIndexed: number;
21
+ errors: string[];
22
+ }
23
+
24
+ /**
25
+ * Index a single session into the database.
26
+ *
27
+ * @returns IndexResult with count of messages indexed
28
+ */
29
+ export function indexSession(dbManager: DatabaseManager, session: ParsedSession): IndexResult {
30
+ const db = dbManager.getDb();
31
+
32
+ // Check if already indexed
33
+ const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(session.id) as { id: string } | undefined;
34
+ if (existing) {
35
+ return { sessionId: session.id, messagesIndexed: 0, skipped: true };
36
+ }
37
+
38
+ // Insert session
39
+ db.prepare(`
40
+ INSERT INTO sessions (id, project, cwd, started_at, ended_at, message_count)
41
+ VALUES (?, ?, ?, ?, ?, ?)
42
+ `).run(
43
+ session.id,
44
+ session.project,
45
+ session.cwd,
46
+ session.startedAt,
47
+ session.endedAt,
48
+ session.messages.length
49
+ );
50
+
51
+ // Insert messages in a transaction for performance
52
+ const insertMsg = db.prepare(`
53
+ INSERT INTO messages (id, session_id, role, content, timestamp, tool_calls)
54
+ VALUES (?, ?, ?, ?, ?, ?)
55
+ `);
56
+
57
+ const insertMany = db.transaction((messages: ParsedSession['messages']) => {
58
+ for (const msg of messages) {
59
+ insertMsg.run(
60
+ msg.id,
61
+ session.id,
62
+ msg.role,
63
+ msg.content,
64
+ msg.timestamp,
65
+ msg.toolCalls ? JSON.stringify(msg.toolCalls) : null
66
+ );
67
+ }
68
+ });
69
+
70
+ insertMany(session.messages);
71
+
72
+ return { sessionId: session.id, messagesIndexed: session.messages.length, skipped: false };
73
+ }
74
+
75
+ /**
76
+ * Index all sessions from disk.
77
+ *
78
+ * @param dbManager — Database manager instance
79
+ * @param sessionsDir — Path to ~/.pi/agent/sessions/
80
+ * @param projectDir — Optional: specific project directory to index
81
+ * @returns Bulk index result
82
+ */
83
+ export function indexAllSessions(
84
+ dbManager: DatabaseManager,
85
+ sessionsDir: string,
86
+ projectDir?: string
87
+ ): BulkIndexResult {
88
+ const files = getSessionFiles(sessionsDir, projectDir);
89
+ const result: BulkIndexResult = {
90
+ sessionsProcessed: 0,
91
+ sessionsIndexed: 0,
92
+ sessionsSkipped: 0,
93
+ messagesIndexed: 0,
94
+ errors: [],
95
+ };
96
+
97
+ for (const file of files) {
98
+ result.sessionsProcessed++;
99
+
100
+ try {
101
+ const session = parseSessionFile(file);
102
+ if (!session) {
103
+ result.errors.push(`Failed to parse: ${file}`);
104
+ continue;
105
+ }
106
+
107
+ const indexResult = indexSession(dbManager, session);
108
+ if (indexResult.skipped) {
109
+ result.sessionsSkipped++;
110
+ } else {
111
+ result.sessionsIndexed++;
112
+ result.messagesIndexed += indexResult.messagesIndexed;
113
+ }
114
+ } catch (err) {
115
+ result.errors.push(`Error indexing ${file}: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Get statistics about indexed sessions.
124
+ */
125
+ export function getSessionStats(dbManager: DatabaseManager): {
126
+ totalSessions: number;
127
+ totalMessages: number;
128
+ projects: { project: string; sessions: number; messages: number }[];
129
+ } {
130
+ const db = dbManager.getDb();
131
+
132
+ const totals = db.prepare(`
133
+ SELECT
134
+ (SELECT COUNT(*) FROM sessions) as sessions,
135
+ (SELECT COUNT(*) FROM messages) as messages
136
+ `).get() as { sessions: number; messages: number };
137
+
138
+ const projects = db.prepare(`
139
+ SELECT
140
+ project,
141
+ COUNT(*) as sessions,
142
+ (SELECT COUNT(*) FROM messages m WHERE m.session_id IN (SELECT id FROM sessions s2 WHERE s2.project = s.project)) as messages
143
+ FROM sessions s
144
+ GROUP BY project
145
+ ORDER BY sessions DESC
146
+ `).all() as { project: string; sessions: number; messages: number }[];
147
+
148
+ return {
149
+ totalSessions: totals.sessions,
150
+ totalMessages: totals.messages,
151
+ projects,
152
+ };
153
+ }