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