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.
- package/README.md +22 -9
- package/agent-runtime-server/package-lock.json +4377 -0
- package/agent-runtime-server/package.json +36 -0
- package/agent-runtime-server/scripts/fix-node-pty.js +67 -0
- package/agent-runtime-server/server/agent-session-service.js +816 -0
- package/agent-runtime-server/server/claude-sdk.js +836 -0
- package/agent-runtime-server/server/cli.js +330 -0
- package/agent-runtime-server/server/constants/config.js +5 -0
- package/agent-runtime-server/server/cursor-cli.js +335 -0
- package/agent-runtime-server/server/database/db.js +653 -0
- package/agent-runtime-server/server/database/init.sql +99 -0
- package/agent-runtime-server/server/gemini-cli.js +460 -0
- package/agent-runtime-server/server/gemini-response-handler.js +79 -0
- package/agent-runtime-server/server/index.js +2569 -0
- package/agent-runtime-server/server/load-env.js +32 -0
- package/agent-runtime-server/server/middleware/auth.js +132 -0
- package/agent-runtime-server/server/openai-codex.js +512 -0
- package/agent-runtime-server/server/projects.js +2594 -0
- package/agent-runtime-server/server/providers/claude/adapter.js +278 -0
- package/agent-runtime-server/server/providers/codex/adapter.js +248 -0
- package/agent-runtime-server/server/providers/cursor/adapter.js +353 -0
- package/agent-runtime-server/server/providers/gemini/adapter.js +186 -0
- package/agent-runtime-server/server/providers/registry.js +44 -0
- package/agent-runtime-server/server/providers/types.js +119 -0
- package/agent-runtime-server/server/providers/utils.js +29 -0
- package/agent-runtime-server/server/routes/agent-sessions.js +238 -0
- package/agent-runtime-server/server/routes/agent.js +1244 -0
- package/agent-runtime-server/server/routes/auth.js +144 -0
- package/agent-runtime-server/server/routes/cli-auth.js +478 -0
- package/agent-runtime-server/server/routes/codex.js +329 -0
- package/agent-runtime-server/server/routes/commands.js +596 -0
- package/agent-runtime-server/server/routes/cursor.js +798 -0
- package/agent-runtime-server/server/routes/gemini.js +24 -0
- package/agent-runtime-server/server/routes/git.js +1508 -0
- package/agent-runtime-server/server/routes/mcp-utils.js +48 -0
- package/agent-runtime-server/server/routes/mcp.js +552 -0
- package/agent-runtime-server/server/routes/messages.js +61 -0
- package/agent-runtime-server/server/routes/plugins.js +307 -0
- package/agent-runtime-server/server/routes/projects.js +548 -0
- package/agent-runtime-server/server/routes/settings.js +276 -0
- package/agent-runtime-server/server/routes/taskmaster.js +1963 -0
- package/agent-runtime-server/server/routes/user.js +123 -0
- package/agent-runtime-server/server/services/notification-orchestrator.js +227 -0
- package/agent-runtime-server/server/services/vapid-keys.js +35 -0
- package/agent-runtime-server/server/sessionManager.js +226 -0
- package/agent-runtime-server/server/utils/commandParser.js +303 -0
- package/agent-runtime-server/server/utils/frontmatter.js +18 -0
- package/agent-runtime-server/server/utils/gitConfig.js +34 -0
- package/agent-runtime-server/server/utils/mcp-detector.js +198 -0
- package/agent-runtime-server/server/utils/plugin-loader.js +457 -0
- package/agent-runtime-server/server/utils/plugin-process-manager.js +184 -0
- package/agent-runtime-server/server/utils/taskmaster-websocket.js +129 -0
- package/agent-runtime-server/shared/modelConstants.js +12 -0
- package/agent-runtime-server/shared/modelConstants.test.js +34 -0
- package/agent-runtime-server/shared/networkHosts.js +22 -0
- package/agent-runtime-server/test_sdk.mjs +16 -0
- package/bin/bridges/darwin-x64/nocode-bridge +0 -0
- package/bin/{nocode-local.js → notioncode.js} +2 -8
- package/dist/assets/icon-CQtd7WEB.png +0 -0
- package/dist/assets/index-D_1ZrHDe.js +1 -0
- package/dist/assets/index-DhCWie1Z.css +1 -0
- package/dist/assets/index-DkGqIiwF.js +689 -0
- package/dist/index.html +46 -0
- package/dist/onboarding/step1_create.png +0 -0
- package/dist/onboarding/step2_capabilities.png +0 -0
- package/dist/onboarding/step2b_content_access.png +0 -0
- package/dist/onboarding/step2c_page_access.png +0 -0
- package/dist/onboarding/step3_token.png +0 -0
- package/dist/onboarding/step4_webhook.png +0 -0
- package/dist/onboarding/step6a_verify.png +0 -0
- package/dist/onboarding/step6b_copy_verify_token.png +0 -0
- package/dist/tinyfish-fish-only.png +0 -0
- package/lib/certs.js +332 -0
- package/lib/install.js +48 -4
- package/lib/start.js +346 -29
- package/package.json +10 -4
- package/src/shared/modelRegistry.d.ts +24 -0
- 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;
|