notioncode 0.1.0 → 0.1.2

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.
Files changed (78) hide show
  1. package/README.md +22 -9
  2. package/agent-runtime-server/package-lock.json +4377 -0
  3. package/agent-runtime-server/package.json +36 -0
  4. package/agent-runtime-server/scripts/fix-node-pty.js +67 -0
  5. package/agent-runtime-server/server/agent-session-service.js +816 -0
  6. package/agent-runtime-server/server/claude-sdk.js +836 -0
  7. package/agent-runtime-server/server/cli.js +330 -0
  8. package/agent-runtime-server/server/constants/config.js +5 -0
  9. package/agent-runtime-server/server/cursor-cli.js +335 -0
  10. package/agent-runtime-server/server/database/db.js +653 -0
  11. package/agent-runtime-server/server/database/init.sql +99 -0
  12. package/agent-runtime-server/server/gemini-cli.js +460 -0
  13. package/agent-runtime-server/server/gemini-response-handler.js +79 -0
  14. package/agent-runtime-server/server/index.js +2569 -0
  15. package/agent-runtime-server/server/load-env.js +32 -0
  16. package/agent-runtime-server/server/middleware/auth.js +132 -0
  17. package/agent-runtime-server/server/openai-codex.js +512 -0
  18. package/agent-runtime-server/server/projects.js +2594 -0
  19. package/agent-runtime-server/server/providers/claude/adapter.js +278 -0
  20. package/agent-runtime-server/server/providers/codex/adapter.js +248 -0
  21. package/agent-runtime-server/server/providers/cursor/adapter.js +353 -0
  22. package/agent-runtime-server/server/providers/gemini/adapter.js +186 -0
  23. package/agent-runtime-server/server/providers/registry.js +44 -0
  24. package/agent-runtime-server/server/providers/types.js +119 -0
  25. package/agent-runtime-server/server/providers/utils.js +29 -0
  26. package/agent-runtime-server/server/routes/agent-sessions.js +238 -0
  27. package/agent-runtime-server/server/routes/agent.js +1244 -0
  28. package/agent-runtime-server/server/routes/auth.js +144 -0
  29. package/agent-runtime-server/server/routes/cli-auth.js +478 -0
  30. package/agent-runtime-server/server/routes/codex.js +329 -0
  31. package/agent-runtime-server/server/routes/commands.js +596 -0
  32. package/agent-runtime-server/server/routes/cursor.js +798 -0
  33. package/agent-runtime-server/server/routes/gemini.js +24 -0
  34. package/agent-runtime-server/server/routes/git.js +1508 -0
  35. package/agent-runtime-server/server/routes/mcp-utils.js +48 -0
  36. package/agent-runtime-server/server/routes/mcp.js +552 -0
  37. package/agent-runtime-server/server/routes/messages.js +61 -0
  38. package/agent-runtime-server/server/routes/plugins.js +307 -0
  39. package/agent-runtime-server/server/routes/projects.js +548 -0
  40. package/agent-runtime-server/server/routes/settings.js +276 -0
  41. package/agent-runtime-server/server/routes/taskmaster.js +1963 -0
  42. package/agent-runtime-server/server/routes/user.js +123 -0
  43. package/agent-runtime-server/server/services/notification-orchestrator.js +227 -0
  44. package/agent-runtime-server/server/services/vapid-keys.js +35 -0
  45. package/agent-runtime-server/server/sessionManager.js +226 -0
  46. package/agent-runtime-server/server/utils/commandParser.js +303 -0
  47. package/agent-runtime-server/server/utils/frontmatter.js +18 -0
  48. package/agent-runtime-server/server/utils/gitConfig.js +34 -0
  49. package/agent-runtime-server/server/utils/mcp-detector.js +198 -0
  50. package/agent-runtime-server/server/utils/plugin-loader.js +457 -0
  51. package/agent-runtime-server/server/utils/plugin-process-manager.js +184 -0
  52. package/agent-runtime-server/server/utils/taskmaster-websocket.js +129 -0
  53. package/agent-runtime-server/shared/modelConstants.js +12 -0
  54. package/agent-runtime-server/shared/modelConstants.test.js +34 -0
  55. package/agent-runtime-server/shared/networkHosts.js +22 -0
  56. package/agent-runtime-server/test_sdk.mjs +16 -0
  57. package/bin/bridges/darwin-x64/nocode-bridge +0 -0
  58. package/bin/{nocode-local.js → notioncode.js} +2 -8
  59. package/dist/assets/icon-CQtd7WEB.png +0 -0
  60. package/dist/assets/index-D_1ZrHDe.js +1 -0
  61. package/dist/assets/index-DhCWie1Z.css +1 -0
  62. package/dist/assets/index-DkGqIiwF.js +689 -0
  63. package/dist/index.html +46 -0
  64. package/dist/onboarding/step1_create.png +0 -0
  65. package/dist/onboarding/step2_capabilities.png +0 -0
  66. package/dist/onboarding/step2b_content_access.png +0 -0
  67. package/dist/onboarding/step2c_page_access.png +0 -0
  68. package/dist/onboarding/step3_token.png +0 -0
  69. package/dist/onboarding/step4_webhook.png +0 -0
  70. package/dist/onboarding/step6a_verify.png +0 -0
  71. package/dist/onboarding/step6b_copy_verify_token.png +0 -0
  72. package/dist/tinyfish-fish-only.png +0 -0
  73. package/lib/certs.js +332 -0
  74. package/lib/install.js +48 -4
  75. package/lib/start.js +346 -29
  76. package/package.json +10 -4
  77. package/src/shared/modelRegistry.d.ts +24 -0
  78. package/src/shared/modelRegistry.js +163 -0
@@ -0,0 +1,123 @@
1
+ import express from 'express';
2
+ import { userDb } from '../database/db.js';
3
+ import { authenticateToken } from '../middleware/auth.js';
4
+ import { getSystemGitConfig } from '../utils/gitConfig.js';
5
+ import { spawn } from 'child_process';
6
+
7
+ const router = express.Router();
8
+
9
+ function spawnAsync(command, args, options = {}) {
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(command, args, { ...options, shell: false });
12
+ let stdout = '';
13
+ let stderr = '';
14
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
15
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
16
+ child.on('error', (error) => { reject(error); });
17
+ child.on('close', (code) => {
18
+ if (code === 0) { resolve({ stdout, stderr }); return; }
19
+ const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
20
+ error.code = code;
21
+ error.stdout = stdout;
22
+ error.stderr = stderr;
23
+ reject(error);
24
+ });
25
+ });
26
+ }
27
+
28
+ router.get('/git-config', authenticateToken, async (req, res) => {
29
+ try {
30
+ const userId = req.user.id;
31
+ let gitConfig = userDb.getGitConfig(userId);
32
+
33
+ // If database is empty, try to get from system git config
34
+ if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) {
35
+ const systemConfig = await getSystemGitConfig();
36
+
37
+ // If system has values, save them to database for this user
38
+ if (systemConfig.git_name || systemConfig.git_email) {
39
+ userDb.updateGitConfig(userId, systemConfig.git_name, systemConfig.git_email);
40
+ gitConfig = systemConfig;
41
+ console.log(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`);
42
+ }
43
+ }
44
+
45
+ res.json({
46
+ success: true,
47
+ gitName: gitConfig?.git_name || null,
48
+ gitEmail: gitConfig?.git_email || null
49
+ });
50
+ } catch (error) {
51
+ console.error('Error getting git config:', error);
52
+ res.status(500).json({ error: 'Failed to get git configuration' });
53
+ }
54
+ });
55
+
56
+ // Apply git config globally via git config --global
57
+ router.post('/git-config', authenticateToken, async (req, res) => {
58
+ try {
59
+ const userId = req.user.id;
60
+ const { gitName, gitEmail } = req.body;
61
+
62
+ if (!gitName || !gitEmail) {
63
+ return res.status(400).json({ error: 'Git name and email are required' });
64
+ }
65
+
66
+ // Validate email format
67
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
68
+ if (!emailRegex.test(gitEmail)) {
69
+ return res.status(400).json({ error: 'Invalid email format' });
70
+ }
71
+
72
+ userDb.updateGitConfig(userId, gitName, gitEmail);
73
+
74
+ try {
75
+ await spawnAsync('git', ['config', '--global', 'user.name', gitName]);
76
+ await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]);
77
+ console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
78
+ } catch (gitError) {
79
+ console.error('Error applying git config:', gitError);
80
+ }
81
+
82
+ res.json({
83
+ success: true,
84
+ gitName,
85
+ gitEmail
86
+ });
87
+ } catch (error) {
88
+ console.error('Error updating git config:', error);
89
+ res.status(500).json({ error: 'Failed to update git configuration' });
90
+ }
91
+ });
92
+
93
+ router.post('/complete-onboarding', authenticateToken, async (req, res) => {
94
+ try {
95
+ const userId = req.user.id;
96
+ userDb.completeOnboarding(userId);
97
+
98
+ res.json({
99
+ success: true,
100
+ message: 'Onboarding completed successfully'
101
+ });
102
+ } catch (error) {
103
+ console.error('Error completing onboarding:', error);
104
+ res.status(500).json({ error: 'Failed to complete onboarding' });
105
+ }
106
+ });
107
+
108
+ router.get('/onboarding-status', authenticateToken, async (req, res) => {
109
+ try {
110
+ const userId = req.user.id;
111
+ const hasCompleted = userDb.hasCompletedOnboarding(userId);
112
+
113
+ res.json({
114
+ success: true,
115
+ hasCompletedOnboarding: hasCompleted
116
+ });
117
+ } catch (error) {
118
+ console.error('Error checking onboarding status:', error);
119
+ res.status(500).json({ error: 'Failed to check onboarding status' });
120
+ }
121
+ });
122
+
123
+ export default router;
@@ -0,0 +1,227 @@
1
+ import webPush from 'web-push';
2
+ import { notificationPreferencesDb, pushSubscriptionsDb, sessionNamesDb } from '../database/db.js';
3
+
4
+ const KIND_TO_PREF_KEY = {
5
+ action_required: 'actionRequired',
6
+ stop: 'stop',
7
+ error: 'error'
8
+ };
9
+
10
+ const PROVIDER_LABELS = {
11
+ claude: 'Claude',
12
+ cursor: 'Cursor',
13
+ codex: 'Codex',
14
+ gemini: 'Gemini',
15
+ system: 'System'
16
+ };
17
+
18
+ const recentEventKeys = new Map();
19
+ const DEDUPE_WINDOW_MS = 20000;
20
+
21
+ const cleanupOldEventKeys = () => {
22
+ const now = Date.now();
23
+ for (const [key, timestamp] of recentEventKeys.entries()) {
24
+ if (now - timestamp > DEDUPE_WINDOW_MS) {
25
+ recentEventKeys.delete(key);
26
+ }
27
+ }
28
+ };
29
+
30
+ function shouldSendPush(preferences, event) {
31
+ const webPushEnabled = Boolean(preferences?.channels?.webPush);
32
+ const prefEventKey = KIND_TO_PREF_KEY[event.kind];
33
+ const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
34
+
35
+ return webPushEnabled && eventEnabled;
36
+ }
37
+
38
+ function isDuplicate(event) {
39
+ cleanupOldEventKeys();
40
+ const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
41
+ if (recentEventKeys.has(key)) {
42
+ return true;
43
+ }
44
+ recentEventKeys.set(key, Date.now());
45
+ return false;
46
+ }
47
+
48
+ function createNotificationEvent({
49
+ provider,
50
+ sessionId = null,
51
+ kind = 'info',
52
+ code = 'generic.info',
53
+ meta = {},
54
+ severity = 'info',
55
+ dedupeKey = null,
56
+ requiresUserAction = false
57
+ }) {
58
+ return {
59
+ provider,
60
+ sessionId,
61
+ kind,
62
+ code,
63
+ meta,
64
+ severity,
65
+ requiresUserAction,
66
+ dedupeKey,
67
+ createdAt: new Date().toISOString()
68
+ };
69
+ }
70
+
71
+ function normalizeErrorMessage(error) {
72
+ if (typeof error === 'string') {
73
+ return error;
74
+ }
75
+
76
+ if (error && typeof error.message === 'string') {
77
+ return error.message;
78
+ }
79
+
80
+ if (error == null) {
81
+ return 'Unknown error';
82
+ }
83
+
84
+ return String(error);
85
+ }
86
+
87
+ function normalizeSessionName(sessionName) {
88
+ if (typeof sessionName !== 'string') {
89
+ return null;
90
+ }
91
+
92
+ const normalized = sessionName.replace(/\s+/g, ' ').trim();
93
+ if (!normalized) {
94
+ return null;
95
+ }
96
+
97
+ return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
98
+ }
99
+
100
+ function resolveSessionName(event) {
101
+ const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
102
+ if (explicitSessionName) {
103
+ return explicitSessionName;
104
+ }
105
+
106
+ if (!event.sessionId || !event.provider) {
107
+ return null;
108
+ }
109
+
110
+ return normalizeSessionName(sessionNamesDb.getName(event.sessionId, event.provider));
111
+ }
112
+
113
+ function buildPushBody(event) {
114
+ const CODE_MAP = {
115
+ 'permission.required': event.meta?.toolName
116
+ ? `Action Required: Tool "${event.meta.toolName}" needs approval`
117
+ : 'Action Required: A tool needs your approval',
118
+ 'run.stopped': event.meta?.stopReason || 'Run Stopped: The run has stopped',
119
+ 'run.failed': event.meta?.error ? `Run Failed: ${event.meta.error}` : 'Run Failed: The run encountered an error',
120
+ 'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification',
121
+ 'push.enabled': 'Push notifications are now enabled!'
122
+ };
123
+ const providerLabel = PROVIDER_LABELS[event.provider] || 'Assistant';
124
+ const sessionName = resolveSessionName(event);
125
+ const message = CODE_MAP[event.code] || 'You have a new notification';
126
+
127
+ return {
128
+ title: sessionName || 'Claude Code UI',
129
+ body: `${providerLabel}: ${message}`,
130
+ data: {
131
+ sessionId: event.sessionId || null,
132
+ code: event.code,
133
+ provider: event.provider || null,
134
+ sessionName,
135
+ tag: `${event.provider || 'assistant'}:${event.sessionId || 'none'}:${event.code}`
136
+ }
137
+ };
138
+ }
139
+
140
+ async function sendWebPush(userId, event) {
141
+ const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
142
+ if (!subscriptions.length) return;
143
+
144
+ const payload = JSON.stringify(buildPushBody(event));
145
+
146
+ const results = await Promise.allSettled(
147
+ subscriptions.map((sub) =>
148
+ webPush.sendNotification(
149
+ {
150
+ endpoint: sub.endpoint,
151
+ keys: {
152
+ p256dh: sub.keys_p256dh,
153
+ auth: sub.keys_auth
154
+ }
155
+ },
156
+ payload
157
+ )
158
+ )
159
+ );
160
+
161
+ // Clean up gone subscriptions (410 Gone or 404)
162
+ results.forEach((result, index) => {
163
+ if (result.status === 'rejected') {
164
+ const statusCode = result.reason?.statusCode;
165
+ if (statusCode === 410 || statusCode === 404) {
166
+ pushSubscriptionsDb.removeSubscription(subscriptions[index].endpoint);
167
+ }
168
+ }
169
+ });
170
+ }
171
+
172
+ function notifyUserIfEnabled({ userId, event }) {
173
+ if (!userId || !event) {
174
+ return;
175
+ }
176
+
177
+ const preferences = notificationPreferencesDb.getPreferences(userId);
178
+ if (!shouldSendPush(preferences, event)) {
179
+ return;
180
+ }
181
+ if (isDuplicate(event)) {
182
+ return;
183
+ }
184
+
185
+ sendWebPush(userId, event).catch((err) => {
186
+ console.error('Web push send error:', err);
187
+ });
188
+ }
189
+
190
+ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
191
+ notifyUserIfEnabled({
192
+ userId,
193
+ event: createNotificationEvent({
194
+ provider,
195
+ sessionId,
196
+ kind: 'stop',
197
+ code: 'run.stopped',
198
+ meta: { stopReason, sessionName },
199
+ severity: 'info',
200
+ dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
201
+ })
202
+ });
203
+ }
204
+
205
+ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
206
+ const errorMessage = normalizeErrorMessage(error);
207
+
208
+ notifyUserIfEnabled({
209
+ userId,
210
+ event: createNotificationEvent({
211
+ provider,
212
+ sessionId,
213
+ kind: 'error',
214
+ code: 'run.failed',
215
+ meta: { error: errorMessage, sessionName },
216
+ severity: 'error',
217
+ dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
218
+ })
219
+ });
220
+ }
221
+
222
+ export {
223
+ createNotificationEvent,
224
+ notifyUserIfEnabled,
225
+ notifyRunStopped,
226
+ notifyRunFailed
227
+ };
@@ -0,0 +1,35 @@
1
+ import webPush from 'web-push';
2
+ import { db } from '../database/db.js';
3
+
4
+ let cachedKeys = null;
5
+
6
+ function ensureVapidKeys() {
7
+ if (cachedKeys) return cachedKeys;
8
+
9
+ const row = db.prepare('SELECT public_key, private_key FROM vapid_keys ORDER BY id DESC LIMIT 1').get();
10
+ if (row) {
11
+ cachedKeys = { publicKey: row.public_key, privateKey: row.private_key };
12
+ return cachedKeys;
13
+ }
14
+
15
+ const keys = webPush.generateVAPIDKeys();
16
+ db.prepare('INSERT INTO vapid_keys (public_key, private_key) VALUES (?, ?)').run(keys.publicKey, keys.privateKey);
17
+ cachedKeys = keys;
18
+ return cachedKeys;
19
+ }
20
+
21
+ function getPublicKey() {
22
+ return ensureVapidKeys().publicKey;
23
+ }
24
+
25
+ function configureWebPush() {
26
+ const keys = ensureVapidKeys();
27
+ webPush.setVapidDetails(
28
+ 'mailto:noreply@claudecodeui.local',
29
+ keys.publicKey,
30
+ keys.privateKey
31
+ );
32
+ console.log('Web Push notifications configured');
33
+ }
34
+
35
+ export { ensureVapidKeys, getPublicKey, configureWebPush };
@@ -0,0 +1,226 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ class SessionManager {
6
+ constructor() {
7
+ // Store sessions in memory with conversation history
8
+ this.sessions = new Map();
9
+ this.maxSessions = 100;
10
+ this.sessionsDir = path.join(os.homedir(), '.gemini', 'sessions');
11
+ this.ready = this.init();
12
+ }
13
+
14
+ async init() {
15
+ await this.initSessionsDir();
16
+ await this.loadSessions();
17
+ }
18
+
19
+ async initSessionsDir() {
20
+ try {
21
+ await fs.mkdir(this.sessionsDir, { recursive: true });
22
+ } catch (error) {
23
+ // console.error('Error creating sessions directory:', error);
24
+ }
25
+ }
26
+
27
+ // Create a new session
28
+ createSession(sessionId, projectPath) {
29
+ const session = {
30
+ id: sessionId,
31
+ projectPath: projectPath,
32
+ messages: [],
33
+ createdAt: new Date(),
34
+ lastActivity: new Date()
35
+ };
36
+
37
+ // Evict oldest session from memory if we exceed limit
38
+ if (this.sessions.size >= this.maxSessions) {
39
+ const oldestKey = this.sessions.keys().next().value;
40
+ if (oldestKey) this.sessions.delete(oldestKey);
41
+ }
42
+
43
+ this.sessions.set(sessionId, session);
44
+ this.saveSession(sessionId);
45
+
46
+ return session;
47
+ }
48
+
49
+ // Add a message to session
50
+ addMessage(sessionId, role, content) {
51
+ let session = this.sessions.get(sessionId);
52
+
53
+ if (!session) {
54
+ // Create session if it doesn't exist
55
+ session = this.createSession(sessionId, '');
56
+ }
57
+
58
+ const message = {
59
+ role: role, // 'user' or 'assistant'
60
+ content: content,
61
+ timestamp: new Date()
62
+ };
63
+
64
+ session.messages.push(message);
65
+ session.lastActivity = new Date();
66
+
67
+ this.saveSession(sessionId);
68
+
69
+ return session;
70
+ }
71
+
72
+ // Get session by ID
73
+ getSession(sessionId) {
74
+ return this.sessions.get(sessionId);
75
+ }
76
+
77
+ // Get all sessions for a project
78
+ getProjectSessions(projectPath) {
79
+ const sessions = [];
80
+
81
+ for (const [id, session] of this.sessions) {
82
+ if (session.projectPath === projectPath) {
83
+ sessions.push({
84
+ id: session.id,
85
+ summary: this.getSessionSummary(session),
86
+ messageCount: session.messages.length,
87
+ lastActivity: session.lastActivity
88
+ });
89
+ }
90
+ }
91
+
92
+ return sessions.sort((a, b) =>
93
+ new Date(b.lastActivity) - new Date(a.lastActivity)
94
+ );
95
+ }
96
+
97
+ // Get session summary
98
+ getSessionSummary(session) {
99
+ if (session.messages.length === 0) {
100
+ return 'New Session';
101
+ }
102
+
103
+ // Find first user message
104
+ const firstUserMessage = session.messages.find(m => m.role === 'user');
105
+ if (firstUserMessage) {
106
+ const content = firstUserMessage.content;
107
+ return content.length > 50 ? content.substring(0, 50) + '...' : content;
108
+ }
109
+
110
+ return 'New Session';
111
+ }
112
+
113
+ // Build conversation context for Gemini
114
+ buildConversationContext(sessionId, maxMessages = 10) {
115
+ const session = this.sessions.get(sessionId);
116
+
117
+ if (!session || session.messages.length === 0) {
118
+ return '';
119
+ }
120
+
121
+ // Get last N messages for context
122
+ const recentMessages = session.messages.slice(-maxMessages);
123
+
124
+ let context = 'Here is the conversation history:\n\n';
125
+
126
+ for (const msg of recentMessages) {
127
+ if (msg.role === 'user') {
128
+ context += `User: ${msg.content}\n`;
129
+ } else {
130
+ context += `Assistant: ${msg.content}\n`;
131
+ }
132
+ }
133
+
134
+ context += '\nBased on the conversation history above, please answer the following:\n';
135
+
136
+ return context;
137
+ }
138
+
139
+ // Prevent path traversal
140
+ _safeFilePath(sessionId) {
141
+ const safeId = String(sessionId).replace(/[/\\]|\.\./g, '');
142
+ return path.join(this.sessionsDir, `${safeId}.json`);
143
+ }
144
+
145
+ // Save session to disk
146
+ async saveSession(sessionId) {
147
+ const session = this.sessions.get(sessionId);
148
+ if (!session) return;
149
+
150
+ try {
151
+ const filePath = this._safeFilePath(sessionId);
152
+ await fs.writeFile(filePath, JSON.stringify(session, null, 2));
153
+ } catch (error) {
154
+ // console.error('Error saving session:', error);
155
+ }
156
+ }
157
+
158
+ // Load sessions from disk
159
+ async loadSessions() {
160
+ try {
161
+ const files = await fs.readdir(this.sessionsDir);
162
+
163
+ for (const file of files) {
164
+ if (file.endsWith('.json')) {
165
+ try {
166
+ const filePath = path.join(this.sessionsDir, file);
167
+ const data = await fs.readFile(filePath, 'utf8');
168
+ const session = JSON.parse(data);
169
+
170
+ // Convert dates
171
+ session.createdAt = new Date(session.createdAt);
172
+ session.lastActivity = new Date(session.lastActivity);
173
+ session.messages.forEach(msg => {
174
+ msg.timestamp = new Date(msg.timestamp);
175
+ });
176
+
177
+ this.sessions.set(session.id, session);
178
+ } catch (error) {
179
+ // console.error(`Error loading session ${file}:`, error);
180
+ }
181
+ }
182
+ }
183
+
184
+ // Enforce eviction after loading to prevent massive memory usage
185
+ while (this.sessions.size > this.maxSessions) {
186
+ const oldestKey = this.sessions.keys().next().value;
187
+ if (oldestKey) this.sessions.delete(oldestKey);
188
+ }
189
+ } catch (error) {
190
+ // console.error('Error loading sessions:', error);
191
+ }
192
+ }
193
+
194
+ // Delete a session
195
+ async deleteSession(sessionId) {
196
+ this.sessions.delete(sessionId);
197
+
198
+ try {
199
+ const filePath = this._safeFilePath(sessionId);
200
+ await fs.unlink(filePath);
201
+ } catch (error) {
202
+ // console.error('Error deleting session file:', error);
203
+ }
204
+ }
205
+
206
+ // Get session messages for display
207
+ getSessionMessages(sessionId) {
208
+ const session = this.sessions.get(sessionId);
209
+ if (!session) return [];
210
+
211
+ return session.messages.map(msg => ({
212
+ type: 'message',
213
+ message: {
214
+ role: msg.role,
215
+ content: msg.content
216
+ },
217
+ timestamp: msg.timestamp.toISOString()
218
+ }));
219
+ }
220
+ }
221
+
222
+ // Singleton instance
223
+ const sessionManager = new SessionManager();
224
+
225
+ export const ready = sessionManager.ready;
226
+ export default sessionManager;