pi-hermes-memory 0.7.7 → 0.7.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.
- package/README.md +52 -38
- package/package.json +2 -1
- package/src/config.ts +14 -1
- package/src/extension-root-migration.ts +101 -0
- package/src/handlers/index-sessions.ts +1 -1
- package/src/handlers/skills-command.ts +1216 -41
- package/src/index.ts +43 -29
- package/src/project-memory-migration.ts +1 -1
- package/src/store/memory-store.ts +1 -1
- package/src/store/session-anchor-search.ts +472 -0
- package/src/store/skill-store.ts +279 -43
- package/src/tools/session-search-tool.ts +106 -1
- package/src/types.ts +11 -6
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ The extension stores memory at two levels:
|
|
|
103
103
|
|
|
104
104
|
| Tier | Location | What goes here | Available when |
|
|
105
105
|
|---|---|---|---|
|
|
106
|
-
| **Global** | `~/.pi/agent/memory/` | Facts that apply everywhere — your name, preferences, OS, tools | Searchable via `memory_search` |
|
|
106
|
+
| **Global** | `~/.pi/agent/pi-hermes-memory/` | Facts that apply everywhere — your name, preferences, OS, tools | Searchable via `memory_search` |
|
|
107
107
|
| **Project** | `~/.pi/agent/projects-memory/<project>/` | Facts scoped to one codebase — architecture decisions, API quirks, team norms | Searchable when cwd matches the project |
|
|
108
108
|
|
|
109
109
|
By default, full Markdown memories are **not** injected into the system prompt. The system prompt gets a full-detail `<memory-policy>` that tells the agent when to call `memory_search` and how to treat memory results. This keeps first-turn token usage low while preserving access to user, project, failure, correction, insight, preference, convention, and tool-quirk memories.
|
|
@@ -185,7 +185,7 @@ The agent also gets a `skill` tool for saving reusable procedures:
|
|
|
185
185
|
|
|
186
186
|
Skills are stored in Pi-native locations:
|
|
187
187
|
|
|
188
|
-
- Global skills: `~/.pi/agent/skills/<slug>/SKILL.md`
|
|
188
|
+
- Global skills: `~/.pi/agent/pi-hermes-memory/skills/<slug>/SKILL.md`
|
|
189
189
|
- Project skills: `~/.pi/agent/projects-memory/<project>/skills/<slug>/SKILL.md`
|
|
190
190
|
|
|
191
191
|
The extension classifies new skills automatically:
|
|
@@ -241,13 +241,13 @@ This lets Pi discover project skills as native skills without copying them into
|
|
|
241
241
|
|---|---|---|---|
|
|
242
242
|
| **memory** | `MEMORY.md` | Agent's notes — env facts, project conventions, tool quirks, lessons learned | 5,000 chars |
|
|
243
243
|
| **user** | `USER.md` | User profile — name, preferences, communication style, habits | 5,000 chars |
|
|
244
|
-
| **skills** | `~/.pi/agent/skills/<slug>/SKILL.md` or `projects-memory/<project>/skills/<slug>/SKILL.md` | Procedures — *how* to debug, deploy, test, or fix something | Unlimited |
|
|
244
|
+
| **skills** | `~/.pi/agent/pi-hermes-memory/skills/<slug>/SKILL.md` or `projects-memory/<project>/skills/<slug>/SKILL.md` | Procedures — *how* to debug, deploy, test, or fix something | Unlimited |
|
|
245
245
|
| **extended** | `sessions.db` | Searchable memories beyond the core limit | Unlimited |
|
|
246
246
|
| **sessions** | `sessions.db` | Past conversation history (searchable via FTS5) | Unlimited |
|
|
247
247
|
|
|
248
248
|
### Session History Search
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
By default, the extension indexes your Pi session history into a SQLite database with FTS5 full-text search. The agent can search across all past conversations using the `session_search` tool:
|
|
251
251
|
|
|
252
252
|
| Tool | What it does |
|
|
253
253
|
|---|---|---|
|
|
@@ -260,6 +260,8 @@ Session history is indexed automatically on session shutdown. To bulk-import exi
|
|
|
260
260
|
/memory-index-sessions
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
For users who prefer source anchors over snippets, `sessionSearch.variant` can be set to `anchors`. In that opt-in mode, the same `session_search` tool reads session JSONL files directly and accepts a Markdown request with fields such as `from`, `to`, `cwd`, and `limit`, plus `all`, `any`, and `exclude` lists. It returns plain text with `count`, an optional `message`, and compact `path:startLine-endLine` style anchors with short reasons instead of summaries or previews.
|
|
264
|
+
|
|
263
265
|
### Extended Memory Store
|
|
264
266
|
|
|
265
267
|
The extension keeps Markdown memory as the human-readable source of truth, and mirrors successful writes into the SQLite-backed search store used by `memory_search`.
|
|
@@ -321,7 +323,7 @@ This means skills build up naturally over time without you having to ask.
|
|
|
321
323
|
| Command | What it does |
|
|
322
324
|
|---|---|
|
|
323
325
|
| `/memory-insights` | Shows everything stored in memory and user profile |
|
|
324
|
-
| `/memory-skills` |
|
|
326
|
+
| `/memory-skills` | Opens an interactive skills manager for search, multi-select, move, and delete |
|
|
325
327
|
| `/memory-consolidate` | Manually trigger memory consolidation to free space |
|
|
326
328
|
| `/memory-interview` | Answer a few questions to pre-fill your user profile |
|
|
327
329
|
| `/memory-switch-project` | List all project memories and their entry counts |
|
|
@@ -350,25 +352,34 @@ This means skills build up naturally over time without you having to ask.
|
|
|
350
352
|
3. codes primarily in TypeScript
|
|
351
353
|
```
|
|
352
354
|
|
|
353
|
-
### `/memory-skills`
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
355
|
+
### `/memory-skills` Manager
|
|
356
|
+
|
|
357
|
+
`/memory-skills` now opens an interactive TUI modal for skill management.
|
|
358
|
+
|
|
359
|
+
Features:
|
|
360
|
+
- fuzzy search by skill name
|
|
361
|
+
- single-list view with scope badges (`[G]` global, `[P]` project)
|
|
362
|
+
- multi-select with spacebar
|
|
363
|
+
- batch move to global or current project
|
|
364
|
+
- batch delete with one confirmation
|
|
365
|
+
- inline action summaries for partial success/conflicts
|
|
366
|
+
|
|
367
|
+
Keybindings:
|
|
368
|
+
- `↑` / `↓` — move focus
|
|
369
|
+
- `space` — toggle selection
|
|
370
|
+
- `/` — focus search
|
|
371
|
+
- `tab` — switch between search and list
|
|
372
|
+
- `g` — move selected skills to global
|
|
373
|
+
- `p` — move selected skills to project
|
|
374
|
+
- `d` — delete selected skills
|
|
375
|
+
- `a` — select all filtered skills
|
|
376
|
+
- `n` — clear selection
|
|
377
|
+
- `esc` — close the modal
|
|
378
|
+
|
|
379
|
+
Move behavior:
|
|
380
|
+
- moves are **conflict-safe**
|
|
381
|
+
- if the destination already contains the same slug, the conflicting skill stays in place
|
|
382
|
+
- batch moves use partial-success semantics: non-conflicting skills move, blocked skills are reported in the summary
|
|
372
383
|
|
|
373
384
|
## Configuration
|
|
374
385
|
|
|
@@ -381,8 +392,9 @@ Create `~/.pi/agent/hermes-memory-config.json`:
|
|
|
381
392
|
"memoryCharLimit": 5000,
|
|
382
393
|
"userCharLimit": 5000,
|
|
383
394
|
"projectCharLimit": 5000,
|
|
384
|
-
"memoryDir": "~/.pi/agent/memory",
|
|
395
|
+
"memoryDir": "~/.pi/agent/pi-hermes-memory",
|
|
385
396
|
"projectsMemoryDir": "projects-memory",
|
|
397
|
+
"sessionSearch": { "variant": "legacy" },
|
|
386
398
|
"nudgeInterval": 10,
|
|
387
399
|
"nudgeToolCalls": 15,
|
|
388
400
|
"reviewRecentMessages": 0,
|
|
@@ -409,8 +421,9 @@ Create `~/.pi/agent/hermes-memory-config.json`:
|
|
|
409
421
|
| `memoryCharLimit` | `5000` | Max characters in MEMORY.md |
|
|
410
422
|
| `userCharLimit` | `5000` | Max characters in USER.md |
|
|
411
423
|
| `projectCharLimit` | `5000` | Max characters in project-scoped MEMORY.md |
|
|
412
|
-
| `memoryDir` | `~/.pi/agent/memory` | Custom directory for
|
|
424
|
+
| `memoryDir` | `~/.pi/agent/pi-hermes-memory` | Custom directory for extension storage files |
|
|
413
425
|
| `projectsMemoryDir` | `projects-memory` | Subdirectory under `~/.pi/agent/` for project-scoped memory |
|
|
426
|
+
| `sessionSearch` | `{ "variant": "legacy" }` | Session search implementation: `legacy` keeps the existing SQLite/FTS snippet search; `anchors` uses the opt-in Markdown request surface and returns compact JSONL line-range anchors from `~/.pi/agent/sessions/` |
|
|
414
427
|
| `nudgeInterval` | `10` | Turns between auto-reviews |
|
|
415
428
|
| `nudgeToolCalls` | `15` | Tool calls between auto-reviews (OR with turns) |
|
|
416
429
|
| `reviewRecentMessages` | `0` | Recent messages included in background review (`0` = all) |
|
|
@@ -435,11 +448,16 @@ Create `~/.pi/agent/hermes-memory-config.json`:
|
|
|
435
448
|
|
|
436
449
|
```
|
|
437
450
|
~/.pi/agent/
|
|
438
|
-
├──
|
|
439
|
-
│ ├──
|
|
440
|
-
│
|
|
441
|
-
│
|
|
442
|
-
│
|
|
451
|
+
├── pi-hermes-memory/ ← Global extension storage root
|
|
452
|
+
│ ├── MEMORY.md ← Agent's personal notes (env facts, patterns, lessons)
|
|
453
|
+
│ ├── USER.md ← User profile (name, preferences, habits)
|
|
454
|
+
│ ├── sessions.db ← SQLite database (session history + extended memory)
|
|
455
|
+
│ ├── skills/ ← Global extension-managed skills
|
|
456
|
+
│ │ ├── debug-typescript-errors/
|
|
457
|
+
│ │ │ └── SKILL.md
|
|
458
|
+
│ │ └── testing-checklist/
|
|
459
|
+
│ │ └── SKILL.md
|
|
460
|
+
│ └── .skills-migrated-to-extension-storage
|
|
443
461
|
├── projects-memory/ ← ALL project-scoped memories (one subfolder per project)
|
|
444
462
|
│ ├── my-project/
|
|
445
463
|
│ │ ├── MEMORY.md
|
|
@@ -448,11 +466,6 @@ Create `~/.pi/agent/hermes-memory-config.json`:
|
|
|
448
466
|
│ │ └── SKILL.md
|
|
449
467
|
│ └── another-project/
|
|
450
468
|
│ └── MEMORY.md
|
|
451
|
-
├── memory/ ← Global memory
|
|
452
|
-
│ ├── MEMORY.md ← Agent's personal notes (env facts, patterns, lessons)
|
|
453
|
-
│ ├── USER.md ← User profile (name, preferences, habits)
|
|
454
|
-
│ ├── sessions.db ← SQLite database (session history + extended memory)
|
|
455
|
-
│ └── .skills-migrated-to-pi-native
|
|
456
469
|
├── hermes-memory-config.json
|
|
457
470
|
└── ...
|
|
458
471
|
```
|
|
@@ -471,8 +484,9 @@ The `sessions.db` SQLite database stores session history and extended memory ent
|
|
|
471
484
|
- **Older Markdown memories may need backfill**: If you saved memories before the SQLite mirror existed or search looks stale, run `/memory-sync-markdown`.
|
|
472
485
|
- **Core memory limits still apply**: SQLite search mirroring does not bypass the 5,000-char core Markdown limit. If consolidation cannot free space, the write fails instead of becoming SQLite-only memory invisibly.
|
|
473
486
|
- **System prompts are invisible**: Pi's TUI does not display the system prompt. Use `/memory-preview-context` to inspect whether policy-only or legacy memory injection is active.
|
|
474
|
-
- **Project skill visibility depends on Pi discovery cycles**: project skills are exposed through `resources_discover` using the active project's `skills/` path. If a
|
|
475
|
-
- **
|
|
487
|
+
- **Project skill visibility depends on Pi discovery cycles**: project skills are exposed through `resources_discover` using the active project's `skills/` path. If a moved or newly created project skill doesn't show up immediately in a running session, trigger a reload/new session so Pi refreshes discovered resources.
|
|
488
|
+
- **Project move requires active project context**: in `/memory-skills`, the `p` hotkey is disabled when Pi is not currently in a detected project directory.
|
|
489
|
+
- **Skills are agent-generated**: Skills are created by the agent based on its experience. They may not always be perfectly structured. You can move, delete, or still edit them directly in `~/.pi/agent/pi-hermes-memory/skills/` or the active project's `skills/` folder.
|
|
476
490
|
|
|
477
491
|
## Architecture
|
|
478
492
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"typescript": "^6.0.3"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
59
60
|
"better-sqlite3": "^12.9.0"
|
|
60
61
|
}
|
|
61
62
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
import type { MemoryConfig, MemoryOverflowStrategy } from "./types.js";
|
|
4
|
+
import type { MemoryConfig, MemoryOverflowStrategy, SessionSearchVariant } from "./types.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_MEMORY_CHAR_LIMIT,
|
|
7
7
|
DEFAULT_USER_CHAR_LIMIT,
|
|
@@ -18,11 +18,16 @@ import {
|
|
|
18
18
|
} from "./constants.js";
|
|
19
19
|
|
|
20
20
|
const MEMORY_OVERFLOW_STRATEGIES: readonly MemoryOverflowStrategy[] = ["auto-consolidate", "reject", "fifo-evict"];
|
|
21
|
+
const SESSION_SEARCH_VARIANTS: readonly SessionSearchVariant[] = ["legacy", "anchors"];
|
|
21
22
|
|
|
22
23
|
function isMemoryOverflowStrategy(value: unknown): value is MemoryOverflowStrategy {
|
|
23
24
|
return typeof value === "string" && MEMORY_OVERFLOW_STRATEGIES.includes(value as MemoryOverflowStrategy);
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function isSessionSearchVariant(value: unknown): value is SessionSearchVariant {
|
|
28
|
+
return typeof value === "string" && SESSION_SEARCH_VARIANTS.includes(value as SessionSearchVariant);
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
const DEFAULT_CONFIG: MemoryConfig = {
|
|
27
32
|
memoryMode: "policy-only",
|
|
28
33
|
memoryPolicyStyle: "full",
|
|
@@ -45,6 +50,7 @@ const DEFAULT_CONFIG: MemoryConfig = {
|
|
|
45
50
|
consolidationTimeoutMs: DEFAULT_CONSOLIDATION_TIMEOUT_MS,
|
|
46
51
|
nudgeToolCalls: DEFAULT_NUDGE_TOOL_CALLS,
|
|
47
52
|
projectsMemoryDir: DEFAULT_PROJECTS_MEMORY_DIR,
|
|
53
|
+
sessionSearch: { variant: "legacy" },
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
export const DEFAULT_CONFIG_PATH = path.join(
|
|
@@ -107,6 +113,13 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
107
113
|
if (typeof parsed.projectCharLimit === "number") config.projectCharLimit = parsed.projectCharLimit;
|
|
108
114
|
if (typeof parsed.memoryDir === "string") config.memoryDir = parsed.memoryDir;
|
|
109
115
|
if (typeof parsed.projectsMemoryDir === "string") config.projectsMemoryDir = parsed.projectsMemoryDir;
|
|
116
|
+
if (
|
|
117
|
+
typeof parsed.sessionSearch === "object" &&
|
|
118
|
+
parsed.sessionSearch !== null &&
|
|
119
|
+
isSessionSearchVariant(parsed.sessionSearch.variant)
|
|
120
|
+
) {
|
|
121
|
+
config.sessionSearch = { variant: parsed.sessionSearch.variant };
|
|
122
|
+
}
|
|
110
123
|
if (hasMemoryOverflowStrategy) {
|
|
111
124
|
config.autoConsolidate = config.memoryOverflowStrategy === "auto-consolidate";
|
|
112
125
|
} else if (hasLegacyAutoConsolidate) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface ExtensionRootMigrationResult {
|
|
6
|
+
moved: number;
|
|
7
|
+
merged: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(filePath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function moveFileSafe(source: string, target: string): Promise<void> {
|
|
22
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await fs.rename(source, target);
|
|
26
|
+
return;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
29
|
+
if (code !== "EXDEV") throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fs.copyFile(source, target);
|
|
33
|
+
await fs.unlink(source);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function moveDirContents(sourceDir: string, targetDir: string, result: ExtensionRootMigrationResult): Promise<void> {
|
|
37
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
42
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
43
|
+
|
|
44
|
+
if (!await pathExists(targetPath)) {
|
|
45
|
+
try {
|
|
46
|
+
await moveFileSafe(sourcePath, targetPath);
|
|
47
|
+
result.moved++;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
result.warnings.push(`${sourcePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
await moveDirContents(sourcePath, targetPath, result);
|
|
56
|
+
result.merged++;
|
|
57
|
+
try {
|
|
58
|
+
const remaining = await fs.readdir(sourcePath);
|
|
59
|
+
if (remaining.length === 0) await fs.rmdir(sourcePath);
|
|
60
|
+
} catch {
|
|
61
|
+
// best effort
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
result.skipped++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Move legacy extension assets from ~/.pi/agent/memory into
|
|
72
|
+
* ~/.pi/agent/pi-hermes-memory. Existing destination files win.
|
|
73
|
+
*/
|
|
74
|
+
export async function migrateExtensionRoot(
|
|
75
|
+
legacyRoot: string,
|
|
76
|
+
targetRoot: string,
|
|
77
|
+
): Promise<ExtensionRootMigrationResult> {
|
|
78
|
+
const result: ExtensionRootMigrationResult = {
|
|
79
|
+
moved: 0,
|
|
80
|
+
merged: 0,
|
|
81
|
+
skipped: 0,
|
|
82
|
+
warnings: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (path.resolve(legacyRoot) === path.resolve(targetRoot)) return result;
|
|
86
|
+
if (!existsSync(legacyRoot)) return result;
|
|
87
|
+
|
|
88
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
89
|
+
await moveDirContents(legacyRoot, targetRoot, result);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const remaining = await fs.readdir(legacyRoot);
|
|
93
|
+
if (remaining.length === 0) {
|
|
94
|
+
await fs.rmdir(legacyRoot);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// best effort
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
@@ -34,7 +34,7 @@ export function registerIndexSessionsCommand(pi: ExtensionAPI): void {
|
|
|
34
34
|
|
|
35
35
|
ctx.ui.notify(`📁 Found ${totalFiles} session files across ${projectDirs.length} projects\n⏳ Indexing...`, 'info');
|
|
36
36
|
|
|
37
|
-
const memoryDir = path.join(os.homedir(), '.pi', 'agent', 'memory');
|
|
37
|
+
const memoryDir = path.join(os.homedir(), '.pi', 'agent', 'pi-hermes-memory');
|
|
38
38
|
const dbManager = new DatabaseManager(memoryDir);
|
|
39
39
|
|
|
40
40
|
try {
|