@fentz26/envcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +27 -0
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/adapters/base.d.ts +79 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +317 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/gemini.d.ts +12 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +284 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.d.ts +12 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +294 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/rest.d.ts +12 -0
- package/dist/adapters/rest.d.ts.map +1 -0
- package/dist/adapters/rest.js +265 -0
- package/dist/adapters/rest.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +472 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/manager.d.ts +11 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +117 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +24 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +539 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/unified.d.ts +21 -0
- package/dist/server/unified.d.ts.map +1 -0
- package/dist/server/unified.js +397 -0
- package/dist/server/unified.js.map +1 -0
- package/dist/storage/index.d.ts +23 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +92 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types.d.ts +404 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +92 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crypto.d.ts +17 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +73 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/http.d.ts +6 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +43 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/session.d.ts +19 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +112 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +50 -0
- package/src/adapters/base.ts +411 -0
- package/src/adapters/gemini.ts +314 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/openai.ts +324 -0
- package/src/adapters/rest.ts +294 -0
- package/src/cli/index.ts +640 -0
- package/src/cli.ts +2 -0
- package/src/config/manager.ts +134 -0
- package/src/index.ts +4 -0
- package/src/mcp/index.ts +1 -0
- package/src/mcp/server.ts +623 -0
- package/src/server/index.ts +1 -0
- package/src/server/unified.ts +460 -0
- package/src/storage/index.ts +112 -0
- package/src/types.ts +181 -0
- package/src/utils/crypto.ts +100 -0
- package/src/utils/http.ts +45 -0
- package/src/utils/session.ts +141 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { EnvCPConfig, ServerMode, ServerConfig, ClientType } from '../types.js';
|
|
2
|
+
import { RESTAdapter } from '../adapters/rest.js';
|
|
3
|
+
import { OpenAIAdapter } from '../adapters/openai.js';
|
|
4
|
+
import { GeminiAdapter } from '../adapters/gemini.js';
|
|
5
|
+
import { EnvCPServer } from '../mcp/server.js';
|
|
6
|
+
import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
|
|
7
|
+
import * as http from 'http';
|
|
8
|
+
import * as url from 'url';
|
|
9
|
+
|
|
10
|
+
export class UnifiedServer {
|
|
11
|
+
private config: EnvCPConfig;
|
|
12
|
+
private serverConfig: ServerConfig;
|
|
13
|
+
private projectPath: string;
|
|
14
|
+
private password?: string;
|
|
15
|
+
|
|
16
|
+
private restAdapter: RESTAdapter | null = null;
|
|
17
|
+
private openaiAdapter: OpenAIAdapter | null = null;
|
|
18
|
+
private geminiAdapter: GeminiAdapter | null = null;
|
|
19
|
+
private mcpServer: EnvCPServer | null = null;
|
|
20
|
+
private httpServer: http.Server | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(config: EnvCPConfig, serverConfig: ServerConfig, projectPath: string, password?: string) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.serverConfig = serverConfig;
|
|
25
|
+
this.projectPath = projectPath;
|
|
26
|
+
this.password = password;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Detect client type from request headers
|
|
30
|
+
detectClientType(req: http.IncomingMessage): ClientType {
|
|
31
|
+
const userAgent = req.headers['user-agent']?.toLowerCase() || '';
|
|
32
|
+
const contentType = req.headers['content-type'] || '';
|
|
33
|
+
const pathname = url.parse(req.url || '/', true).pathname || '/';
|
|
34
|
+
|
|
35
|
+
// Check for OpenAI-style requests
|
|
36
|
+
if (pathname.startsWith('/v1/chat') ||
|
|
37
|
+
pathname.startsWith('/v1/functions') ||
|
|
38
|
+
pathname.startsWith('/v1/tool_calls') ||
|
|
39
|
+
req.headers['openai-organization'] ||
|
|
40
|
+
userAgent.includes('openai')) {
|
|
41
|
+
return 'openai';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for Gemini-style requests
|
|
45
|
+
if (pathname.includes(':generateContent') ||
|
|
46
|
+
pathname.startsWith('/v1beta') ||
|
|
47
|
+
pathname.startsWith('/v1/function_calls') ||
|
|
48
|
+
req.headers['x-goog-api-key'] ||
|
|
49
|
+
userAgent.includes('google') ||
|
|
50
|
+
userAgent.includes('gemini')) {
|
|
51
|
+
return 'gemini';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for MCP (typically stdio, but could be HTTP)
|
|
55
|
+
if (req.headers['x-mcp-version'] ||
|
|
56
|
+
userAgent.includes('mcp') ||
|
|
57
|
+
userAgent.includes('claude')) {
|
|
58
|
+
return 'mcp';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Default to REST for standard HTTP requests
|
|
62
|
+
if (pathname.startsWith('/api')) {
|
|
63
|
+
return 'rest';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async start(): Promise<void> {
|
|
71
|
+
const { mode, port, host, api_key } = this.serverConfig;
|
|
72
|
+
|
|
73
|
+
// MCP mode uses stdio, not HTTP
|
|
74
|
+
if (mode === 'mcp') {
|
|
75
|
+
this.mcpServer = new EnvCPServer(this.config, this.projectPath, this.password);
|
|
76
|
+
await this.mcpServer.start();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize adapters based on mode
|
|
81
|
+
if (mode === 'rest' || mode === 'all' || mode === 'auto') {
|
|
82
|
+
this.restAdapter = new RESTAdapter(this.config, this.projectPath, this.password);
|
|
83
|
+
await this.restAdapter.init();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (mode === 'openai' || mode === 'all' || mode === 'auto') {
|
|
87
|
+
this.openaiAdapter = new OpenAIAdapter(this.config, this.projectPath, this.password);
|
|
88
|
+
await this.openaiAdapter.init();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (mode === 'gemini' || mode === 'all' || mode === 'auto') {
|
|
92
|
+
this.geminiAdapter = new GeminiAdapter(this.config, this.projectPath, this.password);
|
|
93
|
+
await this.geminiAdapter.init();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Single mode - start specific adapter server
|
|
97
|
+
if (mode === 'rest') {
|
|
98
|
+
await this.restAdapter!.startServer(port, host, api_key);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (mode === 'openai') {
|
|
103
|
+
await this.openaiAdapter!.startServer(port, host, api_key);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (mode === 'gemini') {
|
|
108
|
+
await this.geminiAdapter!.startServer(port, host, api_key);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Auto or All mode - unified server that routes based on detection
|
|
113
|
+
this.httpServer = http.createServer(async (req, res) => {
|
|
114
|
+
setCorsHeaders(res);
|
|
115
|
+
|
|
116
|
+
if (req.method === 'OPTIONS') {
|
|
117
|
+
res.writeHead(204);
|
|
118
|
+
res.end();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// API key validation
|
|
123
|
+
if (api_key) {
|
|
124
|
+
const providedKey = (req.headers['x-api-key'] ||
|
|
125
|
+
req.headers['x-goog-api-key'] ||
|
|
126
|
+
req.headers['authorization']?.replace('Bearer ', '')) as string | undefined;
|
|
127
|
+
if (!validateApiKey(providedKey, api_key)) {
|
|
128
|
+
sendJson(res, 401, { error: 'Invalid API key' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parsedUrl = url.parse(req.url || '/', true);
|
|
134
|
+
const pathname = parsedUrl.pathname || '/';
|
|
135
|
+
|
|
136
|
+
// Root endpoint - show server info and detected mode
|
|
137
|
+
if (pathname === '/' && req.method === 'GET') {
|
|
138
|
+
const detectedType = this.serverConfig.auto_detect ? this.detectClientType(req) : 'unknown';
|
|
139
|
+
sendJson(res, 200, {
|
|
140
|
+
name: 'EnvCP Unified Server',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
mode: mode,
|
|
143
|
+
detected_client: detectedType,
|
|
144
|
+
auto_detect: this.serverConfig.auto_detect,
|
|
145
|
+
available_modes: ['rest', 'openai', 'gemini', 'mcp'],
|
|
146
|
+
endpoints: {
|
|
147
|
+
rest: '/api/*',
|
|
148
|
+
openai: '/v1/chat/completions, /v1/functions/*, /v1/tool_calls',
|
|
149
|
+
gemini: '/v1/models/envcp:generateContent, /v1/function_calls',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Detect client type
|
|
156
|
+
let clientType: ClientType = 'unknown';
|
|
157
|
+
if (this.serverConfig.auto_detect) {
|
|
158
|
+
clientType = this.detectClientType(req);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Force mode query param
|
|
162
|
+
const forceMode = parsedUrl.query.mode as string | undefined;
|
|
163
|
+
if (forceMode && ['rest', 'openai', 'gemini'].includes(forceMode)) {
|
|
164
|
+
clientType = forceMode as ClientType;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Route to appropriate adapter
|
|
169
|
+
// REST API routes
|
|
170
|
+
if ((clientType === 'rest' || clientType === 'unknown') && pathname.startsWith('/api')) {
|
|
171
|
+
await this.handleRESTRequest(req, res);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// OpenAI routes
|
|
176
|
+
if (clientType === 'openai' || pathname.startsWith('/v1/chat') || pathname.startsWith('/v1/functions') || pathname === '/v1/tool_calls' || pathname === '/v1/models') {
|
|
177
|
+
await this.handleOpenAIRequest(req, res);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Gemini routes
|
|
182
|
+
if (clientType === 'gemini' || pathname.includes(':generateContent') || pathname === '/v1/function_calls' || pathname === '/v1/tools') {
|
|
183
|
+
await this.handleGeminiRequest(req, res);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Default to REST for unknown paths
|
|
188
|
+
if (pathname.startsWith('/api')) {
|
|
189
|
+
await this.handleRESTRequest(req, res);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 404 with helpful info
|
|
194
|
+
sendJson(res, 404, {
|
|
195
|
+
error: 'Not found',
|
|
196
|
+
hint: 'Use /api/* for REST, /v1/* for OpenAI, or include :generateContent for Gemini',
|
|
197
|
+
available_endpoints: {
|
|
198
|
+
rest: '/api/variables, /api/tools',
|
|
199
|
+
openai: '/v1/functions, /v1/chat/completions',
|
|
200
|
+
gemini: '/v1/models/envcp:generateContent',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
} catch (error: any) {
|
|
205
|
+
sendJson(res, 500, { error: error.message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
this.httpServer!.listen(port, host, () => {
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async handleRESTRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
217
|
+
// Delegate to REST adapter's internal handling
|
|
218
|
+
const parsedUrl = url.parse(req.url || '/', true);
|
|
219
|
+
const pathname = parsedUrl.pathname || '/';
|
|
220
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
221
|
+
|
|
222
|
+
if (!this.restAdapter) {
|
|
223
|
+
sendJson(res, 503, { success: false, error: 'REST adapter not initialized' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const body = await parseBody(req);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
if (segments[0] === 'api') {
|
|
231
|
+
const resource = segments[1];
|
|
232
|
+
|
|
233
|
+
// Health
|
|
234
|
+
if (pathname === '/api/health' || pathname === '/api') {
|
|
235
|
+
sendJson(res, 200, { success: true, data: { status: 'ok', mode: 'rest' }, timestamp: new Date().toISOString() });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Tools
|
|
240
|
+
if (resource === 'tools' && !segments[2] && req.method === 'GET') {
|
|
241
|
+
const tools = this.restAdapter.getToolDefinitions().map(t => ({ name: t.name, description: t.description }));
|
|
242
|
+
sendJson(res, 200, { success: true, data: { tools }, timestamp: new Date().toISOString() });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (resource === 'tools' && segments[2] && req.method === 'POST') {
|
|
247
|
+
const result = await this.restAdapter.callTool(segments[2], body);
|
|
248
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Variables
|
|
253
|
+
if (resource === 'variables') {
|
|
254
|
+
if (!segments[2] && req.method === 'GET') {
|
|
255
|
+
const result = await this.restAdapter.callTool('envcp_list', {});
|
|
256
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!segments[2] && req.method === 'POST') {
|
|
260
|
+
const result = await this.restAdapter.callTool('envcp_set', body);
|
|
261
|
+
sendJson(res, 201, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (segments[2] && req.method === 'GET') {
|
|
265
|
+
const result = await this.restAdapter.callTool('envcp_get', { name: segments[2], show_value: parsedUrl.query.show_value === 'true' });
|
|
266
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (segments[2] && req.method === 'PUT') {
|
|
270
|
+
const result = await this.restAdapter.callTool('envcp_set', { ...body, name: segments[2] });
|
|
271
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (segments[2] && req.method === 'DELETE') {
|
|
275
|
+
const result = await this.restAdapter.callTool('envcp_delete', { name: segments[2] });
|
|
276
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Sync
|
|
282
|
+
if (resource === 'sync' && req.method === 'POST') {
|
|
283
|
+
const result = await this.restAdapter.callTool('envcp_sync', {});
|
|
284
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Run
|
|
289
|
+
if (resource === 'run' && req.method === 'POST') {
|
|
290
|
+
const result = await this.restAdapter.callTool('envcp_run', body);
|
|
291
|
+
sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
sendJson(res, 404, { success: false, error: 'Not found', timestamp: new Date().toISOString() });
|
|
297
|
+
|
|
298
|
+
} catch (error: any) {
|
|
299
|
+
sendJson(res, 500, { success: false, error: error.message, timestamp: new Date().toISOString() });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async handleOpenAIRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
304
|
+
if (!this.openaiAdapter) {
|
|
305
|
+
sendJson(res, 503, { error: { message: 'OpenAI adapter not initialized', type: 'service_unavailable' } });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const parsedUrl = url.parse(req.url || '/', true);
|
|
310
|
+
const pathname = parsedUrl.pathname || '/';
|
|
311
|
+
const body = await parseBody(req);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
if (pathname === '/v1/models' && req.method === 'GET') {
|
|
315
|
+
sendJson(res, 200, {
|
|
316
|
+
object: 'list',
|
|
317
|
+
data: [{ id: 'envcp-1.0', object: 'model', created: Date.now(), owned_by: 'envcp' }],
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (pathname === '/v1/functions' && req.method === 'GET') {
|
|
323
|
+
sendJson(res, 200, { object: 'list', data: this.openaiAdapter.getOpenAIFunctions() });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (pathname === '/v1/functions/call' && req.method === 'POST') {
|
|
328
|
+
const { name, arguments: args } = body as any;
|
|
329
|
+
const result = await this.openaiAdapter.callTool(name, args || {});
|
|
330
|
+
sendJson(res, 200, { object: 'function_result', name, result });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (pathname === '/v1/tool_calls' && req.method === 'POST') {
|
|
335
|
+
const { tool_calls } = body as any;
|
|
336
|
+
const results = await this.openaiAdapter.processToolCalls(tool_calls);
|
|
337
|
+
sendJson(res, 200, { object: 'list', data: results });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (pathname === '/v1/chat/completions' && req.method === 'POST') {
|
|
342
|
+
const messages = (body as any).messages;
|
|
343
|
+
const lastMessage = messages?.[messages.length - 1];
|
|
344
|
+
|
|
345
|
+
if (lastMessage?.tool_calls) {
|
|
346
|
+
const results = await this.openaiAdapter.processToolCalls(lastMessage.tool_calls);
|
|
347
|
+
sendJson(res, 200, {
|
|
348
|
+
id: `chatcmpl-${Date.now()}`,
|
|
349
|
+
object: 'chat.completion',
|
|
350
|
+
model: 'envcp-1.0',
|
|
351
|
+
choices: [{ index: 0, message: { role: 'assistant', content: null }, finish_reason: 'tool_calls' }],
|
|
352
|
+
tool_results: results,
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
sendJson(res, 200, {
|
|
358
|
+
id: `chatcmpl-${Date.now()}`,
|
|
359
|
+
object: 'chat.completion',
|
|
360
|
+
model: 'envcp-1.0',
|
|
361
|
+
choices: [{ index: 0, message: { role: 'assistant', content: 'EnvCP tools available.' }, finish_reason: 'stop' }],
|
|
362
|
+
available_tools: this.openaiAdapter.getOpenAIFunctions().map(f => ({ type: 'function', function: f })),
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
sendJson(res, 404, { error: { message: 'Not found', type: 'not_found' } });
|
|
368
|
+
|
|
369
|
+
} catch (error: any) {
|
|
370
|
+
sendJson(res, 500, { error: { message: error.message, type: 'internal_error' } });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async handleGeminiRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
375
|
+
if (!this.geminiAdapter) {
|
|
376
|
+
sendJson(res, 503, { error: { code: 503, message: 'Gemini adapter not initialized', status: 'UNAVAILABLE' } });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parsedUrl = url.parse(req.url || '/', true);
|
|
381
|
+
const pathname = parsedUrl.pathname || '/';
|
|
382
|
+
const body = await parseBody(req);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
if (pathname === '/v1/tools' && req.method === 'GET') {
|
|
386
|
+
sendJson(res, 200, { tools: [{ functionDeclarations: this.geminiAdapter.getGeminiFunctionDeclarations() }] });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (pathname === '/v1/functions/call' && req.method === 'POST') {
|
|
391
|
+
const { name, args } = body as any;
|
|
392
|
+
const result = await this.geminiAdapter.callTool(name, args || {});
|
|
393
|
+
sendJson(res, 200, { name, response: { result } });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (pathname === '/v1/function_calls' && req.method === 'POST') {
|
|
398
|
+
const { functionCalls } = body as any;
|
|
399
|
+
const results = await this.geminiAdapter.processFunctionCalls(functionCalls);
|
|
400
|
+
sendJson(res, 200, { functionResponses: results });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (pathname.includes(':generateContent') && req.method === 'POST') {
|
|
405
|
+
const contents = (body as any).contents;
|
|
406
|
+
const functionCalls: any[] = [];
|
|
407
|
+
|
|
408
|
+
if (contents) {
|
|
409
|
+
for (const content of contents) {
|
|
410
|
+
for (const part of content.parts || []) {
|
|
411
|
+
if (part.functionCall) functionCalls.push(part.functionCall);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (functionCalls.length > 0) {
|
|
417
|
+
const results = await this.geminiAdapter.processFunctionCalls(functionCalls);
|
|
418
|
+
sendJson(res, 200, {
|
|
419
|
+
candidates: [{
|
|
420
|
+
content: { parts: results.map(r => ({ functionResponse: r })), role: 'model' },
|
|
421
|
+
finishReason: 'STOP',
|
|
422
|
+
}],
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
sendJson(res, 200, {
|
|
428
|
+
candidates: [{
|
|
429
|
+
content: { parts: [{ text: 'EnvCP tools available.' }], role: 'model' },
|
|
430
|
+
finishReason: 'STOP',
|
|
431
|
+
}],
|
|
432
|
+
availableTools: [{ functionDeclarations: this.geminiAdapter.getGeminiFunctionDeclarations() }],
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
sendJson(res, 404, { error: { code: 404, message: 'Not found', status: 'NOT_FOUND' } });
|
|
438
|
+
|
|
439
|
+
} catch (error: any) {
|
|
440
|
+
sendJson(res, 500, { error: { code: 500, message: error.message, status: 'INTERNAL' } });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
stop(): void {
|
|
446
|
+
if (this.httpServer) {
|
|
447
|
+
this.httpServer.close();
|
|
448
|
+
this.httpServer = null;
|
|
449
|
+
}
|
|
450
|
+
if (this.restAdapter) {
|
|
451
|
+
this.restAdapter.stopServer();
|
|
452
|
+
}
|
|
453
|
+
if (this.openaiAdapter) {
|
|
454
|
+
this.openaiAdapter.stopServer();
|
|
455
|
+
}
|
|
456
|
+
if (this.geminiAdapter) {
|
|
457
|
+
this.geminiAdapter.stopServer();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Variable, OperationLog } from '../types.js';
|
|
4
|
+
import { encrypt, decrypt } from '../utils/crypto.js';
|
|
5
|
+
|
|
6
|
+
export class StorageManager {
|
|
7
|
+
private storePath: string;
|
|
8
|
+
private encrypted: boolean;
|
|
9
|
+
private password?: string;
|
|
10
|
+
|
|
11
|
+
constructor(storePath: string, encrypted: boolean = true) {
|
|
12
|
+
this.storePath = storePath;
|
|
13
|
+
this.encrypted = encrypted;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setPassword(password: string): void {
|
|
17
|
+
this.password = password;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async load(): Promise<Record<string, Variable>> {
|
|
21
|
+
if (!await fs.pathExists(this.storePath)) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const data = await fs.readFile(this.storePath, 'utf8');
|
|
26
|
+
|
|
27
|
+
if (this.encrypted && this.password) {
|
|
28
|
+
try {
|
|
29
|
+
const decrypted = decrypt(data, this.password);
|
|
30
|
+
return JSON.parse(decrypted);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error('Failed to decrypt storage. Invalid password or corrupted data.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return JSON.parse(data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async save(variables: Record<string, Variable>): Promise<void> {
|
|
40
|
+
const data = JSON.stringify(variables, null, 2);
|
|
41
|
+
|
|
42
|
+
await fs.ensureDir(path.dirname(this.storePath));
|
|
43
|
+
|
|
44
|
+
if (this.encrypted && this.password) {
|
|
45
|
+
const encryptedData = encrypt(data, this.password);
|
|
46
|
+
await fs.writeFile(this.storePath, encryptedData, 'utf8');
|
|
47
|
+
} else {
|
|
48
|
+
await fs.writeFile(this.storePath, data, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get(name: string): Promise<Variable | undefined> {
|
|
53
|
+
const variables = await this.load();
|
|
54
|
+
return variables[name];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async set(name: string, variable: Variable): Promise<void> {
|
|
58
|
+
const variables = await this.load();
|
|
59
|
+
variables[name] = variable;
|
|
60
|
+
await this.save(variables);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async delete(name: string): Promise<boolean> {
|
|
64
|
+
const variables = await this.load();
|
|
65
|
+
if (variables[name]) {
|
|
66
|
+
delete variables[name];
|
|
67
|
+
await this.save(variables);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async list(): Promise<string[]> {
|
|
74
|
+
const variables = await this.load();
|
|
75
|
+
return Object.keys(variables);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async exists(): Promise<boolean> {
|
|
79
|
+
return fs.pathExists(this.storePath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class LogManager {
|
|
84
|
+
private logDir: string;
|
|
85
|
+
|
|
86
|
+
constructor(logDir: string) {
|
|
87
|
+
this.logDir = logDir;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async init(): Promise<void> {
|
|
91
|
+
await fs.ensureDir(this.logDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async log(entry: OperationLog): Promise<void> {
|
|
95
|
+
const date = new Date().toISOString().split('T')[0];
|
|
96
|
+
const logFile = path.join(this.logDir, `operations-${date}.log`);
|
|
97
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
98
|
+
await fs.appendFile(logFile, logLine);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getLogs(date?: string): Promise<OperationLog[]> {
|
|
102
|
+
const logDate = date || new Date().toISOString().split('T')[0];
|
|
103
|
+
const logFile = path.join(this.logDir, `operations-${logDate}.log`);
|
|
104
|
+
|
|
105
|
+
if (!await fs.pathExists(logFile)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = await fs.readFile(logFile, 'utf8');
|
|
110
|
+
return content.trim().split('\n').map(line => JSON.parse(line));
|
|
111
|
+
}
|
|
112
|
+
}
|