@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,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,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
|
+
}
|