@jungjaehoon/mama-server 1.0.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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@jungjaehoon/mama-server",
3
+ "version": "1.0.0",
4
+ "description": "MAMA MCP Server - Memory-Augmented MCP Assistant for Claude Code & Desktop",
5
+ "main": "src/server.js",
6
+ "bin": {
7
+ "mama-server": "src/server.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/server.js",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest watch",
13
+ "clean": "rm -rf dist node_modules"
14
+ },
15
+ "keywords": [
16
+ "mcp-server",
17
+ "model-context-protocol",
18
+ "claude",
19
+ "memory-assistant",
20
+ "decision-tracking",
21
+ "semantic-search",
22
+ "embeddings"
23
+ ],
24
+ "author": "SpineLift Team",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/jungjaehoon/MAMA.git",
29
+ "directory": "packages/mcp-server"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/jungjaehoon/MAMA/issues"
33
+ },
34
+ "homepage": "https://github.com/jungjaehoon/MAMA#readme",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.1",
40
+ "@huggingface/transformers": "^3.0.0",
41
+ "better-sqlite3": "^11.0.0",
42
+ "sqlite-vec": "^0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "vitest": "^1.0.0",
46
+ "@types/better-sqlite3": "^7.6.0"
47
+ },
48
+ "files": [
49
+ "src/**/*.js",
50
+ "README.md",
51
+ "LICENSE"
52
+ ]
53
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * MAMA Configuration Loader
3
+ *
4
+ * Story M1.4: Configurable embedding model selection
5
+ * Priority: P1 (Core Feature)
6
+ *
7
+ * Loads user configuration from ~/.mama/config.json with sensible defaults.
8
+ * Supports:
9
+ * - Model selection (default: multilingual-e5-small)
10
+ * - Embedding dimensions
11
+ * - Cache directory configuration
12
+ *
13
+ * @module config-loader
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const { info, warn, error: logError } = require('./debug-logger');
20
+
21
+ // Default configuration
22
+ const DEFAULT_CONFIG = {
23
+ modelName: 'Xenova/multilingual-e5-small',
24
+ embeddingDim: 384,
25
+ cacheDir: path.join(os.homedir(), '.cache', 'huggingface', 'transformers'),
26
+ };
27
+
28
+ // Config file path
29
+ const CONFIG_DIR = path.join(os.homedir(), '.mama');
30
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
31
+
32
+ // Cached configuration
33
+ let cachedConfig = null;
34
+
35
+ /**
36
+ * Ensure config directory exists
37
+ * @returns {void}
38
+ */
39
+ function ensureConfigDir() {
40
+ if (!fs.existsSync(CONFIG_DIR)) {
41
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
42
+ info(`[config] Created config directory: ${CONFIG_DIR}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Create default config file if it doesn't exist
48
+ * @returns {void}
49
+ */
50
+ function ensureConfigFile() {
51
+ if (!fs.existsSync(CONFIG_PATH)) {
52
+ ensureConfigDir();
53
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf8');
54
+ info(`[config] Created default config file: ${CONFIG_PATH}`);
55
+ info(`[config] Model: ${DEFAULT_CONFIG.modelName} (${DEFAULT_CONFIG.embeddingDim}-dim)`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Load MAMA configuration from ~/.mama/config.json
61
+ *
62
+ * Story M1.4 AC #1: Config parser loads ~/.mama/config.json
63
+ *
64
+ * @param {boolean} reload - Force reload from disk (default: false)
65
+ * @returns {Object} Configuration object with modelName, embeddingDim, cacheDir
66
+ */
67
+ function loadConfig(reload = false) {
68
+ // Return cached config if available and not forcing reload
69
+ if (cachedConfig && !reload) {
70
+ return cachedConfig;
71
+ }
72
+
73
+ try {
74
+ // Ensure config file exists
75
+ ensureConfigFile();
76
+
77
+ // Read and parse config file
78
+ const configData = fs.readFileSync(CONFIG_PATH, 'utf8');
79
+ const userConfig = JSON.parse(configData);
80
+
81
+ // Merge with defaults (user config overrides)
82
+ const config = {
83
+ ...DEFAULT_CONFIG,
84
+ ...userConfig,
85
+ };
86
+
87
+ // Validate configuration
88
+ if (!config.modelName || typeof config.modelName !== 'string') {
89
+ warn('[config] Invalid modelName, using default:', DEFAULT_CONFIG.modelName);
90
+ config.modelName = DEFAULT_CONFIG.modelName;
91
+ }
92
+
93
+ if (!Number.isInteger(config.embeddingDim) || config.embeddingDim <= 0) {
94
+ warn('[config] Invalid embeddingDim, using default:', DEFAULT_CONFIG.embeddingDim);
95
+ config.embeddingDim = DEFAULT_CONFIG.embeddingDim;
96
+ }
97
+
98
+ if (!config.cacheDir || typeof config.cacheDir !== 'string') {
99
+ warn('[config] Invalid cacheDir, using default:', DEFAULT_CONFIG.cacheDir);
100
+ config.cacheDir = DEFAULT_CONFIG.cacheDir;
101
+ }
102
+
103
+ // Cache the loaded config
104
+ cachedConfig = config;
105
+
106
+ // Log loaded configuration
107
+ if (reload) {
108
+ info(`[config] Configuration reloaded from ${CONFIG_PATH}`);
109
+ info(`[config] Model: ${config.modelName} (${config.embeddingDim}-dim)`);
110
+ info(`[config] Cache: ${config.cacheDir}`);
111
+ }
112
+
113
+ return config;
114
+ } catch (error) {
115
+ logError(`[config] Failed to load config file: ${error.message}`);
116
+ logError('[config] Using default configuration');
117
+
118
+ // Cache defaults on error
119
+ cachedConfig = { ...DEFAULT_CONFIG };
120
+ return cachedConfig;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get current model name
126
+ * @returns {string} Current model name
127
+ */
128
+ function getModelName() {
129
+ const config = loadConfig();
130
+ return config.modelName;
131
+ }
132
+
133
+ /**
134
+ * Get current embedding dimension
135
+ * @returns {number} Current embedding dimension
136
+ */
137
+ function getEmbeddingDim() {
138
+ const config = loadConfig();
139
+ return config.embeddingDim;
140
+ }
141
+
142
+ /**
143
+ * Get current cache directory
144
+ * @returns {string} Current cache directory
145
+ */
146
+ function getCacheDir() {
147
+ const config = loadConfig();
148
+ return config.cacheDir;
149
+ }
150
+
151
+ /**
152
+ * Update configuration and save to file
153
+ *
154
+ * Story M1.4 AC #3: Changing model via config triggers informative log + resets caches
155
+ *
156
+ * @param {Object} updates - Configuration updates
157
+ * @param {string} updates.modelName - New model name
158
+ * @param {number} updates.embeddingDim - New embedding dimension
159
+ * @param {string} updates.cacheDir - New cache directory
160
+ * @returns {boolean} Success status
161
+ */
162
+ function updateConfig(updates) {
163
+ try {
164
+ ensureConfigFile();
165
+
166
+ // Load current config
167
+ const currentConfig = loadConfig();
168
+
169
+ // Check if model is changing
170
+ const modelChanged = updates.modelName && updates.modelName !== currentConfig.modelName;
171
+ const dimChanged = updates.embeddingDim && updates.embeddingDim !== currentConfig.embeddingDim;
172
+
173
+ // Merge updates
174
+ const newConfig = {
175
+ ...currentConfig,
176
+ ...updates,
177
+ };
178
+
179
+ // Save to file
180
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(newConfig, null, 2), 'utf8');
181
+
182
+ // Update cache
183
+ cachedConfig = newConfig;
184
+
185
+ // Story M1.4 AC #3: Informative log when model changes
186
+ if (modelChanged || dimChanged) {
187
+ info('[config] ⚠️ Embedding model configuration changed');
188
+ info(`[config] Old: ${currentConfig.modelName} (${currentConfig.embeddingDim}-dim)`);
189
+ info(`[config] New: ${newConfig.modelName} (${newConfig.embeddingDim}-dim)`);
190
+ info('[config] ⚡ Model cache will be reset on next embedding generation');
191
+ info('[config] ⚡ Existing embeddings in database remain unchanged');
192
+ }
193
+
194
+ info(`[config] Configuration saved to ${CONFIG_PATH}`);
195
+ return true;
196
+ } catch (error) {
197
+ logError(`[config] Failed to update config: ${error.message}`);
198
+ return false;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get config file path
204
+ * @returns {string} Config file path
205
+ */
206
+ function getConfigPath() {
207
+ return CONFIG_PATH;
208
+ }
209
+
210
+ module.exports = {
211
+ loadConfig,
212
+ getModelName,
213
+ getEmbeddingDim,
214
+ getCacheDir,
215
+ updateConfig,
216
+ getConfigPath,
217
+ DEFAULT_CONFIG,
218
+ };
@@ -0,0 +1,105 @@
1
+ # Database Adapter Layer
2
+
3
+ ## Overview
4
+
5
+ Abstraction layer for MAMA database to support both SQLite (development/testing) and PostgreSQL (production on Railway).
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ mama-api.js
11
+
12
+ memory-store.js (business logic)
13
+
14
+ DatabaseAdapter (interface)
15
+
16
+ ├── SQLiteAdapter (better-sqlite3 + sqlite-vss)
17
+ └── PostgreSQLAdapter (pg + pgvector)
18
+ ```
19
+
20
+ ## Decision Rationale
21
+
22
+ **Topic**: mama_db_adapter_pattern
23
+ **Decision**: Abstract database layer with driver-specific implementations
24
+ **Reasoning**:
25
+ 1. **Environment Flexibility**: SQLite for local/testing, PostgreSQL for production
26
+ 2. **Zero Breaking Changes**: Existing memory-store.js API remains unchanged
27
+ 3. **Vector Search Portability**: sqlite-vss → pgvector migration path
28
+ 4. **Testing Simplicity**: Fast SQLite tests, production PostgreSQL validation
29
+
30
+ ## Adapter Interface
31
+
32
+ All adapters must implement:
33
+
34
+ ```javascript
35
+ class DatabaseAdapter {
36
+ // Connection
37
+ connect(config) → db
38
+ disconnect()
39
+ isConnected() → boolean
40
+
41
+ // Prepared Statements
42
+ prepare(sql) → Statement
43
+ exec(sql)
44
+ transaction(fn) → result
45
+
46
+ // Vector Search
47
+ vectorSearch(embedding, limit) → results
48
+ insertEmbedding(rowid, embedding)
49
+
50
+ // Utility
51
+ getLastInsertRowid() → number
52
+ }
53
+ ```
54
+
55
+ ## Implementation Files
56
+
57
+ - `index.js` - Factory + environment detection
58
+ - `sqlite-adapter.js` - SQLite implementation (current behavior)
59
+ - `postgresql-adapter.js` - PostgreSQL implementation
60
+ - `statement.js` - Statement wrapper (unified interface)
61
+
62
+ ## Environment Variable
63
+
64
+ ```bash
65
+ # Default: SQLite
66
+ MAMA_DB_PATH=~/.mama/memories.db
67
+
68
+ # PostgreSQL (Railway)
69
+ MAMA_DATABASE_URL=postgresql://user:pass@host:5432/mama_db
70
+ ```
71
+
72
+ **Detection Logic**:
73
+ - If `MAMA_DATABASE_URL` set → PostgreSQL
74
+ - Else → SQLite with `MAMA_DB_PATH`
75
+
76
+ ## Migration Strategy
77
+
78
+ ### Phase 1: Adapter Layer (Current)
79
+ 1. Create adapter interface
80
+ 2. Extract SQLite logic to SQLiteAdapter
81
+ 3. Update memory-store.js to use adapter
82
+
83
+ ### Phase 2: PostgreSQL Support
84
+ 1. Implement PostgreSQLAdapter
85
+ 2. Convert migration SQL files
86
+ 3. Add pgvector support
87
+
88
+ ### Phase 3: Testing
89
+ 1. Run existing tests with SQLite
90
+ 2. Add PostgreSQL integration tests
91
+ 3. Validate on Railway
92
+
93
+ ## Performance Requirements
94
+
95
+ - Prepared statement caching
96
+ - Connection pooling (PostgreSQL only)
97
+ - Transaction batching support
98
+ - Vector search < 100ms (p95)
99
+
100
+ ## Backward Compatibility
101
+
102
+ ✅ Existing code works without changes
103
+ ✅ SQLite remains default for development
104
+ ✅ Environment variable controls database type
105
+ ✅ No API changes to memory-store.js
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Base Database Adapter Interface
3
+ * All adapters must implement these methods
4
+ */
5
+ class DatabaseAdapter {
6
+ /**
7
+ * Connect to database
8
+ * @returns {Object} Database connection
9
+ */
10
+ connect() {
11
+ throw new Error('connect() must be implemented by subclass');
12
+ }
13
+
14
+ /**
15
+ * Disconnect from database
16
+ */
17
+ disconnect() {
18
+ throw new Error('disconnect() must be implemented by subclass');
19
+ }
20
+
21
+ /**
22
+ * Check if connected
23
+ * @returns {boolean} Connection status
24
+ */
25
+ isConnected() {
26
+ throw new Error('isConnected() must be implemented by subclass');
27
+ }
28
+
29
+ /**
30
+ * Prepare a SQL statement
31
+ * @param {string} sql - SQL query
32
+ * @returns {Statement} Prepared statement
33
+ */
34
+ prepare(sql) {
35
+ throw new Error('prepare() must be implemented by subclass');
36
+ }
37
+
38
+ /**
39
+ * Execute raw SQL
40
+ * @param {string} sql - SQL to execute
41
+ */
42
+ exec(sql) {
43
+ throw new Error('exec() must be implemented by subclass');
44
+ }
45
+
46
+ /**
47
+ * Execute function in transaction
48
+ * @param {Function} fn - Function to execute
49
+ * @returns {*} Function return value
50
+ */
51
+ transaction(fn) {
52
+ throw new Error('transaction() must be implemented by subclass');
53
+ }
54
+
55
+ /**
56
+ * Vector similarity search
57
+ * @param {number[]} embedding - Query embedding (384-dim)
58
+ * @param {number} limit - Max results
59
+ * @returns {Array<Object>} Search results with distance
60
+ */
61
+ vectorSearch(embedding, limit) {
62
+ throw new Error('vectorSearch() must be implemented by subclass');
63
+ }
64
+
65
+ /**
66
+ * Insert vector embedding
67
+ * @param {number} rowid - Decision rowid
68
+ * @param {number[]} embedding - Embedding vector
69
+ */
70
+ insertEmbedding(rowid, embedding) {
71
+ throw new Error('insertEmbedding() must be implemented by subclass');
72
+ }
73
+
74
+ /**
75
+ * Get last inserted row ID
76
+ * @returns {number} Last rowid
77
+ */
78
+ getLastInsertRowid() {
79
+ throw new Error('getLastInsertRowid() must be implemented by subclass');
80
+ }
81
+
82
+ /**
83
+ * Run migrations
84
+ * @param {string} migrationsDir - Path to migrations directory
85
+ */
86
+ runMigrations(migrationsDir) {
87
+ throw new Error('runMigrations() must be implemented by subclass');
88
+ }
89
+ }
90
+
91
+ module.exports = { DatabaseAdapter };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Database Adapter Factory (SQLite-only)
3
+ *
4
+ * MAMA Plugin uses SQLite exclusively for local storage.
5
+ * PostgreSQL support is only available in the legacy mcp-server.
6
+ *
7
+ * @module db-adapter
8
+ */
9
+
10
+ const { info } = require('../debug-logger');
11
+ const SQLiteAdapter = require('./sqlite-adapter');
12
+
13
+ /**
14
+ * Create SQLite database adapter
15
+ *
16
+ * @param {Object} config - Database configuration
17
+ * @param {string} [config.dbPath] - SQLite file path (overrides env)
18
+ * @returns {DatabaseAdapter} Configured SQLite adapter instance
19
+ */
20
+ function createAdapter(config = {}) {
21
+ info('[db-adapter] Using SQLite adapter (plugin mode)');
22
+ const dbPath = config.dbPath || process.env.MAMA_DB_PATH;
23
+ return new SQLiteAdapter({ dbPath });
24
+ }
25
+
26
+ const { DatabaseAdapter } = require('./base-adapter');
27
+
28
+ module.exports = {
29
+ createAdapter,
30
+ DatabaseAdapter,
31
+ };