@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.
- package/package.json +33 -21
- package/src/config-loader.js +0 -218
- package/src/db-adapter/README.md +0 -110
- package/src/db-adapter/base-adapter.js +0 -91
- package/src/db-adapter/index.js +0 -31
- package/src/db-adapter/sqlite-adapter.js +0 -364
- package/src/db-adapter/statement.js +0 -127
- package/src/db-manager.js +0 -671
- package/src/debug-logger.js +0 -86
- package/src/decision-formatter.js +0 -1276
- package/src/decision-tracker.js +0 -621
- package/src/embedding-cache.js +0 -222
- package/src/embedding-client.js +0 -141
- package/src/embedding-server/index.js +0 -424
- package/src/embedding-server/mobile/auth.js +0 -160
- package/src/embedding-server/mobile/daemon.js +0 -313
- package/src/embedding-server/mobile/output-parser.js +0 -281
- package/src/embedding-server/mobile/session-api.js +0 -279
- package/src/embedding-server/mobile/session-manager.js +0 -377
- package/src/embedding-server/mobile/websocket-handler.js +0 -443
- package/src/embeddings.js +0 -305
- package/src/errors.js +0 -326
- package/src/index.js +0 -41
- package/src/mama-api.js +0 -2614
- package/src/memory-inject.js +0 -174
- package/src/memory-store.js +0 -89
- package/src/notification-manager.js +0 -3
- package/src/ollama-client.js +0 -391
- package/src/outcome-tracker.js +0 -351
- package/src/progress-indicator.js +0 -110
- package/src/query-intent.js +0 -237
- package/src/relevance-scorer.js +0 -286
- package/src/tier-validator.js +0 -269
- package/src/time-formatter.js +0 -98
|
@@ -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
|
-
};
|