@fentz26/envcp 1.0.1 → 1.0.2
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 +79 -130
- package/__tests__/config.test.ts +65 -0
- package/__tests__/crypto.test.ts +76 -0
- package/__tests__/http.test.ts +49 -0
- package/__tests__/storage.test.ts +94 -0
- package/dist/adapters/base.d.ts +1 -2
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +139 -14
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +13 -99
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +13 -99
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +1 -0
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +16 -13
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +132 -196
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +4 -1
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -16
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -511
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/unified.d.ts +1 -0
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +31 -19
- package/dist/server/unified.js.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +18 -4
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/http.d.ts +13 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +65 -2
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +8 -3
- package/dist/utils/session.js.map +1 -1
- package/jest.config.js +11 -0
- package/package.json +4 -3
- package/src/adapters/base.ts +147 -16
- package/src/adapters/gemini.ts +19 -105
- package/src/adapters/openai.ts +19 -105
- package/src/adapters/rest.ts +19 -15
- package/src/cli/index.ts +135 -259
- package/src/config/manager.ts +4 -1
- package/src/mcp/server.ts +26 -582
- package/src/server/unified.ts +37 -23
- package/src/storage/index.ts +22 -6
- package/src/types.ts +2 -0
- package/src/utils/http.ts +76 -2
- package/src/utils/session.ts +13 -8
package/dist/utils/session.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { SessionSchema } from '../types.js';
|
|
4
4
|
import { generateId, encrypt, decrypt } from './crypto.js';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
5
6
|
export class SessionManager {
|
|
6
7
|
sessionPath;
|
|
7
8
|
session = null;
|
|
@@ -27,9 +28,11 @@ export class SessionManager {
|
|
|
27
28
|
last_access: now.toISOString(),
|
|
28
29
|
};
|
|
29
30
|
this.password = password;
|
|
31
|
+
// Store a verification hash instead of the raw password
|
|
32
|
+
const passwordHash = crypto.createHash('sha256').update(password).digest('hex');
|
|
30
33
|
const sessionData = JSON.stringify({
|
|
31
34
|
session: this.session,
|
|
32
|
-
|
|
35
|
+
passwordHash,
|
|
33
36
|
});
|
|
34
37
|
const encrypted = encrypt(sessionData, password);
|
|
35
38
|
await fs.writeFile(this.sessionPath, encrypted, 'utf8');
|
|
@@ -48,7 +51,8 @@ export class SessionManager {
|
|
|
48
51
|
const decrypted = decrypt(encrypted, pwd);
|
|
49
52
|
const data = JSON.parse(decrypted);
|
|
50
53
|
this.session = SessionSchema.parse(data.session);
|
|
51
|
-
|
|
54
|
+
// Password is verified by successful decryption — no longer stored in file
|
|
55
|
+
this.password = pwd;
|
|
52
56
|
if (new Date() > new Date(this.session.expires)) {
|
|
53
57
|
await this.destroy();
|
|
54
58
|
return null;
|
|
@@ -80,9 +84,10 @@ export class SessionManager {
|
|
|
80
84
|
this.session.expires = expires.toISOString();
|
|
81
85
|
this.session.extensions += 1;
|
|
82
86
|
this.session.last_access = now.toISOString();
|
|
87
|
+
const passwordHash = crypto.createHash('sha256').update(this.password).digest('hex');
|
|
83
88
|
const sessionData = JSON.stringify({
|
|
84
89
|
session: this.session,
|
|
85
|
-
|
|
90
|
+
passwordHash,
|
|
86
91
|
});
|
|
87
92
|
const encrypted = encrypt(sessionData, this.password);
|
|
88
93
|
await fs.writeFile(this.sessionPath, encrypted, 'utf8');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/utils/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAW,aAAa,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/utils/session.ts"],"names":[],"mappings":"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"}
|
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fentz26/envcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for secure environment variable management - Keep your secrets safe from AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
30
|
"chalk": "^4.1.2",
|
|
31
31
|
"commander": "^11.1.0",
|
|
32
|
-
|
|
33
32
|
"dotenv": "^16.3.1",
|
|
34
33
|
"fs-extra": "^11.2.0",
|
|
35
34
|
"inquirer": "^8.2.6",
|
|
@@ -37,11 +36,13 @@
|
|
|
37
36
|
"zod": "^3.22.4"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
|
|
41
39
|
"@types/fs-extra": "^11.0.4",
|
|
42
40
|
"@types/inquirer": "^9.0.7",
|
|
41
|
+
"@types/jest": "^30.0.0",
|
|
43
42
|
"@types/js-yaml": "^4.0.9",
|
|
44
43
|
"@types/node": "^20.10.0",
|
|
44
|
+
"jest": "^30.3.0",
|
|
45
|
+
"ts-jest": "^29.4.9",
|
|
45
46
|
"typescript": "^5.3.0"
|
|
46
47
|
},
|
|
47
48
|
"engines": {
|
package/src/adapters/base.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StorageManager, LogManager } from '../storage/index.js';
|
|
2
2
|
import { EnvCPConfig, Variable, ToolDefinition } from '../types.js';
|
|
3
3
|
import { maskValue } from '../utils/crypto.js';
|
|
4
|
-
import { canAccess, isBlacklisted, canAIActiveCheck,
|
|
4
|
+
import { canAccess, isBlacklisted, canAIActiveCheck, validateVariableName, matchesPattern } from '../config/manager.js';
|
|
5
5
|
import { SessionManager } from '../utils/session.js';
|
|
6
6
|
import * as fs from 'fs-extra';
|
|
7
7
|
import * as path from 'path';
|
|
@@ -40,6 +40,108 @@ export abstract class BaseAdapter {
|
|
|
40
40
|
|
|
41
41
|
protected abstract registerTools(): void;
|
|
42
42
|
|
|
43
|
+
protected registerDefaultTools(): void {
|
|
44
|
+
const tools: ToolDefinition[] = [
|
|
45
|
+
{
|
|
46
|
+
name: 'envcp_list',
|
|
47
|
+
description: 'List all available environment variable names. Values are never shown.',
|
|
48
|
+
parameters: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
handler: async (params) => this.listVariables(params as { tags?: string[] }),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'envcp_get',
|
|
58
|
+
description: 'Get an environment variable. Returns masked value by default.',
|
|
59
|
+
parameters: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
name: { type: 'string', description: 'Variable name' },
|
|
63
|
+
show_value: { type: 'boolean', description: 'Show actual value (requires user confirmation)' },
|
|
64
|
+
},
|
|
65
|
+
required: ['name'],
|
|
66
|
+
},
|
|
67
|
+
handler: async (params) => this.getVariable(params as { name: string; show_value?: boolean }),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'envcp_set',
|
|
71
|
+
description: 'Create or update an environment variable.',
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
name: { type: 'string', description: 'Variable name' },
|
|
76
|
+
value: { type: 'string', description: 'Variable value' },
|
|
77
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
|
|
78
|
+
description: { type: 'string', description: 'Description' },
|
|
79
|
+
},
|
|
80
|
+
required: ['name', 'value'],
|
|
81
|
+
},
|
|
82
|
+
handler: async (params) => this.setVariable(params as { name: string; value: string; tags?: string[]; description?: string }),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'envcp_delete',
|
|
86
|
+
description: 'Delete an environment variable.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
name: { type: 'string', description: 'Variable name' },
|
|
91
|
+
},
|
|
92
|
+
required: ['name'],
|
|
93
|
+
},
|
|
94
|
+
handler: async (params) => this.deleteVariable(params as { name: string }),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'envcp_sync',
|
|
98
|
+
description: 'Sync variables to .env file.',
|
|
99
|
+
parameters: { type: 'object', properties: {} },
|
|
100
|
+
handler: async () => this.syncToEnv(),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'envcp_run',
|
|
104
|
+
description: 'Execute a command with environment variables injected.',
|
|
105
|
+
parameters: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
command: { type: 'string', description: 'Command to execute' },
|
|
109
|
+
variables: { type: 'array', items: { type: 'string' }, description: 'Variables to inject' },
|
|
110
|
+
},
|
|
111
|
+
required: ['command', 'variables'],
|
|
112
|
+
},
|
|
113
|
+
handler: async (params) => this.runCommand(params as { command: string; variables: string[] }),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'envcp_add_to_env',
|
|
117
|
+
description: 'Write a stored variable to a .env file.',
|
|
118
|
+
parameters: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
name: { type: 'string', description: 'Variable name to add' },
|
|
122
|
+
env_file: { type: 'string', description: 'Path to .env file (default: .env)' },
|
|
123
|
+
},
|
|
124
|
+
required: ['name'],
|
|
125
|
+
},
|
|
126
|
+
handler: async (params) => this.addToEnv(params as { name: string; env_file?: string }),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'envcp_check_access',
|
|
130
|
+
description: 'Check if a variable exists and can be accessed.',
|
|
131
|
+
parameters: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
name: { type: 'string', description: 'Variable name to check' },
|
|
135
|
+
},
|
|
136
|
+
required: ['name'],
|
|
137
|
+
},
|
|
138
|
+
handler: async (params) => this.checkAccess(params as { name: string }),
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
tools.forEach(tool => this.tools.set(tool.name, tool));
|
|
143
|
+
}
|
|
144
|
+
|
|
43
145
|
async init(): Promise<void> {
|
|
44
146
|
await this.logs.init();
|
|
45
147
|
await this.sessionManager.init();
|
|
@@ -158,6 +260,10 @@ export abstract class BaseAdapter {
|
|
|
158
260
|
throw new Error('AI write access is disabled');
|
|
159
261
|
}
|
|
160
262
|
|
|
263
|
+
if (!validateVariableName(args.name)) {
|
|
264
|
+
throw new Error(`Invalid variable name '${args.name}'. Must match [A-Za-z_][A-Za-z0-9_]*`);
|
|
265
|
+
}
|
|
266
|
+
|
|
161
267
|
if (isBlacklisted(args.name, this.config)) {
|
|
162
268
|
throw new Error(`Variable '${args.name}' is blacklisted`);
|
|
163
269
|
}
|
|
@@ -233,16 +339,15 @@ export abstract class BaseAdapter {
|
|
|
233
339
|
continue;
|
|
234
340
|
}
|
|
235
341
|
|
|
236
|
-
const excluded = this.config.sync.exclude?.some(pattern =>
|
|
237
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
238
|
-
return regex.test(name);
|
|
239
|
-
});
|
|
342
|
+
const excluded = this.config.sync.exclude?.some(pattern => matchesPattern(name, pattern));
|
|
240
343
|
|
|
241
344
|
if (excluded || !variable.sync_to_env) {
|
|
242
345
|
continue;
|
|
243
346
|
}
|
|
244
347
|
|
|
245
|
-
|
|
348
|
+
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
349
|
+
const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
350
|
+
lines.push(`${name}=${val}`);
|
|
246
351
|
}
|
|
247
352
|
|
|
248
353
|
const envPath = path.join(this.projectPath, this.config.sync.target);
|
|
@@ -270,7 +375,11 @@ export abstract class BaseAdapter {
|
|
|
270
375
|
throw new Error(`Variable '${args.name}' is blacklisted`);
|
|
271
376
|
}
|
|
272
377
|
|
|
273
|
-
const envPath = path.
|
|
378
|
+
const envPath = path.resolve(this.projectPath, args.env_file || '.env');
|
|
379
|
+
if (!envPath.startsWith(path.resolve(this.projectPath))) {
|
|
380
|
+
throw new Error('env_file must be within the project directory');
|
|
381
|
+
}
|
|
382
|
+
|
|
274
383
|
let content = '';
|
|
275
384
|
|
|
276
385
|
if (await fs.pathExists(envPath)) {
|
|
@@ -279,17 +388,20 @@ export abstract class BaseAdapter {
|
|
|
279
388
|
|
|
280
389
|
const envVars = dotenv.parse(content);
|
|
281
390
|
|
|
391
|
+
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
392
|
+
const quotedValue = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
393
|
+
|
|
282
394
|
if (envVars[args.name]) {
|
|
283
395
|
const lines = content.split('\n');
|
|
284
396
|
const newLines = lines.map(line => {
|
|
285
397
|
if (line.startsWith(`${args.name}=`)) {
|
|
286
|
-
return `${args.name}=${
|
|
398
|
+
return `${args.name}=${quotedValue}`;
|
|
287
399
|
}
|
|
288
400
|
return line;
|
|
289
401
|
});
|
|
290
402
|
content = newLines.join('\n');
|
|
291
403
|
} else {
|
|
292
|
-
content += `\n${args.name}=${
|
|
404
|
+
content += `\n${args.name}=${quotedValue}`;
|
|
293
405
|
}
|
|
294
406
|
|
|
295
407
|
await fs.writeFile(envPath, content, 'utf8');
|
|
@@ -308,9 +420,7 @@ export abstract class BaseAdapter {
|
|
|
308
420
|
|
|
309
421
|
protected async checkAccess(args: { name: string }): Promise<{
|
|
310
422
|
name: string;
|
|
311
|
-
exists: boolean;
|
|
312
423
|
accessible: boolean;
|
|
313
|
-
blacklisted: boolean;
|
|
314
424
|
message: string;
|
|
315
425
|
}> {
|
|
316
426
|
const variable = await this.storage.get(args.name);
|
|
@@ -329,10 +439,8 @@ export abstract class BaseAdapter {
|
|
|
329
439
|
|
|
330
440
|
return {
|
|
331
441
|
name: args.name,
|
|
332
|
-
exists,
|
|
333
442
|
accessible,
|
|
334
|
-
|
|
335
|
-
message: accessible ? 'Variable exists and can be accessed' : 'Variable cannot be accessed or does not exist',
|
|
443
|
+
message: accessible ? 'Variable exists and can be accessed' : 'Variable cannot be accessed',
|
|
336
444
|
};
|
|
337
445
|
}
|
|
338
446
|
|
|
@@ -375,10 +483,20 @@ export abstract class BaseAdapter {
|
|
|
375
483
|
stdout: string;
|
|
376
484
|
stderr: string;
|
|
377
485
|
}> {
|
|
486
|
+
if (!this.config.access.allow_ai_execute) {
|
|
487
|
+
throw new Error('AI command execution is disabled');
|
|
488
|
+
}
|
|
489
|
+
|
|
378
490
|
this.validateCommand(args.command);
|
|
379
491
|
|
|
380
492
|
const { spawn } = await import('child_process');
|
|
381
|
-
const { program, args: cmdArgs } = this.parseCommand(args.command);
|
|
493
|
+
const { program: prog, args: cmdArgs } = this.parseCommand(args.command);
|
|
494
|
+
|
|
495
|
+
if (this.config.access.allowed_commands && this.config.access.allowed_commands.length > 0) {
|
|
496
|
+
if (!this.config.access.allowed_commands.includes(prog)) {
|
|
497
|
+
throw new Error(`Command '${prog}' is not in the allowed commands list`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
382
500
|
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
383
501
|
|
|
384
502
|
for (const name of args.variables) {
|
|
@@ -391,19 +509,32 @@ export abstract class BaseAdapter {
|
|
|
391
509
|
}
|
|
392
510
|
}
|
|
393
511
|
|
|
512
|
+
const TIMEOUT_MS = 30000;
|
|
513
|
+
|
|
394
514
|
return new Promise((resolve) => {
|
|
395
|
-
const proc = spawn(
|
|
515
|
+
const proc = spawn(prog, cmdArgs, {
|
|
396
516
|
env,
|
|
397
517
|
cwd: this.projectPath,
|
|
398
518
|
});
|
|
399
519
|
|
|
400
520
|
let stdout = '';
|
|
401
521
|
let stderr = '';
|
|
522
|
+
let killed = false;
|
|
523
|
+
|
|
524
|
+
const timer = setTimeout(() => {
|
|
525
|
+
killed = true;
|
|
526
|
+
proc.kill('SIGTERM');
|
|
527
|
+
setTimeout(() => { if (!proc.killed) proc.kill('SIGKILL'); }, 5000);
|
|
528
|
+
}, TIMEOUT_MS);
|
|
402
529
|
|
|
403
530
|
proc.stdout.on('data', (data) => { stdout += data; });
|
|
404
531
|
proc.stderr.on('data', (data) => { stderr += data; });
|
|
405
532
|
|
|
406
533
|
proc.on('close', (code) => {
|
|
534
|
+
clearTimeout(timer);
|
|
535
|
+
if (killed) {
|
|
536
|
+
stderr += '\n[Process killed: exceeded 30s timeout]';
|
|
537
|
+
}
|
|
407
538
|
resolve({ exitCode: code, stdout, stderr });
|
|
408
539
|
});
|
|
409
540
|
});
|
package/src/adapters/gemini.ts
CHANGED
|
@@ -1,110 +1,18 @@
|
|
|
1
1
|
import { BaseAdapter } from './base.js';
|
|
2
|
-
import { EnvCPConfig, GeminiFunctionDeclaration, GeminiFunctionCall, GeminiFunctionResponse
|
|
3
|
-
import { setCorsHeaders, sendJson, parseBody, validateApiKey } from '../utils/http.js';
|
|
2
|
+
import { EnvCPConfig, GeminiFunctionDeclaration, GeminiFunctionCall, GeminiFunctionResponse } from '../types.js';
|
|
3
|
+
import { setCorsHeaders, sendJson, parseBody, validateApiKey, RateLimiter, rateLimitMiddleware } from '../utils/http.js';
|
|
4
4
|
import * as http from 'http';
|
|
5
|
-
import * as url from 'url';
|
|
6
5
|
|
|
7
6
|
export class GeminiAdapter extends BaseAdapter {
|
|
8
7
|
private server: http.Server | null = null;
|
|
8
|
+
private rateLimiter = new RateLimiter(60, 60000);
|
|
9
9
|
|
|
10
10
|
constructor(config: EnvCPConfig, projectPath: string, password?: string) {
|
|
11
11
|
super(config, projectPath, password);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
protected registerTools(): void {
|
|
15
|
-
|
|
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));
|
|
15
|
+
this.registerDefaultTools();
|
|
108
16
|
}
|
|
109
17
|
|
|
110
18
|
// Convert tools to Gemini function declaration format
|
|
@@ -114,8 +22,8 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
114
22
|
description: tool.description,
|
|
115
23
|
parameters: {
|
|
116
24
|
type: 'object' as const,
|
|
117
|
-
properties: (tool.parameters as
|
|
118
|
-
required: (tool.parameters as
|
|
25
|
+
properties: (tool.parameters as Record<string, unknown>).properties as Record<string, unknown> || {},
|
|
26
|
+
required: (tool.parameters as Record<string, unknown>).required as string[] | undefined,
|
|
119
27
|
},
|
|
120
28
|
}));
|
|
121
29
|
}
|
|
@@ -131,10 +39,11 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
131
39
|
name: call.name,
|
|
132
40
|
response: { result },
|
|
133
41
|
});
|
|
134
|
-
} catch (error:
|
|
42
|
+
} catch (error: unknown) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
44
|
results.push({
|
|
136
45
|
name: call.name,
|
|
137
|
-
response: { error:
|
|
46
|
+
response: { error: message },
|
|
138
47
|
});
|
|
139
48
|
}
|
|
140
49
|
}
|
|
@@ -147,7 +56,7 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
147
56
|
await this.init();
|
|
148
57
|
|
|
149
58
|
this.server = http.createServer(async (req, res) => {
|
|
150
|
-
setCorsHeaders(res);
|
|
59
|
+
setCorsHeaders(res, undefined, req.headers.origin);
|
|
151
60
|
|
|
152
61
|
if (req.method === 'OPTIONS') {
|
|
153
62
|
res.writeHead(204);
|
|
@@ -155,6 +64,10 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
155
64
|
return;
|
|
156
65
|
}
|
|
157
66
|
|
|
67
|
+
if (!rateLimitMiddleware(this.rateLimiter, req, res)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
158
71
|
// API key validation
|
|
159
72
|
if (apiKey) {
|
|
160
73
|
const providedKey = (req.headers['x-goog-api-key'] || req.headers['authorization']?.replace('Bearer ', '')) as string | undefined;
|
|
@@ -164,8 +77,8 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
164
77
|
}
|
|
165
78
|
}
|
|
166
79
|
|
|
167
|
-
const parsedUrl =
|
|
168
|
-
const pathname = parsedUrl.pathname
|
|
80
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
81
|
+
const pathname = parsedUrl.pathname;
|
|
169
82
|
|
|
170
83
|
try {
|
|
171
84
|
// Gemini-compatible endpoints
|
|
@@ -293,8 +206,9 @@ export class GeminiAdapter extends BaseAdapter {
|
|
|
293
206
|
// 404
|
|
294
207
|
sendJson(res, 404, { error: { code: 404, message: 'Not found', status: 'NOT_FOUND' } });
|
|
295
208
|
|
|
296
|
-
} catch (error:
|
|
297
|
-
|
|
209
|
+
} catch (error: unknown) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
sendJson(res, 500, { error: { code: 500, message, status: 'INTERNAL' } });
|
|
298
212
|
}
|
|
299
213
|
});
|
|
300
214
|
|