pi-memory-stone 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pi-memory-stone
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # pi-memory-stone
2
+
3
+ A global pi extension that preserves and retrieves useful memory across pi sessions. Raw pi session JSONL files remain the source of truth; the extension builds a searchable, privacy-safe SQLite+FTS5 index with backreferences to exact session entries.
4
+
5
+ ## Status
6
+
7
+ **MVP vertical slice** — deterministic indexing, FTS5 search, conservative injection, commands, tools, and tests are complete. LLM extraction, embeddings, and historical backfill are deferred.
8
+
9
+ ## Quick Start
10
+
11
+ Install globally as a pi package:
12
+
13
+ ```bash
14
+ pi install git:github.com/nikolasp/pi-memory-stone
15
+ ```
16
+
17
+ Or install manually in pi's global extension directory:
18
+
19
+ ```bash
20
+ git clone https://github.com/nikolasp/pi-memory-stone ~/.pi/agent/extensions/pi-memory-stone
21
+ ```
22
+
23
+ Then restart pi or run `/reload`, and verify it is active:
24
+
25
+ ```bash
26
+ /memory-status
27
+ ```
28
+
29
+ > Scope note: `pi install -l ...` / `pi install --local ...` writes to the current project's `.pi/settings.json` and only loads there. For all projects, run `pi install git:github.com/nikolasp/pi-memory-stone` without `--local` (user settings) or use `~/.pi/agent/extensions/`.
30
+
31
+ ## Architecture
32
+
33
+ ```
34
+ ~/.pi/agent/memory/memory.db # SQLite + FTS5 (WAL mode)
35
+ ~/.pi/agent/extensions/pi-memory-stone/
36
+ ├── src/
37
+ │ ├── index.ts # Entry point: hooks, lifecycle
38
+ │ ├── db/ # SQLite connection, migrations, CRUD
39
+ │ ├── indexing/ # Deterministic JSONL parser, agent_end handler
40
+ │ ├── retrieval/ # FTS search, hybrid ranking, injection builder
41
+ │ ├── commands/ # /memory-* slash commands
42
+ │ ├── tools/ # LLM-callable tools
43
+ │ ├── privacy/ # Secret redaction, sensitive path filtering
44
+ │ └── config/ # Project identity, settings
45
+ └── test/ # 44 tests across 4 suites
46
+ ```
47
+
48
+ ## How It Works
49
+
50
+ ### 1. Indexing (agent_end)
51
+
52
+ Every time the agent finishes a response, the extension:
53
+
54
+ - Reads new session entries since the last indexed position
55
+ - Parses turns deterministically (no LLM): extracts user prompts, assistant responses, tool calls, errors
56
+ - Redacts secrets (API keys, tokens, passwords, private keys) before storage
57
+ - Skips sensitive files (`.env`, keys, certs, `node_modules`, `.git`)
58
+ - Stores structured `turn_summary` and `error_resolution` records in SQLite
59
+ - Maintains `index_state` per session file so indexing is incremental
60
+
61
+ ### 2. Retrieval (before_agent_start)
62
+
63
+ Before each agent turn, the extension:
64
+
65
+ - Builds a focused query from the user's prompt
66
+ - Searches records via FTS5
67
+ - Ranks by hybrid score: FTS match + same-project boost + recency decay + kind weight + confidence
68
+ - Injects top results (max 5, threshold-limited) as system prompt context
69
+ - Tracks injected refs to prevent feedback loops
70
+ - Logs every injection for audit via `/memory-last`
71
+
72
+ ### 3. Storage Model
73
+
74
+ **Records:**
75
+ | Kind | Description |
76
+ |---|---|
77
+ | `turn_summary` | Concatenated user prompt + assistant response + tools used |
78
+ | `error_resolution` | Tool errors with context |
79
+ | `decision` | Explicitly remembered decisions (LLM/tool) |
80
+ | `preference` | User preferences (LLM/tool) |
81
+ | `task` | Tracked tasks (LLM/tool) |
82
+ | `session_summary` | Session-level summary (future) |
83
+
84
+ **Scopes:**
85
+ | Scope | Visibility |
86
+ |---|---|
87
+ | `project` | Visible within the same project (git repo root) |
88
+ | `global` | Visible across all projects (explicit opt-in only) |
89
+
90
+ **Statuses:** `active`, `soft_forgotten`, `hard_forgotten`, `superseded`
91
+
92
+ ## Commands
93
+
94
+ | Command | Alias | Description |
95
+ |---|---|---|
96
+ | `/memory-status` | `/stone-status` | Show index statistics, record counts by kind, config |
97
+ | `/memory-status --verbose` | | Include per-kind record breakdown |
98
+ | `/memory-search <query>` | `/stone-search` | Search memory for relevant records |
99
+ | `/memory-last` | `/stone-last` | Show the last memory injection packet |
100
+ | `/memory-forget <id>` | `/stone-forget` | Soft-forget a record (hide from searches) |
101
+ | `/memory-forget <id> --hard` | | Permanently delete (with confirmation) |
102
+ | `/memory-on` | | Enable memory injection for this session |
103
+ | `/memory-off` | | Disable memory injection for this session |
104
+
105
+ ## Tools
106
+
107
+ ### `memory_search`
108
+
109
+ Search memory stone for relevant records. Use before making decisions to recall past context.
110
+
111
+ ```
112
+ Parameters:
113
+ query Search query text
114
+ kind? Filter: decision | preference | task | error_resolution | turn_summary | session_summary
115
+ scope? Filter: project | global
116
+ limit? Max results (default 5)
117
+ ```
118
+
119
+ ### `memory_open`
120
+
121
+ Open a specific memory record by its reference ID.
122
+
123
+ ```
124
+ Parameters:
125
+ ref Memory record reference ID (from search results or injection packets)
126
+ ```
127
+
128
+ ### `memory_remember`
129
+
130
+ Explicitly store a memory record. Only use when the user explicitly asks to remember something.
131
+
132
+ ```
133
+ Parameters:
134
+ kind Record kind: decision | preference | task | error_resolution | turn_summary | session_summary
135
+ text Memory text to store
136
+ scope? project (default) | global
137
+ tags? Comma-separated tags
138
+ importance? 0-1 (default 0.5)
139
+ ```
140
+
141
+ ### `memory_forget`
142
+
143
+ Soft-forget a memory record by its reference ID. Hard deletion requires explicit user confirmation via command.
144
+
145
+ ```
146
+ Parameters:
147
+ ref Memory record reference ID
148
+ hard? Request permanent deletion (requires confirmation)
149
+ ```
150
+
151
+ ## Privacy & Safety
152
+
153
+ ### Secret Redaction
154
+
155
+ All text is redacted before storage. Patterns covered:
156
+
157
+ - OpenAI API keys (`sk-...`)
158
+ - GitHub tokens (`ghp_...`, `ghs_...`)
159
+ - AWS access keys (`AKIA...`)
160
+ - JWT tokens
161
+ - Generic API keys and secrets in `key=value` assignments
162
+ - Private keys (PEM format)
163
+ - Password/secrets in assignments
164
+ - Connection strings (credentials removed)
165
+
166
+ ### Path Filtering
167
+
168
+ Files at these paths are never indexed:
169
+
170
+ - `.env*`, `.envrc`
171
+ - Keys and certificates (`.pem`, `.key`, `.crt`)
172
+ - SSH keys (`id_rsa`, `id_ed25519`, `.ssh/`)
173
+ - AWS credentials (`~/.aws/`)
174
+ - GPG keys (`~/.gnupg/`)
175
+ - Dependency dirs (`node_modules`, `.git`, `dist`, `build`, `.next`, etc.)
176
+
177
+ ### Cross-Project Safety
178
+
179
+ - Records default to project scope
180
+ - Cross-project memory requires explicit global flag
181
+ - Global promotion refuses sensitive patterns (paths, secrets, hostnames, implementation details)
182
+ - `memory_open` never sends raw excerpts to LLM automatically
183
+
184
+ ## Ranking
185
+
186
+ Results are ranked by a hybrid score:
187
+
188
+ | Factor | Effect |
189
+ |---|---|
190
+ | FTS5 match quality | Base score (normalized reciprocal rank) |
191
+ | Same project | ×1.5 boost |
192
+ | Global scope | ×1.2 boost |
193
+ | Kind weight | Decision ×1.5, Preference ×1.3, Error ×1.4 |
194
+ | Recency | Exponential decay, half-life 7 days |
195
+ | Confidence | Direct multiplier |
196
+ | Importance | 0.5–1.5x multiplier |
197
+
198
+ ## DB Schema
199
+
200
+ ```sql
201
+ sessions — Indexed session metadata
202
+ records — Structured memory records (with FTS5 index)
203
+ record_fts — Full-text search index (contentless)
204
+ file_activity — File read/write/edit/bash tracking
205
+ injections — Audit log of memory injections
206
+ index_state — Per-session indexing progress
207
+ jobs — Background job queue
208
+ schema_migrations— Versioned migration tracking
209
+ ```
210
+
211
+ Storage: `~/.pi/agent/memory/memory.db` (SQLite, WAL mode, busy timeout 5s).
212
+
213
+ ## Tests
214
+
215
+ ```bash
216
+ cd ~/.pi/agent/extensions/pi-memory-stone
217
+ npm test
218
+ npm run typecheck
219
+ ```
220
+
221
+ These scripts are also runnable from a pi package clone installed from git; the required script runners are regular dependencies because pi package installs omit `devDependencies`.
222
+
223
+ 44 tests across 4 suites:
224
+
225
+ | Suite | Tests | Focus |
226
+ |---|---|---|
227
+ | `indexing.test.ts` | 1 | Incremental session indexing |
228
+ | `privacy.test.ts` | 17 | Secret redaction, sensitive path filtering |
229
+ | `parser.test.ts` | 10 | Turn parsing, file activity detection, error extraction |
230
+ | `ranking.test.ts` | 16 | Hybrid ranking, cross-project filtering, injection formatting |
231
+
232
+ ## Configuration
233
+
234
+ Project settings in `.pi/settings.json`:
235
+
236
+ ```json
237
+ {
238
+ "memory": {
239
+ "enabled": true,
240
+ "maxInjectedRecords": 5,
241
+ "maxInjectedTokens": 1000,
242
+ "scoreThreshold": 0.3,
243
+ "crossProjectEnabled": false
244
+ }
245
+ }
246
+ ```
247
+
248
+ ## Deferred (Future Slices)
249
+
250
+ - LLM extraction (decisions, preferences, tasks)
251
+ - Historical backfill (`/memory-backfill`)
252
+ - Embedding-based semantic search
253
+ - `/memory-edit`, `/memory-supersede`
254
+ - Rich TUI memory browser
255
+ - Multi-machine sync
256
+ - Daemon-mode background worker for LLM extraction
257
+ - `/memory-prune-missing`
258
+ - `.pi/memoryignore` and `~/.pi/agent/memoryignore`
259
+
260
+ ## License
261
+
262
+ MIT — see [LICENSE](./LICENSE) for details.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pi-memory-stone",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Global pi extension: preserves and retrieves useful memory across pi sessions",
6
+ "license": "MIT",
7
+ "pi": {
8
+ "extensions": ["./src/index.ts"]
9
+ },
10
+ "scripts": {
11
+ "test": "NODE_OPTIONS='--experimental-sqlite' tsx --test test/*.test.ts",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "tsx": "^4.22.3",
16
+ "typescript": "^5.9.3"
17
+ },
18
+ "peerDependencies": {
19
+ "@earendil-works/pi-ai": "*",
20
+ "@earendil-works/pi-coding-agent": "*",
21
+ "typebox": "*"
22
+ }
23
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Commands: /memory-status, /memory-search, /memory-last
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
+ import { getStats, getLastInjection } from "../db/index.js";
7
+ import { retrieve } from "../retrieval/index.js";
8
+ import { getProjectId, getConfig } from "../config/index.js";
9
+
10
+ export function registerCommands(pi: ExtensionAPI): void {
11
+ // ── /memory-status ──────────────────────────────────────────────
12
+
13
+ pi.registerCommand("memory-status", {
14
+ description: "Show memory stone index statistics",
15
+ handler: async (args, ctx) => {
16
+ await handleMemoryStatus(args, ctx);
17
+ },
18
+ });
19
+
20
+ pi.registerCommand("stone-status", {
21
+ description: "Alias for /memory-status",
22
+ handler: async (args, ctx) => {
23
+ await handleMemoryStatus(args, ctx);
24
+ },
25
+ });
26
+
27
+ // ── /memory-search ──────────────────────────────────────────────
28
+
29
+ pi.registerCommand("memory-search", {
30
+ description: "Search memory stone for relevant records",
31
+ handler: async (args, ctx) => {
32
+ await handleMemorySearch(args, ctx);
33
+ },
34
+ });
35
+
36
+ pi.registerCommand("stone-search", {
37
+ description: "Alias for /memory-search",
38
+ handler: async (args, ctx) => {
39
+ await handleMemorySearch(args, ctx);
40
+ },
41
+ });
42
+
43
+ // ── /memory-last ────────────────────────────────────────────────
44
+
45
+ pi.registerCommand("memory-last", {
46
+ description: "Show the last memory injection packet",
47
+ handler: async (_args, ctx) => {
48
+ await handleMemoryLast(ctx);
49
+ },
50
+ });
51
+
52
+ pi.registerCommand("stone-last", {
53
+ description: "Alias for /memory-last",
54
+ handler: async (_args, ctx) => {
55
+ await handleMemoryLast(ctx);
56
+ },
57
+ });
58
+
59
+ // ── /memory-forget ──────────────────────────────────────────────
60
+
61
+ pi.registerCommand("memory-forget", {
62
+ description: "Soft-forget a memory record by ID. Use --hard for permanent deletion.",
63
+ handler: async (args, ctx) => {
64
+ await handleMemoryForget(args, ctx);
65
+ },
66
+ });
67
+
68
+ pi.registerCommand("stone-forget", {
69
+ description: "Alias for /memory-forget",
70
+ handler: async (args, ctx) => {
71
+ await handleMemoryForget(args, ctx);
72
+ },
73
+ });
74
+
75
+ // ── /memory-on / /memory-off ────────────────────────────────────
76
+
77
+ pi.registerCommand("memory-on", {
78
+ description: "Enable memory injection for this session",
79
+ handler: async (_args, ctx) => {
80
+ ctx.ui.notify("Memory injection enabled for this session", "info");
81
+ // Session toggle — stored in extension state
82
+ pi.appendEntry("memory-stone:session-toggle", { enabled: true });
83
+ },
84
+ });
85
+
86
+ pi.registerCommand("memory-off", {
87
+ description: "Disable memory injection for this session",
88
+ handler: async (_args, ctx) => {
89
+ ctx.ui.notify("Memory injection disabled for this session", "info");
90
+ pi.appendEntry("memory-stone:session-toggle", { enabled: false });
91
+ },
92
+ });
93
+ }
94
+
95
+ // ─── Handler implementations ────────────────────────────────────────
96
+
97
+ async function handleMemoryStatus(args: string, ctx: ExtensionCommandContext): Promise<void> {
98
+ const verbose = args.includes("--verbose") || args.includes("-v");
99
+ const stats = getStats();
100
+
101
+ const lines: string[] = [];
102
+ lines.push("📊 Memory Stone Status");
103
+ lines.push("");
104
+ lines.push(` Total active records: ${stats.totalRecords}`);
105
+ lines.push(` Indexed sessions: ${stats.totalSessions}`);
106
+ lines.push(` File activity entries: ${stats.totalFileActivity}`);
107
+ lines.push("");
108
+
109
+ if (verbose && Object.keys(stats.recordsByKind).length > 0) {
110
+ lines.push(" Records by kind:");
111
+ for (const [kind, count] of Object.entries(stats.recordsByKind)) {
112
+ lines.push(` ${kind}: ${count}`);
113
+ }
114
+ lines.push("");
115
+ }
116
+
117
+ const config = getConfig(ctx.cwd);
118
+ lines.push(" Config:");
119
+ lines.push(` enabled: ${config.enabled}`);
120
+ lines.push(` maxInjectedRecords: ${config.maxInjectedRecords}`);
121
+ lines.push(` maxInjectedTokens: ${config.maxInjectedTokens}`);
122
+ lines.push(` crossProjectEnabled: ${config.crossProjectEnabled}`);
123
+
124
+ if (ctx.hasUI) {
125
+ ctx.ui.notify(lines.join("\n"), "info");
126
+ }
127
+ }
128
+
129
+ async function handleMemorySearch(args: string, ctx: ExtensionCommandContext): Promise<void> {
130
+ const query = args?.trim();
131
+
132
+ if (!query) {
133
+ ctx.ui.notify("Usage: /memory-search <query>", "warning");
134
+ return;
135
+ }
136
+
137
+ const projectId = getProjectId(ctx.cwd);
138
+ const config = getConfig(ctx.cwd);
139
+
140
+ const results = retrieve(query, projectId, [], {
141
+ limit: 20,
142
+ crossProjectEnabled: config.crossProjectEnabled,
143
+ });
144
+
145
+ if (results.length === 0) {
146
+ ctx.ui.notify("No matching memories found.", "info");
147
+ return;
148
+ }
149
+
150
+ const lines: string[] = [];
151
+ lines.push(`🔍 Memory search results for: "${query}"`);
152
+ lines.push("");
153
+
154
+ for (const [i, r] of results.entries()) {
155
+ const age = Date.now() - r.record.created_at;
156
+ const ageStr = age < 3600000
157
+ ? `${Math.floor(age / 60000)}m ago`
158
+ : age < 86400000
159
+ ? `${Math.floor(age / 3600000)}h ago`
160
+ : `${Math.floor(age / 86400000)}d ago`;
161
+
162
+ lines.push(` ${i + 1}. [${r.record.kind}] ${r.record.id} (${ageStr}, score: ${r.score.toFixed(2)})`);
163
+ lines.push(` ${r.record.text.slice(0, 120)}`);
164
+ lines.push("");
165
+ }
166
+
167
+ if (ctx.hasUI) {
168
+ ctx.ui.notify(lines.join("\n"), "info");
169
+ }
170
+ }
171
+
172
+ async function handleMemoryLast(ctx: ExtensionCommandContext): Promise<void> {
173
+ const sessionId = ctx.sessionManager.getSessionId();
174
+ const last = getLastInjection(sessionId);
175
+
176
+ if (!last) {
177
+ ctx.ui.notify("No memory injections in this session yet.", "info");
178
+ return;
179
+ }
180
+
181
+ const lines: string[] = [];
182
+ lines.push("📋 Last Memory Injection");
183
+ lines.push("");
184
+
185
+ if (last.packet) {
186
+ lines.push(last.packet);
187
+ } else {
188
+ lines.push(` Injected refs: ${last.injected_refs ?? "none"}`);
189
+ lines.push(` Reasons: ${last.reasons ?? "none"}`);
190
+ lines.push(` Time: ${new Date(last.created_at).toISOString()}`);
191
+ }
192
+
193
+ if (ctx.hasUI) {
194
+ ctx.ui.notify(lines.join("\n"), "info");
195
+ }
196
+ }
197
+
198
+ async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): Promise<void> {
199
+ const { softForgetRecord, hardDeleteRecord, getRecord } = await import("../db/index.js");
200
+
201
+ const parts = args?.trim().split(/\s+/) ?? [];
202
+ const hardFlag = parts.includes("--hard");
203
+ const refIds = parts.filter((p) => !p.startsWith("--"));
204
+
205
+ if (refIds.length === 0) {
206
+ ctx.ui.notify("Usage: /memory-forget <ref-id> [--hard]", "warning");
207
+ return;
208
+ }
209
+
210
+ for (const refId of refIds) {
211
+ const record = getRecord(refId);
212
+ if (!record) {
213
+ ctx.ui.notify(`Record ${refId} not found.`, "warning");
214
+ continue;
215
+ }
216
+
217
+ if (hardFlag) {
218
+ const confirmed = ctx.hasUI
219
+ ? await ctx.ui.confirm(
220
+ "Permanent deletion",
221
+ `Permanently delete memory record?\n\nKind: ${record.kind}\nText: ${record.text.slice(0, 100)}`,
222
+ )
223
+ : false;
224
+
225
+ if (confirmed) {
226
+ hardDeleteRecord(refId);
227
+ ctx.ui.notify(`Permanently deleted record ${refId}`, "info");
228
+ }
229
+ } else {
230
+ softForgetRecord(refId);
231
+ ctx.ui.notify(`Soft-forgotten record ${refId}`, "info");
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Configuration module.
3
+ * Project identity defaults to git repo root.
4
+ * Reads .pi/settings.json for memory.* config overrides.
5
+ */
6
+
7
+ import { execSync } from "node:child_process";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+
12
+ // ─── Project identity ───────────────────────────────────────────────
13
+
14
+ let _gitRootCache: Map<string, string | null> = new Map();
15
+
16
+ export function getProjectId(cwd: string): string | null {
17
+ // Use git repo root as project identity
18
+ if (_gitRootCache.has(cwd)) {
19
+ return _gitRootCache.get(cwd) ?? null;
20
+ }
21
+
22
+ try {
23
+ const root = execSync("git rev-parse --show-toplevel", {
24
+ cwd,
25
+ encoding: "utf8",
26
+ stdio: ["pipe", "pipe", "pipe"],
27
+ }).trim();
28
+ _gitRootCache.set(cwd, root);
29
+ return root;
30
+ } catch {
31
+ // Not a git repo; use cwd as fallback
32
+ _gitRootCache.set(cwd, cwd);
33
+ return cwd;
34
+ }
35
+ }
36
+
37
+ export function clearProjectCache(): void {
38
+ _gitRootCache.clear();
39
+ }
40
+
41
+ // ─── Config ─────────────────────────────────────────────────────────
42
+
43
+ export interface MemoryConfig {
44
+ /** Whether memory injection is enabled */
45
+ enabled: boolean;
46
+ /** Max records to inject per turn */
47
+ maxInjectedRecords: number;
48
+ /** Max tokens for injected packet */
49
+ maxInjectedTokens: number;
50
+ /** Minimum score threshold for injection */
51
+ scoreThreshold: number;
52
+ /** Whether cross-project injection is enabled */
53
+ crossProjectEnabled: boolean;
54
+ /** Extra ignore patterns */
55
+ ignorePatterns: string[];
56
+ }
57
+
58
+ const DEFAULT_CONFIG: MemoryConfig = {
59
+ enabled: true,
60
+ maxInjectedRecords: 5,
61
+ maxInjectedTokens: 1000,
62
+ scoreThreshold: 0.3,
63
+ crossProjectEnabled: false,
64
+ ignorePatterns: [],
65
+ };
66
+
67
+ const _configCache: Map<string, MemoryConfig> = new Map();
68
+
69
+ export function getConfig(cwd: string = process.cwd()): MemoryConfig {
70
+ const cacheKey = cwd;
71
+ const cached = _configCache.get(cacheKey);
72
+ if (cached) return cached;
73
+
74
+ // Try loading from project .pi/settings.json
75
+ const projectSettings = join(cwd, ".pi", "settings.json");
76
+
77
+ if (existsSync(projectSettings)) {
78
+ try {
79
+ const raw = readFileSync(projectSettings, "utf8");
80
+ const settings = JSON.parse(raw);
81
+ if (settings.memory) {
82
+ const config = { ...DEFAULT_CONFIG, ...settings.memory };
83
+ _configCache.set(cacheKey, config);
84
+ return config;
85
+ }
86
+ } catch {
87
+ // Invalid JSON, use defaults
88
+ }
89
+ }
90
+
91
+ const config = { ...DEFAULT_CONFIG };
92
+ _configCache.set(cacheKey, config);
93
+ return config;
94
+ }
95
+
96
+ export function reloadConfig(): void {
97
+ _configCache.clear();
98
+ }
99
+
100
+ // ─── Environment ────────────────────────────────────────────────────
101
+
102
+ export function getHomeDir(): string {
103
+ return homedir() || "/tmp";
104
+ }
105
+
106
+ export function getMemoryDir(): string {
107
+ return `${getHomeDir()}/.pi/agent/memory`;
108
+ }