@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.
Files changed (95) hide show
  1. package/.github/workflows/publish.yml +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +381 -0
  4. package/dist/adapters/base.d.ts +79 -0
  5. package/dist/adapters/base.d.ts.map +1 -0
  6. package/dist/adapters/base.js +317 -0
  7. package/dist/adapters/base.js.map +1 -0
  8. package/dist/adapters/gemini.d.ts +12 -0
  9. package/dist/adapters/gemini.d.ts.map +1 -0
  10. package/dist/adapters/gemini.js +284 -0
  11. package/dist/adapters/gemini.js.map +1 -0
  12. package/dist/adapters/index.d.ts +5 -0
  13. package/dist/adapters/index.d.ts.map +1 -0
  14. package/dist/adapters/index.js +5 -0
  15. package/dist/adapters/index.js.map +1 -0
  16. package/dist/adapters/openai.d.ts +12 -0
  17. package/dist/adapters/openai.d.ts.map +1 -0
  18. package/dist/adapters/openai.js +294 -0
  19. package/dist/adapters/openai.js.map +1 -0
  20. package/dist/adapters/rest.d.ts +12 -0
  21. package/dist/adapters/rest.d.ts.map +1 -0
  22. package/dist/adapters/rest.js +265 -0
  23. package/dist/adapters/rest.js.map +1 -0
  24. package/dist/cli/index.d.ts +2 -0
  25. package/dist/cli/index.d.ts.map +1 -0
  26. package/dist/cli/index.js +472 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +3 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/config/manager.d.ts +11 -0
  33. package/dist/config/manager.d.ts.map +1 -0
  34. package/dist/config/manager.js +117 -0
  35. package/dist/config/manager.js.map +1 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/mcp/index.d.ts +2 -0
  41. package/dist/mcp/index.d.ts.map +1 -0
  42. package/dist/mcp/index.js +2 -0
  43. package/dist/mcp/index.js.map +1 -0
  44. package/dist/mcp/server.d.ts +24 -0
  45. package/dist/mcp/server.d.ts.map +1 -0
  46. package/dist/mcp/server.js +539 -0
  47. package/dist/mcp/server.js.map +1 -0
  48. package/dist/server/index.d.ts +2 -0
  49. package/dist/server/index.d.ts.map +1 -0
  50. package/dist/server/index.js +2 -0
  51. package/dist/server/index.js.map +1 -0
  52. package/dist/server/unified.d.ts +21 -0
  53. package/dist/server/unified.d.ts.map +1 -0
  54. package/dist/server/unified.js +397 -0
  55. package/dist/server/unified.js.map +1 -0
  56. package/dist/storage/index.d.ts +23 -0
  57. package/dist/storage/index.d.ts.map +1 -0
  58. package/dist/storage/index.js +92 -0
  59. package/dist/storage/index.js.map +1 -0
  60. package/dist/types.d.ts +404 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +92 -0
  63. package/dist/types.js.map +1 -0
  64. package/dist/utils/crypto.d.ts +17 -0
  65. package/dist/utils/crypto.d.ts.map +1 -0
  66. package/dist/utils/crypto.js +73 -0
  67. package/dist/utils/crypto.js.map +1 -0
  68. package/dist/utils/http.d.ts +6 -0
  69. package/dist/utils/http.d.ts.map +1 -0
  70. package/dist/utils/http.js +43 -0
  71. package/dist/utils/http.js.map +1 -0
  72. package/dist/utils/session.d.ts +19 -0
  73. package/dist/utils/session.d.ts.map +1 -0
  74. package/dist/utils/session.js +112 -0
  75. package/dist/utils/session.js.map +1 -0
  76. package/package.json +50 -0
  77. package/src/adapters/base.ts +411 -0
  78. package/src/adapters/gemini.ts +314 -0
  79. package/src/adapters/index.ts +4 -0
  80. package/src/adapters/openai.ts +324 -0
  81. package/src/adapters/rest.ts +294 -0
  82. package/src/cli/index.ts +640 -0
  83. package/src/cli.ts +2 -0
  84. package/src/config/manager.ts +134 -0
  85. package/src/index.ts +4 -0
  86. package/src/mcp/index.ts +1 -0
  87. package/src/mcp/server.ts +623 -0
  88. package/src/server/index.ts +1 -0
  89. package/src/server/unified.ts +460 -0
  90. package/src/storage/index.ts +112 -0
  91. package/src/types.ts +181 -0
  92. package/src/utils/crypto.ts +100 -0
  93. package/src/utils/http.ts +45 -0
  94. package/src/utils/session.ts +141 -0
  95. 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
+ }