diff --git a/packages/memory/jest.config.js b/packages/memory/jest.config.js new file mode 100644 index 0000000000..98643257c6 --- /dev/null +++ b/packages/memory/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; \ No newline at end of file diff --git a/packages/memory/package.json b/packages/memory/package.json new file mode 100644 index 0000000000..1a99faaa39 --- /dev/null +++ b/packages/memory/package.json @@ -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" + } +} diff --git a/packages/memory/src/index.test.ts b/packages/memory/src/index.test.ts new file mode 100644 index 0000000000..3350b43c52 --- /dev/null +++ b/packages/memory/src/index.test.ts @@ -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 = { + 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); + }); +}); \ No newline at end of file diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts new file mode 100644 index 0000000000..8c0db682d2 --- /dev/null +++ b/packages/memory/src/index.ts @@ -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; + + 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): Promise { + 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>): Promise { + 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 { + 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) + ); + } +} \ No newline at end of file diff --git a/packages/memory/tsconfig.json b/packages/memory/tsconfig.json new file mode 100644 index 0000000000..4f37b792e0 --- /dev/null +++ b/packages/memory/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} \ No newline at end of file