Skip to content

Implemented long term memory #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/memory/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
25 changes: 25 additions & 0 deletions packages/memory/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@onlook/memory",
"version": "0.1.0",
"private": true,
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "jest --config jest.config.js"
},
"dependencies": {
"@onlook/types": "*",
"fs-extra": "^11.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.3.2",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
}
}
91 changes: 91 additions & 0 deletions packages/memory/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { LongTermMemory, Rule } from './index';
import fs from 'fs-extra';
import path from 'path';

describe('LongTermMemory', () => {
let memory: LongTermMemory;
const testRulesDir = path.join(process.cwd(), 'test-rules');

beforeEach(async () => {
await fs.remove(testRulesDir);
memory = new LongTermMemory(testRulesDir);
await memory.init();
});

afterEach(async () => {
await fs.remove(testRulesDir);
});

it('should initialize with empty rules', () => {
expect(memory.getAllRules()).toEqual([]);
});

it('should add a new rule', async () => {
const rule: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'> = {
content: 'Test rule content',
tags: ['test']
};

const addedRule = await memory.addRule(rule);
expect(addedRule).toMatchObject({
content: rule.content,
tags: rule.tags
});
expect(addedRule.id).toBeDefined();
expect(addedRule.createdAt).toBeInstanceOf(Date);
expect(addedRule.updatedAt).toBeInstanceOf(Date);

const filePath = path.join(testRulesDir, `${addedRule.id}.json`);
expect(await fs.pathExists(filePath)).toBe(true);
});

it('should update an existing rule', async () => {
const rule = await memory.addRule({
content: 'Original content',
tags: ['test']
});

const updatedRule = await memory.updateRule(rule.id, {
content: 'Updated content'
});

expect(updatedRule).not.toBeNull();
expect(updatedRule?.content).toBe('Updated content');
expect(updatedRule?.tags).toEqual(['test']);
});

it('should delete a rule', async () => {
const rule = await memory.addRule({
content: 'Test rule',
tags: ['test']
});

const deleted = await memory.deleteRule(rule.id);
expect(deleted).toBe(true);
expect(memory.getRule(rule.id)).toBeNull();

const filePath = path.join(testRulesDir, `${rule.id}.json`);
expect(await fs.pathExists(filePath)).toBe(false);
});

it('should get rules by tag', async () => {
await memory.addRule({
content: 'Rule 1',
tags: ['test']
});

await memory.addRule({
content: 'Rule 2',
tags: ['test']
});

await memory.addRule({
content: 'Rule 3',
tags: ['other']
});

const testRules = memory.getRulesByTag('test');
expect(testRules).toHaveLength(2);
expect(testRules.every(rule => rule.tags?.includes('test'))).toBe(true);
});
});
109 changes: 109 additions & 0 deletions packages/memory/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import fs from 'fs-extra';
import path from 'path';
import crypto from 'crypto';

export interface Rule {
id: string;
content: string;
createdAt: Date;
updatedAt: Date;
tags?: string[];
}

export class LongTermMemory {
private rulesDir: string;
private rules: Map<string, Rule>;

constructor(rulesDir: string = path.join(process.cwd(), 'rules')) {
this.rulesDir = rulesDir;
this.rules = new Map();
}

async init() {
await this.initialize();
}

private async initialize() {
try {
await fs.ensureDir(this.rulesDir);
await this.loadRules();
} catch (error) {
console.error('Failed to initialize long-term memory:', error);
}
}

private async loadRules() {
try {
const files = await fs.readdir(this.rulesDir);
for (const file of files) {
if (file.endsWith('.json')) {
const rulePath = path.join(this.rulesDir, file);
const ruleData = await fs.readJson(rulePath);
this.rules.set(ruleData.id, {
...ruleData,
createdAt: new Date(ruleData.createdAt),
updatedAt: new Date(ruleData.updatedAt)
});
}
}
} catch (error) {
console.error('Failed to load rules:', error);
}
}

async addRule(rule: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>): Promise<Rule> {
const newRule: Rule = {
...rule,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date()
};

const rulePath = path.join(this.rulesDir, `${newRule.id}.json`);
await fs.writeJson(rulePath, newRule);
this.rules.set(newRule.id, newRule);
return newRule;
}

async updateRule(id: string, updates: Partial<Omit<Rule, 'id' | 'createdAt'>>): Promise<Rule | null> {
const existingRule = this.rules.get(id);
if (!existingRule) return null;

const updatedRule: Rule = {
...existingRule,
...updates,
updatedAt: new Date()
};

const rulePath = path.join(this.rulesDir, `${id}.json`);
await fs.writeJson(rulePath, updatedRule);
this.rules.set(id, updatedRule);
return updatedRule;
}

async deleteRule(id: string): Promise<boolean> {
const rulePath = path.join(this.rulesDir, `${id}.json`);
try {
await fs.remove(rulePath);
this.rules.delete(id);
return true;
} catch (error) {
console.error('Failed to delete rule:', error);
return false;
}
}

getRule(id: string): Rule | null {
return this.rules.get(id) || null;
}

getAllRules(): Rule[] {
return Array.from(this.rules.values());
}

getRulesByTag(tag: string): Rule[] {
return Array.from(this.rules.values()).filter(rule =>
rule.tags?.includes(tag)
);
}
}
9 changes: 9 additions & 0 deletions packages/memory/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}