Project Files
docs / AGENT.md
A plugin for LM Studio that allows AI models to remember information between chats.
The memory.json store is a flat JSON file in the plugin directory (full CRUD process).
src/index.ts - Entry point, registers all tools (read, add, update, remove, clear, search, count)src/memoryManager.ts - Core MemoryManager class with CRUD and search methodssrc/config.ts - Unused dummy file with config schemasrc/test.ts - Deprecated test code, not in buildmemory.json - Data store (29 entries as of analysis)manifest.json - Plugin manifest (type: plugin, runner: node)package.json - Dependencies: openai, zod, @lmstudio/sdklms.config.js - Build config - bundle all dependenciesread_Memory(key?, id?) - Read specific slot or list alladd_Memory(key, data) - Add new memory slotupdate_Memory(id, key?, data?) - Update existing slotremove_Memory(id) - Delete slot by IDMemoryManager loads memory.json from process.cwd()Problem: File path uses process.cwd() instead of plugin directory
Fix: Use __dirname relative to plugin directory
Why: process.cwd() can be any directory, making memory.json creation unpredictable. __dirname is always the plugin directory.
memoryManager.ts FilesProblem: Two different versions of memoryManager.ts exist in src/
src/memoryManager.tssrc/memoryManager.tsFix: Delete the old sync version, keep only the async one.
Current old version (DELETE THIS):
Keep only: src/memoryManager.ts with async methods and saveQueue
Problem: All tool implementations are async but have no try/catch
Fix: Add try/catch to all tool implementations
Apply this pattern to ALL 7 tools in src/index.ts.
Problem: data parameter accepts z.any() - any JSON-serializable value
Fix: Add validation or documentation
Why: Users could store executable code or base64-encoded content, creating security risks if memory.json is shared.
clear_All_MemoryProblem: One call deletes all memory without confirmation
Fix: Add confirmation parameter
test.ts FileProblem: src/test.ts contains old code with pullHistory that's no longer used
Fix: Move to src/test/ or delete entirely
Problem: fs.writeFileSync does full file overwrite - data loss on process interruption
Fix: Use temp file + atomic rename
Alternative: Use write queue with backpressure
Problem: JSON.stringify(slot.data).toLowerCase().includes() is O(n) for every search
Fix: Maintain a Map for key lookups, use indexed search for content
Problem: No timeout on read/write operations - could hang indefinitely
Fix: Add timeout to all async operations
Problem: No audit trail of operations
Fix: Append operations to memory.log
Problem: openai declared in package.json but not used in code
Fix: Remove from dependencies
config.tsProblem: src/config.ts contains config schema that's never registered
Fix: Delete or move to proper location
Problem: No tests exist for MemoryManager
Fix: Create comprehensive test suite
Problem: No version tracking for memory.json format changes
Fix: Add version field and migration logic
Problem: README is minimal and doesn't explain usage
Fix: Expand README with examples
Each memory slot contains:
id: Auto-incremented unique identifierkey: Unique string key for identificationdata: JSON-serializable object (not code or scripts)createdAt: ISO 8601 timestampupdatedAt: ISO 8601 timestampdata must be a JSON object, not executable codeclear_All_Memory requires explicit confirmationmemory.json is stored in the plugin directorysrc/index.ts - Plugin entry point and tool registrationsrc/memoryManager.ts - Core memory management logicmemory.json - Data store (created on first use)memory.log - Operation audit log (optional)ISC
clear_All_Memory() - Delete ALL slots (DANGER)search_Memory(query, limit?) - Search by key or data contentcount_Memory(query) - Count matching slots// CURRENT (INSECURE)
this.filePath = path.join(process.cwd(), 'memory.json');
// FIXED
this.filePath = path.join(__dirname, '..', 'memory.json');
// src/memoryManager.ts (OLD - DELETE)
import fs from 'fs';
import path from 'path';
export interface MemorySlot {
id: number;
key: string;
data: unknown;
createdAt: string;
updatedAt: string;
}
export class MemoryManager {
private slots: MemorySlot[] = [];
private filePath: string;
constructor() {
this.filePath = path.join(process.cwd(), 'memory.json');
this.load();
}
private load() {
if (fs.existsSync(this.filePath)) {
try {
const data = fs.readFileSync(this.filePath, 'utf-8');
this.slots = JSON.parse(data);
// ...
} catch (e) {
console.error("Failed to parse memory.json", e);
this.slots = [];
}
} else {
this.slots = [];
}
}
private save() {
try {
fs.writeFileSync(this.filePath, JSON.stringify(this.slots, null, 2));
} catch (e) {
console.error("Failed to save memory.json", e);
}
}
// SYNC methods - NO async
search(query: string, limit?: number): MemorySlot[] {
const queryLower = query.toLowerCase();
const results = this.slots.filter(slot =>
slot.key.toLowerCase().includes(queryLower) ||
JSON.stringify(slot.data).toLowerCase().includes(queryLower)
);
if (typeof limit === 'number' && limit > 0) {
return results.slice(0, limit);
}
return results;
}
add(key: string, data: unknown): string {
// ... sync implementation
}
// ... more sync methods
}
// CURRENT (BROKEN)
implementation: async ({ key, data }) => {
return memoryManager.add(key, data);
},
// FIXED
implementation: async ({ key, data }) => {
try {
return memoryManager.add(key, data);
} catch (err) {
return `Error: ${err.message}`;
}
},
// CURRENT (INSECURE)
parameters: {
key: z.string().describe("A unique key to identify this memory slot"),
data: z.any().describe("The data to store (any JSON-serializable value)"),
},
// OPTION A: Restrict to objects
parameters: {
key: z.string().describe("A unique key to identify this memory slot"),
data: z.any().refine((val) => typeof val === 'object' || val === null, {
message: "Data must be a JSON object or null"
}).describe("The data to store (must be a JSON object)"),
},
// OPTION B: Add documentation
parameters: {
key: z.string().describe("A unique key to identify this memory slot"),
data: z.any().describe("The data to store (must be a JSON-serializable object, not code or scripts)"),
},
// CURRENT (DANGEROUS)
const clearAllMemory = tool({
name: "clear_All_Memory",
description: "DANGER: Delete ALL memory slots. Use only if explicitly asked to wipe or reset memory.",
parameters: {},
implementation: async () => {
return memoryManager.clear();
},
});
const clearAllMemory = tool({
name: "clear_All_Memory",
description: "Delete ALL memory slots permanently. Use only when explicitly asked to wipe or reset memory.",
parameters: {
confirm: z.string().describe("Type 'YES' to confirm clearing all memory"),
},
implementation: async ({ confirm }) => {
if (confirm !== 'YES') {
return "Error: Please type 'YES' to confirm clearing all memory.";
}
return memoryManager.clear();
},
});
# Move to test directory
mv src/test.ts src/test/main.test.ts
# Or delete if not needed
rm src/test.ts
// In memoryManager.ts
private async save() {
const tmpPath = `${this.filePath}.tmp`;
// Write to temp file first
await fs.promises.writeFile(tmpPath, JSON.stringify(this.slots, null, 2));
// Atomic rename (no data loss)
await fs.promises.rename(tmpPath, this.filePath);
},
private saveQueue: Promise<void> = Promise.resolve();
private async save() {
this.saveQueue = this.saveQueue.then(async () => {
try {
fs.writeFileSync(this.filePath, JSON.stringify(this.slots, null, 2));
} catch (e) {
console.error("Failed to save memory.json", e);
}
});
await this.saveQueue;
}
// In memoryManager.ts
private keyIndex: Map<string, number[]> = new Map();
// Build index on load
private load() {
// ... existing load logic ...
// Build key index
this.keyIndex.clear();
this.slots.forEach((slot, index) => {
if (!this.keyIndex.has(slot.key)) {
this.keyIndex.set(slot.key, []);
}
this.keyIndex.get(slot.key)!.push(index);
});
},
// Optimized search
search(query: string, limit?: number): MemorySlot[] {
const queryLower = query.toLowerCase();
// First check key index
const matchingIndices = this.keyIndex.get(queryLower) || [];
// Then check data content
let allIndices = new Set(matchingIndices);
for (let i = 0; i < this.slots.length; i++) {
if (!allIndices.has(i) && JSON.stringify(this.slots[i].data).toLowerCase().includes(queryLower)) {
allIndices.add(i);
}
}
// Sort by relevance (key matches first)
const results = [...allIndices].map(i => this.slots[i]);
if (typeof limit === 'number' && limit > 0) {
return results.slice(0, limit);
}
return results;
},
// Add searchCount
searchCount(query: string): number {
const queryLower = query.toLowerCase();
const matchingIndices = this.keyIndex.get(queryLower) || [];
let count = matchingIndices.length;
for (let i = 0; i < this.slots.length; i++) {
if (!matchingIndices.includes(i) && JSON.stringify(this.slots[i].data).toLowerCase().includes(queryLower)) {
count++;
}
}
return count;
},
// In memoryManager.ts
private async saveWithTimeout() {
const timeout = 10000; // 10 seconds
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Save operation timed out'));
}, timeout);
try {
const tmpPath = `${this.filePath}.tmp`;
fs.promises.writeFile(tmpPath, JSON.stringify(this.slots, null, 2))
.then(() => {
clearTimeout(timer);
fs.promises.rename(tmpPath, this.filePath)
.then(() => resolve())
.catch(err => {
clearTimeout(timer);
reject(err);
});
})
.catch(err => {
clearTimeout(timer);
reject(err);
});
} catch (err) {
clearTimeout(timer);
reject(err);
}
});
},
// In memoryManager.ts
private logFile: string;
constructor() {
this.filePath = path.join(__dirname, '..', 'memory.json');
this.logFile = path.join(__dirname, '..', 'memory.log');
this.load();
},
private log(action: string, key?: string, data?: unknown) {
const timestamp = new Date().toISOString();
const entry = `[${timestamp}] ${action}${key ? ` - Key: ${key}` : ''}`;
try {
fs.appendFileSync(this.logFile, entry + '\n');
} catch (e) {
console.error('Failed to write log:', e);
}
},
// In add method
async add(key: string, data: unknown): Promise<string> {
if (this.keyExists(key)) {
return `Error: Memory slot with key "${key}" already exists. Use a unique key.`;
}
const maxId = this.slots.length > 0 ? Math.max(...this.slots.map(s => s.id)) : 0;
const id = maxId + 1;
const now = new Date().toISOString();
this.slots.push({
id,
key,
data,
createdAt: now,
updatedAt: now,
});
await this.saveWithTimeout();
this.log('ADD', key);
return `Success: Added memory slot #${id} with key "${key}"`;
},
// Similar logging for update, remove, clear
// package.json - REMOVE this line:
"openai": "^5.2.0",
// Keep only:
"dependencies": {
"zod": "^3.24.1"
},
rm src/config.ts
// src/__tests__/memoryManager.test.ts
import { MemoryManager } from '../memoryManager';
import fs from 'fs';
import path from 'path';
import os from 'os';
describe('MemoryManager', () => {
let manager: MemoryManager;
let tmpDir: string;
let originalCwd: string;
beforeEach(() => {
originalCwd = process.cwd();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-test-'));
process.chdir(tmpDir);
// Override filePath to use tmpDir
(MemoryManager as any).filePath = path.join(tmpDir, 'memory.json');
manager = new MemoryManager();
});
afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('add', () => {
it('should add a new memory slot', () => {
const result = manager.add('test-key', { hello: 'world' });
expect(result).toContain('Success');
expect(result).toContain('#1');
expect(result).toContain('test-key');
});
it('should reject duplicate keys', () => {
manager.add('test-key', { data: 1 });
const result = manager.add('test-key', { data: 2 });
expect(result).toContain('Error');
expect(result).toContain('already exists');
});
it('should increment ID sequentially', () => {
manager.add('key1', 'data1');
manager.add('key2', 'data2');
const result = manager.add('key3', 'data3');
expect(result).toContain('#3');
});
});
describe('read', () => {
it('should read by key', () => {
manager.add('test-key', { hello: 'world' });
const result = manager.read('test-key');
expect(result).toContain('test-key');
expect(result).toContain('hello');
});
it('should read by ID', () => {
manager.add('test-key', { hello: 'world' });
const result = manager.read(1);
expect(result).toContain('#1');
});
it('should return empty status when no slots', () => {
const result = manager.read();
expect(result).toContain('empty');
});
});
describe('update', () => {
it('should update existing slot', () => {
manager.add('test-key', { old: 'data' });
const result = manager.update(1, undefined, { new: 'data' });
expect(result).toContain('Success');
expect(result).toContain('Updated');
});
it('should reject update with duplicate key', () => {
manager.add('key1', 'data1');
manager.add('key2', 'data2');
const result = manager.update(1, 'key2', { updated: true });
expect(result).toContain('Error');
expect(result).toContain('already exists');
});
});
describe('remove', () => {
it('should remove slot by ID', () => {
manager.add('test-key', 'data');
const result = manager.remove(1);
expect(result).toContain('Success');
expect(result).toContain('deleted');
});
it('should fail when removing non-existent ID', () => {
const result = manager.remove(999);
expect(result).toContain('Error');
});
});
describe('clear', () => {
it('should clear all slots', () => {
manager.add('key1', 'data1');
manager.add('key2', 'data2');
const result = manager.clear();
expect(result).toContain('Cleared all 2');
});
});
describe('search', () => {
it('should search by key', () => {
manager.add('user-preferences', { theme: 'dark' });
manager.add('session-data', { active: true });
const results = manager.search('user');
expect(results.length).toBe(1);
expect(results[0].key).toBe('user-preferences');
});
it('should search by data content', () => {
manager.add('test-key', { theme: 'dark' });
const results = manager.search('dark');
expect(results.length).toBe(1);
});
it('should respect limit parameter', () => {
for (let i = 0; i < 5; i++) {
manager.add(`key-${i}`, `data-${i}`);
}
const results = manager.search('key', 3);
expect(results.length).toBeLessThanOrEqual(3);
});
it('should return empty array when no matches', () => {
manager.add('test-key', 'data');
const results = manager.search('nonexistent');
expect(results.length).toBe(0);
});
});
});
// In memoryManager.ts
interface MemoryFile {
version: number;
slots: MemorySlot[];
metadata?: Record<string, unknown>;
}
private load(): void {
if (fs.existsSync(this.filePath)) {
try {
const data = fs.readFileSync(this.filePath, 'utf-8');
const file: MemoryFile = JSON.parse(data);
// Version migration
if (file.version === 1) {
this.migrateV1toV2(file);
} else if (file.version < 2) {
console.warn('Unsupported memory.json version:', file.version);
this.slots = [];
return;
}
this.slots = file.slots;
this.slots.forEach(s => {
s.id = Number(s.id);
if (!s.createdAt) s.createdAt = new Date().toISOString();
if (!s.updatedAt) s.updatedAt = s.createdAt;
});
} catch (e) {
console.error("Failed to parse memory.json", e);
this.slots = [];
}
} else {
this.slots = [];
}
},
private migrateV1toV2(file: MemoryFile): void {
// V1 format: [{id, key, data, createdAt, updatedAt}]
// V2 format: same but with version tracking
file.version = 2;
// Add metadata if missing
if (!file.metadata) {
file.metadata = {};
}
// Preserve existing slots
this.slots = file.slots;
},
private save(): void {
const file: MemoryFile = {
version: 2,
slots: this.slots,
metadata: {},
};
try {
fs.writeFileSync(this.filePath, JSON.stringify(file, null, 2));
} catch (e) {
console.error("Failed to save memory.json", e);
}
},
# LM Studio Plugin: memory
A plugin for LM Studio that allows AI models to remember information between chats.
## Features
- Full CRUD operations (Create, Read, Update, Delete)
- Search by key or data content
- Persistent storage in `memory.json`
- Atomic writes with temp file + rename
## Installation
1. Clone this repository
2. Run `npm install`
3. Run `npm run dev` or `npm run build`
4. Load the plugin in LM Studio
## Usage
### Add Memory
```typescript
// Tool: add_Memory
{
"key": "user-preferences",
"data": {
"theme": "dark",
"language": "Russian",
"timezone": "UTC+3"
}
}
// Tool: read_Memory
{
"key": "user-preferences"
}
// OR
{
"id": 1
}
// Tool: update_Memory
{
"id": 1,
"data": {
"theme": "light",
"language": "English"
}
}
// Tool: search_Memory
{
"query": "user",
"limit": 10
}
// Tool: clear_All_Memory
{
"confirm": "YES"
}
npm install
npm run dev # Development mode with hot reload
npm run build # Production build
npm test # Run tests
---
## SUMMARY OF CHANGES NEEDED
### Priority 1 (Critical - Fix Immediately)
- [ ] Fix Path Traversal: Change `process.cwd()` to `__dirname`
- [ ] Remove duplicate `memoryManager.ts` (keep only async version)
- [ ] Remove deprecated `test.ts` from `src/`
- [ ] Add try/catch to all 7 tool implementations
- [ ] Add data validation (reject non-object data)
- [ ] Add confirmation for `clear_All_Memory`
### Priority 2 (Important - Fix Soon)
- [ ] Add atomic writes (temp file + rename)
- [ ] Add search indexing for performance
- [ ] Add operation timeout (10 seconds)
- [ ] Add file logging to `memory.log`
### Priority 3 (Nice to Have)
- [ ] Remove unused `openai` dependency
- [ ] Remove unused `config.ts`
- [ ] Add unit tests
- [ ] Add version migration system
- [ ] Expand README documentation
### Expected Result
After these changes, the plugin will be:
- Secure (no path traversal, validated input)
- Reliable (atomic writes, error handling)
- Performant (indexed search, timeouts)
- Maintainable (clean code, tests, documentation)