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