@jungjaehoon/mama-core 1.0.2 → 1.1.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.
@@ -1,279 +0,0 @@
1
- /**
2
- * @fileoverview Session REST API - HTTP endpoints for session management
3
- * @module mobile/session-api
4
- * @version 1.5.0
5
- *
6
- * Provides REST API endpoints for creating, listing, and terminating
7
- * Claude Code sessions.
8
- *
9
- * @example
10
- * const { createSessionHandler } = require('./session-api');
11
- * const sessionHandler = createSessionHandler(sessionManager);
12
- * // Use in HTTP server: if (sessionHandler(req, res)) return;
13
- */
14
-
15
- const { SessionManager } = require('./session-manager.js');
16
- const { authenticate } = require('./auth.js');
17
-
18
- /**
19
- * Default WebSocket port (same as HTTP port)
20
- * @type {number}
21
- */
22
- const DEFAULT_WS_PORT = parseInt(process.env.MAMA_HTTP_PORT, 10) || 3847;
23
-
24
- /**
25
- * Parse request body as JSON
26
- * @param {http.IncomingMessage} req - HTTP request
27
- * @returns {Promise<Object>} Parsed JSON body
28
- */
29
- function readBody(req) {
30
- return new Promise((resolve, reject) => {
31
- let data = '';
32
- req.on('data', (chunk) => (data += chunk));
33
- req.on('end', () => {
34
- try {
35
- if (!data) {
36
- resolve({});
37
- } else {
38
- resolve(JSON.parse(data));
39
- }
40
- } catch (e) {
41
- reject(new Error('Invalid JSON'));
42
- }
43
- });
44
- req.on('error', reject);
45
- });
46
- }
47
-
48
- /**
49
- * Extract session ID from URL path
50
- * @param {string} url - Request URL
51
- * @returns {string|null} Session ID or null
52
- */
53
- function extractSessionId(url) {
54
- const match = url.match(/\/api\/sessions\/([^/?]+)/);
55
- return match ? match[1] : null;
56
- }
57
-
58
- /**
59
- * Get WebSocket URL for a session
60
- * @param {http.IncomingMessage} req - HTTP request
61
- * @param {string} sessionId - Session ID
62
- * @returns {string} WebSocket URL
63
- */
64
- function getWsUrl(req, sessionId) {
65
- const host = req.headers.host || `localhost:${DEFAULT_WS_PORT}`;
66
- const protocol = req.headers['x-forwarded-proto'] === 'https' ? 'wss' : 'ws';
67
- return `${protocol}://${host}/ws?session=${sessionId}`;
68
- }
69
-
70
- /**
71
- * Create session API request handler
72
- * @param {SessionManager} [sessionManager] - Session manager instance (will create if not provided)
73
- * @returns {Function} Request handler function
74
- */
75
- function createSessionHandler(sessionManager = null) {
76
- // Create session manager if not provided
77
- const manager = sessionManager || new SessionManager();
78
- let initialized = false;
79
-
80
- /**
81
- * Handle session API requests
82
- * @param {http.IncomingMessage} req - HTTP request
83
- * @param {http.ServerResponse} res - HTTP response
84
- * @returns {Promise<boolean>} True if request was handled
85
- */
86
- return async function handleSessionRequest(req, res) {
87
- const url = new URL(req.url, `http://${req.headers.host}`);
88
- const pathname = url.pathname;
89
-
90
- // Only handle /api/session routes (includes /api/sessions and /api/session/create)
91
- if (!pathname.startsWith('/api/session')) {
92
- return false;
93
- }
94
-
95
- // Initialize manager on first request
96
- if (!initialized) {
97
- try {
98
- await manager.initDB();
99
- initialized = true;
100
- } catch (err) {
101
- console.error('[SessionAPI] Failed to initialize:', err.message);
102
- res.writeHead(500, { 'Content-Type': 'application/json' });
103
- res.end(JSON.stringify({ error: 'Session manager initialization failed' }));
104
- return true;
105
- }
106
- }
107
-
108
- // Set common headers
109
- res.setHeader('Content-Type', 'application/json');
110
- res.setHeader('Access-Control-Allow-Origin', '*');
111
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
112
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
113
-
114
- // Handle CORS preflight
115
- if (req.method === 'OPTIONS') {
116
- res.writeHead(204);
117
- res.end();
118
- return true;
119
- }
120
-
121
- // Authentication check
122
- if (!authenticate(req)) {
123
- res.writeHead(401, { 'Content-Type': 'application/json' });
124
- res.end(JSON.stringify({ error: 'Unauthorized' }));
125
- return true;
126
- }
127
-
128
- try {
129
- // GET /api/sessions - List active sessions
130
- if (pathname === '/api/sessions' && req.method === 'GET') {
131
- const sessions = await manager.getActiveSessions();
132
-
133
- res.writeHead(200);
134
- res.end(
135
- JSON.stringify({
136
- sessions: sessions.map((s) => ({
137
- id: s.id,
138
- projectDir: s.projectDir,
139
- status: s.status,
140
- createdAt: s.createdAt,
141
- lastActive: s.lastActive,
142
- isAlive: s.isAlive,
143
- })),
144
- })
145
- );
146
- return true;
147
- }
148
-
149
- // POST /api/sessions - Create new session
150
- // Alias: POST /api/session/create
151
- if (
152
- (pathname === '/api/sessions' || pathname === '/api/session/create') &&
153
- req.method === 'POST'
154
- ) {
155
- const body = await readBody(req);
156
-
157
- const projectDir = body.projectDir || body.workDir || '.';
158
-
159
- try {
160
- const { sessionId } = await manager.createSession(projectDir);
161
- const wsUrl = getWsUrl(req, sessionId);
162
-
163
- res.writeHead(201);
164
- res.end(
165
- JSON.stringify({
166
- sessionId,
167
- wsUrl,
168
- status: 'active',
169
- })
170
- );
171
- } catch (err) {
172
- console.error('[SessionAPI] Failed to create session:', err.message);
173
- res.writeHead(500);
174
- res.end(JSON.stringify({ error: 'Failed to create session', details: err.message }));
175
- }
176
- return true;
177
- }
178
-
179
- // DELETE /api/sessions/:id - Terminate session
180
- if (pathname.startsWith('/api/sessions/') && req.method === 'DELETE') {
181
- const sessionId = extractSessionId(pathname);
182
-
183
- if (!sessionId) {
184
- res.writeHead(400);
185
- res.end(JSON.stringify({ error: 'Missing session ID' }));
186
- return true;
187
- }
188
-
189
- const terminated = await manager.terminateSession(sessionId);
190
-
191
- if (terminated) {
192
- res.writeHead(200);
193
- res.end(JSON.stringify({ success: true, sessionId }));
194
- } else {
195
- res.writeHead(404);
196
- res.end(JSON.stringify({ error: 'Session not found', sessionId }));
197
- }
198
- return true;
199
- }
200
-
201
- // GET /api/sessions/last-active - Get the most recently active session
202
- // NOTE: This must come BEFORE the generic :id handler to avoid being matched as an ID
203
- if (pathname === '/api/sessions/last-active' && req.method === 'GET') {
204
- const sessions = await manager.getActiveSessions();
205
-
206
- if (sessions.length > 0) {
207
- // Sort by lastActive descending and get the first one
208
- const sorted = sessions.sort((a, b) => (b.lastActive || 0) - (a.lastActive || 0));
209
- const lastSession = sorted[0];
210
-
211
- res.writeHead(200);
212
- res.end(
213
- JSON.stringify({
214
- id: lastSession.id,
215
- projectDir: lastSession.projectDir,
216
- status: lastSession.status,
217
- createdAt: lastSession.createdAt,
218
- lastActive: lastSession.lastActive,
219
- isAlive: lastSession.isAlive,
220
- })
221
- );
222
- } else {
223
- res.writeHead(404);
224
- res.end(JSON.stringify({ error: 'No active sessions found' }));
225
- }
226
- return true;
227
- }
228
-
229
- // GET /api/sessions/:id - Get single session (bonus endpoint)
230
- if (pathname.startsWith('/api/sessions/') && req.method === 'GET') {
231
- const sessionId = extractSessionId(pathname);
232
-
233
- if (!sessionId) {
234
- res.writeHead(400);
235
- res.end(JSON.stringify({ error: 'Missing session ID' }));
236
- return true;
237
- }
238
-
239
- const session = manager.getSession(sessionId);
240
-
241
- if (session) {
242
- res.writeHead(200);
243
- res.end(
244
- JSON.stringify({
245
- id: sessionId,
246
- projectDir: session.projectDir,
247
- status: 'active',
248
- createdAt: session.createdAt,
249
- clientId: session.clientId,
250
- isAlive: session.daemon?.isActive() || false,
251
- })
252
- );
253
- } else {
254
- res.writeHead(404);
255
- res.end(JSON.stringify({ error: 'Session not found', sessionId }));
256
- }
257
- return true;
258
- }
259
-
260
- // Unknown /api/sessions route
261
- res.writeHead(404);
262
- res.end(JSON.stringify({ error: 'Not found' }));
263
- return true;
264
- } catch (err) {
265
- console.error('[SessionAPI] Error:', err.message);
266
- res.writeHead(500);
267
- res.end(JSON.stringify({ error: 'Internal server error', details: err.message }));
268
- return true;
269
- }
270
- };
271
- }
272
-
273
- module.exports = {
274
- createSessionHandler,
275
- readBody,
276
- extractSessionId,
277
- getWsUrl,
278
- DEFAULT_WS_PORT,
279
- };
@@ -1,377 +0,0 @@
1
- /**
2
- * @fileoverview Session Manager - tracks multiple Claude Code sessions in SQLite
3
- * @module mobile/session-manager
4
- * @version 1.5.0
5
- *
6
- * Manages session lifecycle including creation, tracking, and termination.
7
- * Sessions are persisted to SQLite for durability across server restarts.
8
- *
9
- * @example
10
- * const { SessionManager } = require('./session-manager');
11
- * const manager = new SessionManager();
12
- * await manager.initDB();
13
- * const { sessionId, daemon } = await manager.createSession('/path/to/project');
14
- */
15
-
16
- const path = require('path');
17
- const os = require('os');
18
- const { ClaudeDaemon } = require('./daemon.js');
19
-
20
- /**
21
- * Default database path
22
- * @type {string}
23
- */
24
- const DEFAULT_DB_PATH =
25
- process.env.MAMA_DB_PATH || path.join(os.homedir(), '.claude', 'mama-memory.db');
26
-
27
- /**
28
- * Sessions table creation SQL
29
- * @type {string}
30
- */
31
- const CREATE_SESSIONS_TABLE = `
32
- CREATE TABLE IF NOT EXISTS sessions (
33
- id TEXT PRIMARY KEY,
34
- project_dir TEXT NOT NULL,
35
- created_at TEXT DEFAULT (datetime('now')),
36
- last_active TEXT DEFAULT (datetime('now')),
37
- status TEXT DEFAULT 'active',
38
- pid INTEGER,
39
- client_id TEXT
40
- )
41
- `;
42
-
43
- /**
44
- * Create index for status queries
45
- * @type {string}
46
- */
47
- const CREATE_STATUS_INDEX = `
48
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)
49
- `;
50
-
51
- /**
52
- * SessionManager class - manages Claude Code sessions
53
- */
54
- class SessionManager {
55
- /**
56
- * Create a new SessionManager instance
57
- * @param {string} [dbPath] - Path to SQLite database
58
- */
59
- constructor(dbPath = DEFAULT_DB_PATH) {
60
- this.dbPath = dbPath;
61
- this.sessions = new Map();
62
- this.db = null;
63
- this.initialized = false;
64
- }
65
-
66
- /**
67
- * Initialize the SQLite database and create sessions table
68
- * @returns {Promise<void>}
69
- */
70
- async initDB() {
71
- if (this.initialized) {
72
- return;
73
- }
74
-
75
- try {
76
- // Dynamically import better-sqlite3 to handle cases where it's not installed
77
- const Database = require('better-sqlite3');
78
- this.db = new Database(this.dbPath);
79
-
80
- // Create sessions table if not exists
81
- this.db.exec(CREATE_SESSIONS_TABLE);
82
- this.db.exec(CREATE_STATUS_INDEX);
83
-
84
- // Mark any orphaned 'active' sessions as 'terminated'
85
- // (from previous server crashes)
86
- this.db
87
- .prepare(
88
- `
89
- UPDATE sessions
90
- SET status = 'terminated', last_active = datetime('now')
91
- WHERE status = 'active'
92
- `
93
- )
94
- .run();
95
-
96
- this.initialized = true;
97
- console.error(`[SessionManager] Database initialized at ${this.dbPath}`);
98
-
99
- // Setup cleanup on process exit
100
- this._setupProcessCleanup();
101
- } catch (err) {
102
- console.error('[SessionManager] Failed to initialize database:', err.message);
103
- throw err;
104
- }
105
- }
106
-
107
- /**
108
- * Create a new Claude Code session
109
- * @param {string} projectDir - Working directory for the session
110
- * @returns {Promise<{sessionId: string, daemon: ClaudeDaemon}>}
111
- */
112
- async createSession(projectDir) {
113
- if (!this.initialized) {
114
- await this.initDB();
115
- }
116
-
117
- // Generate unique session ID
118
- const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
119
-
120
- // Create daemon instance
121
- const daemon = new ClaudeDaemon(projectDir, sessionId);
122
-
123
- try {
124
- // Spawn the daemon
125
- await daemon.spawn();
126
-
127
- // Insert into database
128
- const stmt = this.db.prepare(`
129
- INSERT INTO sessions (id, project_dir, status, pid)
130
- VALUES (?, ?, 'active', ?)
131
- `);
132
- stmt.run(sessionId, projectDir, daemon.getPid());
133
-
134
- // Store in memory map
135
- this.sessions.set(sessionId, {
136
- daemon,
137
- clientId: null,
138
- projectDir,
139
- createdAt: new Date().toISOString(),
140
- });
141
-
142
- // Setup daemon event handlers
143
- this._setupDaemonHandlers(sessionId, daemon);
144
-
145
- console.error(`[SessionManager] Session ${sessionId} created for ${projectDir}`);
146
-
147
- return { sessionId, daemon };
148
- } catch (err) {
149
- console.error(`[SessionManager] Failed to create session:`, err.message);
150
- // Cleanup on failure
151
- daemon.kill();
152
- throw err;
153
- }
154
- }
155
-
156
- /**
157
- * Get all active sessions
158
- * @returns {Promise<Array<Object>>}
159
- */
160
- async getActiveSessions() {
161
- if (!this.initialized) {
162
- await this.initDB();
163
- }
164
-
165
- const stmt = this.db.prepare(`
166
- SELECT id, project_dir, started_at, last_active_at, status, pid, client_id
167
- FROM sessions
168
- WHERE status = 'active'
169
- ORDER BY started_at DESC
170
- `);
171
-
172
- const rows = stmt.all();
173
-
174
- // Enrich with in-memory data
175
- return rows.map((row) => {
176
- const memSession = this.sessions.get(row.id);
177
- return {
178
- id: row.id,
179
- projectDir: row.project_dir,
180
- createdAt: row.started_at,
181
- lastActive: row.last_active_at,
182
- status: row.status,
183
- pid: row.pid,
184
- clientId: row.client_id,
185
- isAlive: memSession?.daemon?.isActive() || false,
186
- };
187
- });
188
- }
189
-
190
- /**
191
- * Terminate a session by ID
192
- * @param {string} sessionId - Session to terminate
193
- * @returns {Promise<boolean>} True if session was terminated
194
- */
195
- async terminateSession(sessionId) {
196
- if (!this.initialized) {
197
- await this.initDB();
198
- }
199
-
200
- const session = this.sessions.get(sessionId);
201
-
202
- if (session) {
203
- // Kill the daemon
204
- if (session.daemon) {
205
- session.daemon.kill();
206
- }
207
-
208
- // Remove from memory
209
- this.sessions.delete(sessionId);
210
- }
211
-
212
- // Update database
213
- const stmt = this.db.prepare(`
214
- UPDATE sessions
215
- SET status = 'terminated', last_active = datetime('now')
216
- WHERE id = ?
217
- `);
218
- const result = stmt.run(sessionId);
219
-
220
- console.error(`[SessionManager] Session ${sessionId} terminated`);
221
-
222
- return result.changes > 0;
223
- }
224
-
225
- /**
226
- * Get a session by ID
227
- * @param {string} sessionId - Session ID to look up
228
- * @returns {Object|undefined}
229
- */
230
- getSession(sessionId) {
231
- return this.sessions.get(sessionId);
232
- }
233
-
234
- /**
235
- * Update session's last active timestamp
236
- * @param {string} sessionId - Session ID
237
- */
238
- touchSession(sessionId) {
239
- if (!this.db || !this.initialized) {
240
- return;
241
- }
242
-
243
- const stmt = this.db.prepare(`
244
- UPDATE sessions
245
- SET last_active = datetime('now')
246
- WHERE id = ?
247
- `);
248
- stmt.run(sessionId);
249
- }
250
-
251
- /**
252
- * Assign a client to a session
253
- * @param {string} sessionId - Session ID
254
- * @param {string} clientId - Client ID
255
- */
256
- assignClient(sessionId, clientId) {
257
- const session = this.sessions.get(sessionId);
258
- if (session) {
259
- session.clientId = clientId;
260
- }
261
-
262
- if (this.db && this.initialized) {
263
- const stmt = this.db.prepare(`
264
- UPDATE sessions
265
- SET client_id = ?
266
- WHERE id = ?
267
- `);
268
- stmt.run(clientId, sessionId);
269
- }
270
- }
271
-
272
- /**
273
- * Remove client assignment from a session
274
- * @param {string} sessionId - Session ID
275
- */
276
- unassignClient(sessionId) {
277
- const session = this.sessions.get(sessionId);
278
- if (session) {
279
- session.clientId = null;
280
- }
281
-
282
- if (this.db && this.initialized) {
283
- const stmt = this.db.prepare(`
284
- UPDATE sessions
285
- SET client_id = NULL
286
- WHERE id = ?
287
- `);
288
- stmt.run(sessionId);
289
- }
290
- }
291
-
292
- /**
293
- * Get session count
294
- * @returns {number}
295
- */
296
- getSessionCount() {
297
- return this.sessions.size;
298
- }
299
-
300
- /**
301
- * Terminate all active sessions
302
- * @returns {Promise<number>} Number of sessions terminated
303
- */
304
- async terminateAll() {
305
- const sessionIds = Array.from(this.sessions.keys());
306
- let count = 0;
307
-
308
- for (const sessionId of sessionIds) {
309
- if (await this.terminateSession(sessionId)) {
310
- count++;
311
- }
312
- }
313
-
314
- return count;
315
- }
316
-
317
- /**
318
- * Setup daemon event handlers
319
- * @private
320
- */
321
- _setupDaemonHandlers(sessionId, daemon) {
322
- daemon.on('exit', () => {
323
- // Update database on daemon exit
324
- if (this.db && this.initialized) {
325
- const stmt = this.db.prepare(`
326
- UPDATE sessions
327
- SET status = 'terminated', last_active = datetime('now')
328
- WHERE id = ?
329
- `);
330
- stmt.run(sessionId);
331
- }
332
-
333
- // Remove from memory
334
- this.sessions.delete(sessionId);
335
- console.error(`[SessionManager] Session ${sessionId} daemon exited, cleaned up`);
336
- });
337
-
338
- daemon.on('error', (err) => {
339
- console.error(`[SessionManager] Session ${sessionId} daemon error:`, err);
340
- });
341
- }
342
-
343
- /**
344
- * Setup process cleanup handlers
345
- * @private
346
- */
347
- _setupProcessCleanup() {
348
- const cleanup = async () => {
349
- console.error('[SessionManager] Process exiting, cleaning up sessions...');
350
- await this.terminateAll();
351
- if (this.db) {
352
- this.db.close();
353
- }
354
- };
355
-
356
- process.on('exit', cleanup);
357
- process.on('SIGINT', cleanup);
358
- process.on('SIGTERM', cleanup);
359
- }
360
-
361
- /**
362
- * Close the database connection
363
- */
364
- close() {
365
- if (this.db) {
366
- this.db.close();
367
- this.db = null;
368
- this.initialized = false;
369
- }
370
- }
371
- }
372
-
373
- module.exports = {
374
- SessionManager,
375
- DEFAULT_DB_PATH,
376
- CREATE_SESSIONS_TABLE,
377
- };