@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,314 @@
1
+ import { BaseAdapter } from './base.js';
2
+ import { EnvCPConfig, GeminiFunctionDeclaration, GeminiFunctionCall, GeminiFunctionResponse, ToolDefinition } from '../types.js';
3
+ import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
4
+ import * as http from 'http';
5
+ import * as url from 'url';
6
+
7
+ export class GeminiAdapter extends BaseAdapter {
8
+ private server: http.Server | null = null;
9
+
10
+ constructor(config: EnvCPConfig, projectPath: string, password?: string) {
11
+ super(config, projectPath, password);
12
+ }
13
+
14
+ protected registerTools(): void {
15
+ const tools: ToolDefinition[] = [
16
+ {
17
+ name: 'envcp_list',
18
+ description: 'List all available environment variable names. Values are never shown.',
19
+ parameters: {
20
+ type: 'object',
21
+ properties: {
22
+ tags: {
23
+ type: 'array',
24
+ items: { type: 'string' },
25
+ description: 'Filter by tags',
26
+ },
27
+ },
28
+ },
29
+ handler: async (params) => this.listVariables(params as { tags?: string[] }),
30
+ },
31
+ {
32
+ name: 'envcp_get',
33
+ description: 'Get an environment variable. Returns masked value by default.',
34
+ parameters: {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string', description: 'Variable name' },
38
+ show_value: { type: 'boolean', description: 'Show actual value (requires user confirmation)' },
39
+ },
40
+ required: ['name'],
41
+ },
42
+ handler: async (params) => this.getVariable(params as { name: string; show_value?: boolean }),
43
+ },
44
+ {
45
+ name: 'envcp_set',
46
+ description: 'Create or update an environment variable.',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: {
50
+ name: { type: 'string', description: 'Variable name' },
51
+ value: { type: 'string', description: 'Variable value' },
52
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
53
+ description: { type: 'string', description: 'Description' },
54
+ },
55
+ required: ['name', 'value'],
56
+ },
57
+ handler: async (params) => this.setVariable(params as any),
58
+ },
59
+ {
60
+ name: 'envcp_delete',
61
+ description: 'Delete an environment variable.',
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ name: { type: 'string', description: 'Variable name' },
66
+ },
67
+ required: ['name'],
68
+ },
69
+ handler: async (params) => this.deleteVariable(params as { name: string }),
70
+ },
71
+ {
72
+ name: 'envcp_sync',
73
+ description: 'Sync variables to .env file.',
74
+ parameters: {
75
+ type: 'object',
76
+ properties: {},
77
+ },
78
+ handler: async () => this.syncToEnv(),
79
+ },
80
+ {
81
+ name: 'envcp_run',
82
+ description: 'Execute a command with environment variables injected.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ command: { type: 'string', description: 'Command to execute' },
87
+ variables: { type: 'array', items: { type: 'string' }, description: 'Variables to inject' },
88
+ },
89
+ required: ['command', 'variables'],
90
+ },
91
+ handler: async (params) => this.runCommand(params as { command: string; variables: string[] }),
92
+ },
93
+ {
94
+ name: 'envcp_check_access',
95
+ description: 'Check if a variable exists and can be accessed.',
96
+ parameters: {
97
+ type: 'object',
98
+ properties: {
99
+ name: { type: 'string', description: 'Variable name' },
100
+ },
101
+ required: ['name'],
102
+ },
103
+ handler: async (params) => this.checkAccess(params as { name: string }),
104
+ },
105
+ ];
106
+
107
+ tools.forEach(tool => this.tools.set(tool.name, tool));
108
+ }
109
+
110
+ // Convert tools to Gemini function declaration format
111
+ getGeminiFunctionDeclarations(): GeminiFunctionDeclaration[] {
112
+ return this.getToolDefinitions().map(tool => ({
113
+ name: tool.name,
114
+ description: tool.description,
115
+ parameters: {
116
+ type: 'object' as const,
117
+ properties: (tool.parameters as any).properties || {},
118
+ required: (tool.parameters as any).required,
119
+ },
120
+ }));
121
+ }
122
+
123
+ // Process Gemini function calls
124
+ async processFunctionCalls(calls: GeminiFunctionCall[]): Promise<GeminiFunctionResponse[]> {
125
+ const results: GeminiFunctionResponse[] = [];
126
+
127
+ for (const call of calls) {
128
+ try {
129
+ const result = await this.callTool(call.name, call.args);
130
+ results.push({
131
+ name: call.name,
132
+ response: { result },
133
+ });
134
+ } catch (error: any) {
135
+ results.push({
136
+ name: call.name,
137
+ response: { error: error.message },
138
+ });
139
+ }
140
+ }
141
+
142
+ return results;
143
+ }
144
+
145
+
146
+ async startServer(port: number, host: string, apiKey?: string): Promise<void> {
147
+ await this.init();
148
+
149
+ this.server = http.createServer(async (req, res) => {
150
+ setCorsHeaders(res);
151
+
152
+ if (req.method === 'OPTIONS') {
153
+ res.writeHead(204);
154
+ res.end();
155
+ return;
156
+ }
157
+
158
+ // API key validation
159
+ if (apiKey) {
160
+ const providedKey = (req.headers['x-goog-api-key'] || req.headers['authorization']?.replace('Bearer ', '')) as string | undefined;
161
+ if (!validateApiKey(providedKey, apiKey)) {
162
+ sendJson(res, 401, { error: { code: 401, message: 'Invalid API key', status: 'UNAUTHENTICATED' } });
163
+ return;
164
+ }
165
+ }
166
+
167
+ const parsedUrl = url.parse(req.url || '/', true);
168
+ const pathname = parsedUrl.pathname || '/';
169
+
170
+ try {
171
+ // Gemini-compatible endpoints
172
+
173
+ // GET /v1/models - List models
174
+ if (pathname === '/v1/models' && req.method === 'GET') {
175
+ sendJson(res, 200, {
176
+ models: [{
177
+ name: 'models/envcp-1.0',
178
+ displayName: 'EnvCP Tool Server',
179
+ description: 'Environment variable management tools',
180
+ supportedGenerationMethods: ['generateContent'],
181
+ }],
182
+ });
183
+ return;
184
+ }
185
+
186
+ // GET /v1/tools - List available tools/functions
187
+ if (pathname === '/v1/tools' && req.method === 'GET') {
188
+ sendJson(res, 200, {
189
+ tools: [{
190
+ functionDeclarations: this.getGeminiFunctionDeclarations(),
191
+ }],
192
+ });
193
+ return;
194
+ }
195
+
196
+ // POST /v1/functions/call - Call a function directly
197
+ if (pathname === '/v1/functions/call' && req.method === 'POST') {
198
+ const body = await parseBody(req);
199
+ const { name, args } = body as { name: string; args: Record<string, unknown> };
200
+
201
+ if (!name) {
202
+ sendJson(res, 400, { error: { code: 400, message: 'Function name required', status: 'INVALID_ARGUMENT' } });
203
+ return;
204
+ }
205
+
206
+ const result = await this.callTool(name, args || {});
207
+ sendJson(res, 200, {
208
+ name,
209
+ response: { result },
210
+ });
211
+ return;
212
+ }
213
+
214
+ // POST /v1/function_calls - Process function calls (batch)
215
+ if (pathname === '/v1/function_calls' && req.method === 'POST') {
216
+ const body = await parseBody(req);
217
+ const { functionCalls } = body as { functionCalls: GeminiFunctionCall[] };
218
+
219
+ if (!functionCalls || !Array.isArray(functionCalls)) {
220
+ sendJson(res, 400, { error: { code: 400, message: 'functionCalls array required', status: 'INVALID_ARGUMENT' } });
221
+ return;
222
+ }
223
+
224
+ const results = await this.processFunctionCalls(functionCalls);
225
+ sendJson(res, 200, {
226
+ functionResponses: results,
227
+ });
228
+ return;
229
+ }
230
+
231
+ // POST /v1/models/envcp:generateContent - Gemini-style content generation
232
+ if ((pathname === '/v1/models/envcp:generateContent' || pathname === '/v1beta/models/envcp:generateContent') && req.method === 'POST') {
233
+ const body = await parseBody(req);
234
+ const contents = body.contents as Array<{ parts: Array<{ functionCall?: GeminiFunctionCall }> }> | undefined;
235
+
236
+ // Look for function calls in the content
237
+ const functionCalls: GeminiFunctionCall[] = [];
238
+ if (contents) {
239
+ for (const content of contents) {
240
+ for (const part of content.parts || []) {
241
+ if (part.functionCall) {
242
+ functionCalls.push(part.functionCall);
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ if (functionCalls.length > 0) {
249
+ const results = await this.processFunctionCalls(functionCalls);
250
+ sendJson(res, 200, {
251
+ candidates: [{
252
+ content: {
253
+ parts: results.map(r => ({
254
+ functionResponse: r,
255
+ })),
256
+ role: 'model',
257
+ },
258
+ finishReason: 'STOP',
259
+ }],
260
+ });
261
+ return;
262
+ }
263
+
264
+ // Return available tools if no function calls
265
+ sendJson(res, 200, {
266
+ candidates: [{
267
+ content: {
268
+ parts: [{
269
+ text: 'EnvCP tools available. Use function calling to interact with environment variables.',
270
+ }],
271
+ role: 'model',
272
+ },
273
+ finishReason: 'STOP',
274
+ }],
275
+ availableTools: [{
276
+ functionDeclarations: this.getGeminiFunctionDeclarations(),
277
+ }],
278
+ });
279
+ return;
280
+ }
281
+
282
+ // Health check
283
+ if (pathname === '/v1/health' || pathname === '/') {
284
+ sendJson(res, 200, {
285
+ status: 'ok',
286
+ version: '1.0.0',
287
+ mode: 'gemini',
288
+ endpoints: ['/v1/models', '/v1/tools', '/v1/functions/call', '/v1/function_calls', '/v1/models/envcp:generateContent'],
289
+ });
290
+ return;
291
+ }
292
+
293
+ // 404
294
+ sendJson(res, 404, { error: { code: 404, message: 'Not found', status: 'NOT_FOUND' } });
295
+
296
+ } catch (error: any) {
297
+ sendJson(res, 500, { error: { code: 500, message: error.message, status: 'INTERNAL' } });
298
+ }
299
+ });
300
+
301
+ return new Promise((resolve) => {
302
+ this.server!.listen(port, host, () => {
303
+ resolve();
304
+ });
305
+ });
306
+ }
307
+
308
+ stopServer(): void {
309
+ if (this.server) {
310
+ this.server.close();
311
+ this.server = null;
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,4 @@
1
+ export { BaseAdapter } from './base.js';
2
+ export { RESTAdapter } from './rest.js';
3
+ export { OpenAIAdapter } from './openai.js';
4
+ export { GeminiAdapter } from './gemini.js';
@@ -0,0 +1,324 @@
1
+ import { BaseAdapter } from './base.js';
2
+ import { EnvCPConfig, OpenAIFunction, OpenAIToolCall, OpenAIMessage, ToolDefinition } from '../types.js';
3
+ import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
4
+ import * as http from 'http';
5
+ import * as url from 'url';
6
+
7
+ export class OpenAIAdapter extends BaseAdapter {
8
+ private server: http.Server | null = null;
9
+
10
+ constructor(config: EnvCPConfig, projectPath: string, password?: string) {
11
+ super(config, projectPath, password);
12
+ }
13
+
14
+ protected registerTools(): void {
15
+ const tools: ToolDefinition[] = [
16
+ {
17
+ name: 'envcp_list',
18
+ description: 'List all available environment variable names. Values are never shown.',
19
+ parameters: {
20
+ type: 'object',
21
+ properties: {
22
+ tags: {
23
+ type: 'array',
24
+ items: { type: 'string' },
25
+ description: 'Filter by tags',
26
+ },
27
+ },
28
+ },
29
+ handler: async (params) => this.listVariables(params as { tags?: string[] }),
30
+ },
31
+ {
32
+ name: 'envcp_get',
33
+ description: 'Get an environment variable. Returns masked value by default.',
34
+ parameters: {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string', description: 'Variable name' },
38
+ show_value: { type: 'boolean', description: 'Show actual value (requires user confirmation)' },
39
+ },
40
+ required: ['name'],
41
+ },
42
+ handler: async (params) => this.getVariable(params as { name: string; show_value?: boolean }),
43
+ },
44
+ {
45
+ name: 'envcp_set',
46
+ description: 'Create or update an environment variable.',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: {
50
+ name: { type: 'string', description: 'Variable name' },
51
+ value: { type: 'string', description: 'Variable value' },
52
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
53
+ description: { type: 'string', description: 'Description' },
54
+ },
55
+ required: ['name', 'value'],
56
+ },
57
+ handler: async (params) => this.setVariable(params as any),
58
+ },
59
+ {
60
+ name: 'envcp_delete',
61
+ description: 'Delete an environment variable.',
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ name: { type: 'string', description: 'Variable name' },
66
+ },
67
+ required: ['name'],
68
+ },
69
+ handler: async (params) => this.deleteVariable(params as { name: string }),
70
+ },
71
+ {
72
+ name: 'envcp_sync',
73
+ description: 'Sync variables to .env file.',
74
+ parameters: {
75
+ type: 'object',
76
+ properties: {},
77
+ },
78
+ handler: async () => this.syncToEnv(),
79
+ },
80
+ {
81
+ name: 'envcp_run',
82
+ description: 'Execute a command with environment variables injected.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ command: { type: 'string', description: 'Command to execute' },
87
+ variables: { type: 'array', items: { type: 'string' }, description: 'Variables to inject' },
88
+ },
89
+ required: ['command', 'variables'],
90
+ },
91
+ handler: async (params) => this.runCommand(params as { command: string; variables: string[] }),
92
+ },
93
+ {
94
+ name: 'envcp_check_access',
95
+ description: 'Check if a variable exists and can be accessed.',
96
+ parameters: {
97
+ type: 'object',
98
+ properties: {
99
+ name: { type: 'string', description: 'Variable name' },
100
+ },
101
+ required: ['name'],
102
+ },
103
+ handler: async (params) => this.checkAccess(params as { name: string }),
104
+ },
105
+ ];
106
+
107
+ tools.forEach(tool => this.tools.set(tool.name, tool));
108
+ }
109
+
110
+ // Convert tools to OpenAI function format
111
+ getOpenAIFunctions(): OpenAIFunction[] {
112
+ return this.getToolDefinitions().map(tool => ({
113
+ name: tool.name,
114
+ description: tool.description,
115
+ parameters: {
116
+ type: 'object' as const,
117
+ properties: (tool.parameters as any).properties || {},
118
+ required: (tool.parameters as any).required,
119
+ },
120
+ }));
121
+ }
122
+
123
+ // Process OpenAI tool calls
124
+ async processToolCalls(toolCalls: OpenAIToolCall[]): Promise<OpenAIMessage[]> {
125
+ const results: OpenAIMessage[] = [];
126
+
127
+ for (const call of toolCalls) {
128
+ try {
129
+ const args = JSON.parse(call.function.arguments);
130
+ const result = await this.callTool(call.function.name, args);
131
+ results.push({
132
+ role: 'tool',
133
+ tool_call_id: call.id,
134
+ content: JSON.stringify(result),
135
+ });
136
+ } catch (error: any) {
137
+ results.push({
138
+ role: 'tool',
139
+ tool_call_id: call.id,
140
+ content: JSON.stringify({ error: error.message }),
141
+ });
142
+ }
143
+ }
144
+
145
+ return results;
146
+ }
147
+
148
+
149
+ async startServer(port: number, host: string, apiKey?: string): Promise<void> {
150
+ await this.init();
151
+
152
+ this.server = http.createServer(async (req, res) => {
153
+ setCorsHeaders(res);
154
+
155
+ if (req.method === 'OPTIONS') {
156
+ res.writeHead(204);
157
+ res.end();
158
+ return;
159
+ }
160
+
161
+ // API key validation
162
+ if (apiKey) {
163
+ const providedKey = req.headers['authorization']?.replace('Bearer ', '');
164
+ if (!validateApiKey(providedKey, apiKey)) {
165
+ sendJson(res, 401, { error: { message: 'Invalid API key', type: 'invalid_api_key' } });
166
+ return;
167
+ }
168
+ }
169
+
170
+ const parsedUrl = url.parse(req.url || '/', true);
171
+ const pathname = parsedUrl.pathname || '/';
172
+
173
+ try {
174
+ // OpenAI-compatible endpoints
175
+
176
+ // GET /v1/models - List models (for compatibility)
177
+ if (pathname === '/v1/models' && req.method === 'GET') {
178
+ sendJson(res, 200, {
179
+ object: 'list',
180
+ data: [{
181
+ id: 'envcp-1.0',
182
+ object: 'model',
183
+ created: Date.now(),
184
+ owned_by: 'envcp',
185
+ }],
186
+ });
187
+ return;
188
+ }
189
+
190
+ // GET /v1/functions - List available functions
191
+ if (pathname === '/v1/functions' && req.method === 'GET') {
192
+ sendJson(res, 200, {
193
+ object: 'list',
194
+ data: this.getOpenAIFunctions(),
195
+ });
196
+ return;
197
+ }
198
+
199
+ // POST /v1/functions/call - Call a function directly
200
+ if (pathname === '/v1/functions/call' && req.method === 'POST') {
201
+ const body = await parseBody(req);
202
+ const { name, arguments: args } = body as { name: string; arguments: Record<string, unknown> };
203
+
204
+ if (!name) {
205
+ sendJson(res, 400, { error: { message: 'Function name required', type: 'invalid_request_error' } });
206
+ return;
207
+ }
208
+
209
+ const result = await this.callTool(name, args || {});
210
+ sendJson(res, 200, {
211
+ object: 'function_result',
212
+ name,
213
+ result,
214
+ });
215
+ return;
216
+ }
217
+
218
+ // POST /v1/tool_calls - Process tool calls (batch)
219
+ if (pathname === '/v1/tool_calls' && req.method === 'POST') {
220
+ const body = await parseBody(req);
221
+ const { tool_calls } = body as { tool_calls: OpenAIToolCall[] };
222
+
223
+ if (!tool_calls || !Array.isArray(tool_calls)) {
224
+ sendJson(res, 400, { error: { message: 'tool_calls array required', type: 'invalid_request_error' } });
225
+ return;
226
+ }
227
+
228
+ const results = await this.processToolCalls(tool_calls);
229
+ sendJson(res, 200, {
230
+ object: 'list',
231
+ data: results,
232
+ });
233
+ return;
234
+ }
235
+
236
+ // POST /v1/chat/completions - For integration with proxies
237
+ // This allows tools to be called through a chat completion-like interface
238
+ if (pathname === '/v1/chat/completions' && req.method === 'POST') {
239
+ const body = await parseBody(req);
240
+ const messages = body.messages as OpenAIMessage[] | undefined;
241
+
242
+ if (!messages || !Array.isArray(messages)) {
243
+ sendJson(res, 400, { error: { message: 'messages array required', type: 'invalid_request_error' } });
244
+ return;
245
+ }
246
+
247
+ // Check if last message has tool_calls to process
248
+ const lastMessage = messages[messages.length - 1];
249
+ if (lastMessage?.tool_calls) {
250
+ const results = await this.processToolCalls(lastMessage.tool_calls);
251
+ sendJson(res, 200, {
252
+ id: `chatcmpl-${Date.now()}`,
253
+ object: 'chat.completion',
254
+ created: Math.floor(Date.now() / 1000),
255
+ model: 'envcp-1.0',
256
+ choices: [{
257
+ index: 0,
258
+ message: {
259
+ role: 'assistant',
260
+ content: null,
261
+ tool_calls: null,
262
+ },
263
+ finish_reason: 'tool_calls',
264
+ }],
265
+ tool_results: results,
266
+ });
267
+ return;
268
+ }
269
+
270
+ // Return available tools if no tool_calls
271
+ sendJson(res, 200, {
272
+ id: `chatcmpl-${Date.now()}`,
273
+ object: 'chat.completion',
274
+ created: Math.floor(Date.now() / 1000),
275
+ model: 'envcp-1.0',
276
+ choices: [{
277
+ index: 0,
278
+ message: {
279
+ role: 'assistant',
280
+ content: 'EnvCP tools available. Use function calling to interact with environment variables.',
281
+ },
282
+ finish_reason: 'stop',
283
+ }],
284
+ available_tools: this.getOpenAIFunctions().map(f => ({
285
+ type: 'function',
286
+ function: f,
287
+ })),
288
+ });
289
+ return;
290
+ }
291
+
292
+ // Health check
293
+ if (pathname === '/v1/health' || pathname === '/') {
294
+ sendJson(res, 200, {
295
+ status: 'ok',
296
+ version: '1.0.0',
297
+ mode: 'openai',
298
+ endpoints: ['/v1/models', '/v1/functions', '/v1/functions/call', '/v1/tool_calls', '/v1/chat/completions'],
299
+ });
300
+ return;
301
+ }
302
+
303
+ // 404
304
+ sendJson(res, 404, { error: { message: 'Not found', type: 'not_found' } });
305
+
306
+ } catch (error: any) {
307
+ sendJson(res, 500, { error: { message: error.message, type: 'internal_error' } });
308
+ }
309
+ });
310
+
311
+ return new Promise((resolve) => {
312
+ this.server!.listen(port, host, () => {
313
+ resolve();
314
+ });
315
+ });
316
+ }
317
+
318
+ stopServer(): void {
319
+ if (this.server) {
320
+ this.server.close();
321
+ this.server = null;
322
+ }
323
+ }
324
+ }