@rv404/mcp-launchpad 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/.env.example +29 -0
- package/README.md +248 -0
- package/dist/backends/clickup.d.ts +3 -0
- package/dist/backends/clickup.d.ts.map +1 -0
- package/dist/backends/clickup.js +1550 -0
- package/dist/backends/clickup.js.map +1 -0
- package/dist/backends/email.d.ts +9 -0
- package/dist/backends/email.d.ts.map +1 -0
- package/dist/backends/email.js +452 -0
- package/dist/backends/email.js.map +1 -0
- package/dist/backends/firecrawl.d.ts +3 -0
- package/dist/backends/firecrawl.d.ts.map +1 -0
- package/dist/backends/firecrawl.js +297 -0
- package/dist/backends/firecrawl.js.map +1 -0
- package/dist/backends/gemini.d.ts +3 -0
- package/dist/backends/gemini.d.ts.map +1 -0
- package/dist/backends/gemini.js +702 -0
- package/dist/backends/gemini.js.map +1 -0
- package/dist/backends/github.d.ts +3 -0
- package/dist/backends/github.d.ts.map +1 -0
- package/dist/backends/github.js +2129 -0
- package/dist/backends/github.js.map +1 -0
- package/dist/backends/index.d.ts +2 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +27 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/kie.d.ts +3 -0
- package/dist/backends/kie.d.ts.map +1 -0
- package/dist/backends/kie.js +399 -0
- package/dist/backends/kie.js.map +1 -0
- package/dist/backends/n8n.d.ts +3 -0
- package/dist/backends/n8n.d.ts.map +1 -0
- package/dist/backends/n8n.js +414 -0
- package/dist/backends/n8n.js.map +1 -0
- package/dist/backends/notion.d.ts +3 -0
- package/dist/backends/notion.d.ts.map +1 -0
- package/dist/backends/notion.js +375 -0
- package/dist/backends/notion.js.map +1 -0
- package/dist/backends/quickbooks.d.ts +3 -0
- package/dist/backends/quickbooks.d.ts.map +1 -0
- package/dist/backends/quickbooks.js +2183 -0
- package/dist/backends/quickbooks.js.map +1 -0
- package/dist/backends/slack.d.ts +3 -0
- package/dist/backends/slack.d.ts.map +1 -0
- package/dist/backends/slack.js +1725 -0
- package/dist/backends/slack.js.map +1 -0
- package/dist/backends/youtube.d.ts +3 -0
- package/dist/backends/youtube.d.ts.map +1 -0
- package/dist/backends/youtube.js +936 -0
- package/dist/backends/youtube.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +150 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +36 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +283 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +65 -0
- package/dist/types.js.map +1 -0
- package/lib/mcp-file-output/dist/index.d.ts +99 -0
- package/lib/mcp-file-output/dist/index.d.ts.map +1 -0
- package/lib/mcp-file-output/dist/index.js +215 -0
- package/lib/mcp-file-output/dist/index.js.map +1 -0
- package/lib/mcp-file-output/node_modules/@types/node/LICENSE +21 -0
- package/lib/mcp-file-output/node_modules/@types/node/README.md +15 -0
- package/lib/mcp-file-output/node_modules/@types/node/assert/strict.d.ts +8 -0
- package/lib/mcp-file-output/node_modules/@types/node/assert.d.ts +1062 -0
- package/lib/mcp-file-output/node_modules/@types/node/async_hooks.d.ts +605 -0
- package/lib/mcp-file-output/node_modules/@types/node/buffer.buffer.d.ts +471 -0
- package/lib/mcp-file-output/node_modules/@types/node/buffer.d.ts +1936 -0
- package/lib/mcp-file-output/node_modules/@types/node/child_process.d.ts +1475 -0
- package/lib/mcp-file-output/node_modules/@types/node/cluster.d.ts +577 -0
- package/lib/mcp-file-output/node_modules/@types/node/compatibility/disposable.d.ts +16 -0
- package/lib/mcp-file-output/node_modules/@types/node/compatibility/index.d.ts +9 -0
- package/lib/mcp-file-output/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
- package/lib/mcp-file-output/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
- package/lib/mcp-file-output/node_modules/@types/node/console.d.ts +452 -0
- package/lib/mcp-file-output/node_modules/@types/node/constants.d.ts +21 -0
- package/lib/mcp-file-output/node_modules/@types/node/crypto.d.ts +4590 -0
- package/lib/mcp-file-output/node_modules/@types/node/dgram.d.ts +597 -0
- package/lib/mcp-file-output/node_modules/@types/node/diagnostics_channel.d.ts +578 -0
- package/lib/mcp-file-output/node_modules/@types/node/dns/promises.d.ts +479 -0
- package/lib/mcp-file-output/node_modules/@types/node/dns.d.ts +871 -0
- package/lib/mcp-file-output/node_modules/@types/node/domain.d.ts +170 -0
- package/lib/mcp-file-output/node_modules/@types/node/events.d.ts +977 -0
- package/lib/mcp-file-output/node_modules/@types/node/fs/promises.d.ts +1270 -0
- package/lib/mcp-file-output/node_modules/@types/node/fs.d.ts +4375 -0
- package/lib/mcp-file-output/node_modules/@types/node/globals.d.ts +172 -0
- package/lib/mcp-file-output/node_modules/@types/node/globals.typedarray.d.ts +38 -0
- package/lib/mcp-file-output/node_modules/@types/node/http.d.ts +2049 -0
- package/lib/mcp-file-output/node_modules/@types/node/http2.d.ts +2631 -0
- package/lib/mcp-file-output/node_modules/@types/node/https.d.ts +578 -0
- package/lib/mcp-file-output/node_modules/@types/node/index.d.ts +93 -0
- package/lib/mcp-file-output/node_modules/@types/node/inspector.generated.d.ts +3966 -0
- package/lib/mcp-file-output/node_modules/@types/node/module.d.ts +539 -0
- package/lib/mcp-file-output/node_modules/@types/node/net.d.ts +1012 -0
- package/lib/mcp-file-output/node_modules/@types/node/os.d.ts +506 -0
- package/lib/mcp-file-output/node_modules/@types/node/package.json +140 -0
- package/lib/mcp-file-output/node_modules/@types/node/path.d.ts +200 -0
- package/lib/mcp-file-output/node_modules/@types/node/perf_hooks.d.ts +961 -0
- package/lib/mcp-file-output/node_modules/@types/node/process.d.ts +1957 -0
- package/lib/mcp-file-output/node_modules/@types/node/punycode.d.ts +117 -0
- package/lib/mcp-file-output/node_modules/@types/node/querystring.d.ts +152 -0
- package/lib/mcp-file-output/node_modules/@types/node/readline/promises.d.ts +162 -0
- package/lib/mcp-file-output/node_modules/@types/node/readline.d.ts +589 -0
- package/lib/mcp-file-output/node_modules/@types/node/repl.d.ts +430 -0
- package/lib/mcp-file-output/node_modules/@types/node/sea.d.ts +153 -0
- package/lib/mcp-file-output/node_modules/@types/node/stream/consumers.d.ts +38 -0
- package/lib/mcp-file-output/node_modules/@types/node/stream/promises.d.ts +90 -0
- package/lib/mcp-file-output/node_modules/@types/node/stream/web.d.ts +533 -0
- package/lib/mcp-file-output/node_modules/@types/node/stream.d.ts +1675 -0
- package/lib/mcp-file-output/node_modules/@types/node/string_decoder.d.ts +67 -0
- package/lib/mcp-file-output/node_modules/@types/node/test.d.ts +1787 -0
- package/lib/mcp-file-output/node_modules/@types/node/timers/promises.d.ts +108 -0
- package/lib/mcp-file-output/node_modules/@types/node/timers.d.ts +286 -0
- package/lib/mcp-file-output/node_modules/@types/node/tls.d.ts +1255 -0
- package/lib/mcp-file-output/node_modules/@types/node/trace_events.d.ts +197 -0
- package/lib/mcp-file-output/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +468 -0
- package/lib/mcp-file-output/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +34 -0
- package/lib/mcp-file-output/node_modules/@types/node/ts5.6/index.d.ts +93 -0
- package/lib/mcp-file-output/node_modules/@types/node/tty.d.ts +208 -0
- package/lib/mcp-file-output/node_modules/@types/node/url.d.ts +964 -0
- package/lib/mcp-file-output/node_modules/@types/node/util.d.ts +2331 -0
- package/lib/mcp-file-output/node_modules/@types/node/v8.d.ts +809 -0
- package/lib/mcp-file-output/node_modules/@types/node/vm.d.ts +1001 -0
- package/lib/mcp-file-output/node_modules/@types/node/wasi.d.ts +181 -0
- package/lib/mcp-file-output/node_modules/@types/node/web-globals/abortcontroller.d.ts +34 -0
- package/lib/mcp-file-output/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
- package/lib/mcp-file-output/node_modules/@types/node/web-globals/events.d.ts +97 -0
- package/lib/mcp-file-output/node_modules/@types/node/web-globals/fetch.d.ts +46 -0
- package/lib/mcp-file-output/node_modules/@types/node/worker_threads.d.ts +715 -0
- package/lib/mcp-file-output/node_modules/@types/node/zlib.d.ts +540 -0
- package/lib/mcp-file-output/package.json +19 -0
- package/lib/mcp-file-output/src/index.ts +309 -0
- package/lib/mcp-file-output/tsconfig.json +20 -0
- package/package.json +64 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
// Google AI Gemini API configuration (uses API key)
|
|
5
|
+
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
|
|
6
|
+
const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
7
|
+
// Default output directory for generated images (auto-saves there if no outputPath specified)
|
|
8
|
+
const DEFAULT_IMAGE_OUTPUT_DIR = process.env.GEMINI_IMAGE_OUTPUT_DIR;
|
|
9
|
+
// Local store registry (like the Python CLI)
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gemini-file-search');
|
|
11
|
+
const STORES_FILE = path.join(CONFIG_DIR, 'stores.json');
|
|
12
|
+
function ensureConfigDir() {
|
|
13
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function loadStores() {
|
|
18
|
+
if (!fs.existsSync(STORES_FILE)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const content = fs.readFileSync(STORES_FILE, 'utf-8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
function saveStores(stores) {
|
|
25
|
+
ensureConfigDir();
|
|
26
|
+
fs.writeFileSync(STORES_FILE, JSON.stringify(stores, null, 2));
|
|
27
|
+
}
|
|
28
|
+
function getStoreId(name) {
|
|
29
|
+
const stores = loadStores();
|
|
30
|
+
if (!(name in stores)) {
|
|
31
|
+
const available = Object.keys(stores).join(', ') || 'none';
|
|
32
|
+
throw new Error(`Store '${name}' not found. Available stores: ${available}`);
|
|
33
|
+
}
|
|
34
|
+
return stores[name].id;
|
|
35
|
+
}
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Action Definitions
|
|
38
|
+
// ============================================================================
|
|
39
|
+
const actions = [
|
|
40
|
+
// Image Generation
|
|
41
|
+
{
|
|
42
|
+
name: 'generate_image',
|
|
43
|
+
description: 'Generate images using Google Imagen 4. Creates high-quality images from text descriptions.',
|
|
44
|
+
params: [
|
|
45
|
+
{ name: 'prompt', type: 'string', required: true, description: 'Detailed text description of the image to generate' },
|
|
46
|
+
{ name: 'aspectRatio', type: 'string', required: false, description: 'Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4 (default: 1:1)' },
|
|
47
|
+
{ name: 'negativePrompt', type: 'string', required: false, description: 'What to exclude or avoid in the generated image' },
|
|
48
|
+
{ name: 'sampleCount', type: 'number', required: false, description: 'Number of image variations (1-4, default: 1)' },
|
|
49
|
+
{ name: 'outputPath', type: 'string', required: false, description: 'File path to save the generated image (default: returns base64)' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
// File Search - Store Management
|
|
53
|
+
{
|
|
54
|
+
name: 'create_store',
|
|
55
|
+
description: 'Create a new File Search store for indexing documents. Returns the store ID.',
|
|
56
|
+
params: [
|
|
57
|
+
{ name: 'name', type: 'string', required: true, description: 'Local name for the store (used to reference it in other commands)' },
|
|
58
|
+
{ name: 'displayName', type: 'string', required: false, description: 'Display name in Google Cloud (defaults to name)' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'list_stores',
|
|
63
|
+
description: 'List all registered File Search stores with their metadata.',
|
|
64
|
+
params: [],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'delete_store',
|
|
68
|
+
description: 'Delete a File Search store. The store must be empty (no indexed files).',
|
|
69
|
+
params: [
|
|
70
|
+
{ name: 'name', type: 'string', required: true, description: 'Name of the store to delete' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
// File Search - Indexing
|
|
74
|
+
{
|
|
75
|
+
name: 'index_file',
|
|
76
|
+
description: 'Index a single file into a File Search store. Supports text files, code, JSON, etc.',
|
|
77
|
+
params: [
|
|
78
|
+
{ name: 'storeName', type: 'string', required: true, description: 'Name of the store to index into' },
|
|
79
|
+
{ name: 'filePath', type: 'string', required: true, description: 'Absolute path to the file to index' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'index_directory',
|
|
84
|
+
description: 'Index all supported files in a directory into a File Search store.',
|
|
85
|
+
params: [
|
|
86
|
+
{ name: 'storeName', type: 'string', required: true, description: 'Name of the store to index into' },
|
|
87
|
+
{ name: 'directoryPath', type: 'string', required: true, description: 'Absolute path to the directory to index' },
|
|
88
|
+
{ name: 'extensions', type: 'string', required: false, description: 'Comma-separated file extensions to include (e.g., ".py,.js,.ts"). Defaults to common code/text files.' },
|
|
89
|
+
{ name: 'excludeDirs', type: 'string', required: false, description: 'Comma-separated directory names to exclude (e.g., "node_modules,.git,venv")' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
// File Search - Querying
|
|
93
|
+
{
|
|
94
|
+
name: 'query_store',
|
|
95
|
+
description: 'Query a File Search store with a natural language question. Returns an answer with citations.',
|
|
96
|
+
params: [
|
|
97
|
+
{ name: 'storeName', type: 'string', required: true, description: 'Name of the store to query' },
|
|
98
|
+
{ name: 'question', type: 'string', required: true, description: 'Natural language question to ask about the indexed content' },
|
|
99
|
+
{ name: 'model', type: 'string', required: false, description: 'Gemini model to use (default: gemini-2.5-flash)' },
|
|
100
|
+
{ name: 'filter', type: 'string', required: false, description: 'Metadata filter (e.g., "file_extension=.py")' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Image Generation
|
|
106
|
+
// ============================================================================
|
|
107
|
+
async function generateImage(params) {
|
|
108
|
+
let { prompt, aspectRatio = '1:1', negativePrompt, sampleCount = 1, outputPath, } = params;
|
|
109
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
110
|
+
throw new Error('prompt is required and must be a string');
|
|
111
|
+
}
|
|
112
|
+
if (!GOOGLE_API_KEY) {
|
|
113
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
114
|
+
}
|
|
115
|
+
// If no outputPath provided but DEFAULT_IMAGE_OUTPUT_DIR is set, generate a filename
|
|
116
|
+
if (!outputPath && DEFAULT_IMAGE_OUTPUT_DIR) {
|
|
117
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
118
|
+
const sanitizedPrompt = prompt.slice(0, 30).replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_');
|
|
119
|
+
outputPath = path.join(DEFAULT_IMAGE_OUTPUT_DIR, `${timestamp}_${sanitizedPrompt}.png`);
|
|
120
|
+
}
|
|
121
|
+
// Build the prompt with negative prompt if provided
|
|
122
|
+
let fullPrompt = prompt;
|
|
123
|
+
if (negativePrompt && typeof negativePrompt === 'string') {
|
|
124
|
+
fullPrompt = prompt + '. Avoid: ' + negativePrompt;
|
|
125
|
+
}
|
|
126
|
+
// Use Imagen 4 model via Google AI API
|
|
127
|
+
const endpoint = BASE_URL + '/models/imagen-4.0-generate-001:predict?key=' + GOOGLE_API_KEY;
|
|
128
|
+
const response = await fetch(endpoint, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
instances: [{ prompt: fullPrompt }],
|
|
135
|
+
parameters: {
|
|
136
|
+
sampleCount: Math.min(Math.max(Number(sampleCount) || 1, 1), 4),
|
|
137
|
+
aspectRatio,
|
|
138
|
+
outputOptions: {
|
|
139
|
+
mimeType: 'image/png',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const errorText = await response.text();
|
|
146
|
+
throw new Error(`Imagen API error (${response.status}): ${errorText}`);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
// Handle both response formats (:predict uses predictions, legacy uses generatedImages)
|
|
150
|
+
const predictions = data.predictions || [];
|
|
151
|
+
const generatedImages = data.generatedImages || [];
|
|
152
|
+
// Normalize to unified format
|
|
153
|
+
const images = predictions.length > 0
|
|
154
|
+
? predictions.map(p => ({ imageBytes: p.bytesBase64Encoded }))
|
|
155
|
+
: generatedImages.map(g => ({ imageBytes: g.image.imageBytes }));
|
|
156
|
+
if (images.length === 0) {
|
|
157
|
+
throw new Error('No images generated. The prompt may have been filtered for safety reasons.');
|
|
158
|
+
}
|
|
159
|
+
// If outputPath is provided, save the image(s) to disk
|
|
160
|
+
if (outputPath && typeof outputPath === 'string' && images.length > 0) {
|
|
161
|
+
const savedFiles = [];
|
|
162
|
+
for (let i = 0; i < images.length; i++) {
|
|
163
|
+
const imageData = images[i].imageBytes;
|
|
164
|
+
const imageBuffer = Buffer.from(imageData, 'base64');
|
|
165
|
+
let filePath = outputPath;
|
|
166
|
+
if (images.length > 1) {
|
|
167
|
+
const ext = path.extname(outputPath);
|
|
168
|
+
const base = outputPath.slice(0, -ext.length);
|
|
169
|
+
filePath = base + '_' + (i + 1) + ext;
|
|
170
|
+
}
|
|
171
|
+
// Ensure directory exists
|
|
172
|
+
const dir = path.dirname(filePath);
|
|
173
|
+
if (!fs.existsSync(dir)) {
|
|
174
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
fs.writeFileSync(filePath, imageBuffer);
|
|
177
|
+
savedFiles.push(filePath);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
savedFiles,
|
|
182
|
+
count: savedFiles.length,
|
|
183
|
+
message: 'Saved ' + savedFiles.length + ' image(s) to disk',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Return the raw response with base64 data
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
images: images.map(img => ({ base64: img.imageBytes })),
|
|
190
|
+
count: images.length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// File Search - Store Management
|
|
195
|
+
// ============================================================================
|
|
196
|
+
async function createStore(params) {
|
|
197
|
+
const { name, displayName } = params;
|
|
198
|
+
if (!name || typeof name !== 'string') {
|
|
199
|
+
throw new Error('name is required and must be a string');
|
|
200
|
+
}
|
|
201
|
+
if (!GOOGLE_API_KEY) {
|
|
202
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
203
|
+
}
|
|
204
|
+
// Validate store name
|
|
205
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
|
206
|
+
throw new Error('Store name must start with a letter and contain only letters, numbers, hyphens, and underscores');
|
|
207
|
+
}
|
|
208
|
+
if (name.length > 64) {
|
|
209
|
+
throw new Error('Store name too long (max 64 characters)');
|
|
210
|
+
}
|
|
211
|
+
// Check if store already exists locally
|
|
212
|
+
const stores = loadStores();
|
|
213
|
+
if (name in stores) {
|
|
214
|
+
throw new Error(`Store '${name}' already exists. Use a different name or delete it first.`);
|
|
215
|
+
}
|
|
216
|
+
// Create the store via API
|
|
217
|
+
const endpoint = `${BASE_URL}/fileSearchStores?key=${GOOGLE_API_KEY}`;
|
|
218
|
+
const response = await fetch(endpoint, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
displayName: displayName || name,
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const errorText = await response.text();
|
|
229
|
+
throw new Error(`Failed to create store: ${errorText}`);
|
|
230
|
+
}
|
|
231
|
+
const storeData = await response.json();
|
|
232
|
+
// Register locally
|
|
233
|
+
stores[name] = {
|
|
234
|
+
id: storeData.name,
|
|
235
|
+
metadata: {
|
|
236
|
+
display_name: displayName || name,
|
|
237
|
+
created_at: new Date().toISOString(),
|
|
238
|
+
file_count: 0,
|
|
239
|
+
total_size: 0,
|
|
240
|
+
indexed_files: [],
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
saveStores(stores);
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
name,
|
|
247
|
+
storeId: storeData.name,
|
|
248
|
+
message: `Store '${name}' created successfully`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function listStores() {
|
|
252
|
+
const stores = loadStores();
|
|
253
|
+
const storeList = Object.entries(stores).map(([name, data]) => ({
|
|
254
|
+
name,
|
|
255
|
+
id: data.id,
|
|
256
|
+
...data.metadata,
|
|
257
|
+
}));
|
|
258
|
+
return {
|
|
259
|
+
stores: storeList,
|
|
260
|
+
count: storeList.length,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function deleteStore(params) {
|
|
264
|
+
const { name } = params;
|
|
265
|
+
if (!name || typeof name !== 'string') {
|
|
266
|
+
throw new Error('name is required and must be a string');
|
|
267
|
+
}
|
|
268
|
+
if (!GOOGLE_API_KEY) {
|
|
269
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
270
|
+
}
|
|
271
|
+
const stores = loadStores();
|
|
272
|
+
if (!(name in stores)) {
|
|
273
|
+
throw new Error(`Store '${name}' not found`);
|
|
274
|
+
}
|
|
275
|
+
const storeId = stores[name].id;
|
|
276
|
+
// Delete from Google Cloud
|
|
277
|
+
const endpoint = `${BASE_URL}/${storeId}?key=${GOOGLE_API_KEY}`;
|
|
278
|
+
const response = await fetch(endpoint, {
|
|
279
|
+
method: 'DELETE',
|
|
280
|
+
});
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
const errorText = await response.text();
|
|
283
|
+
throw new Error(`Failed to delete store: ${errorText}`);
|
|
284
|
+
}
|
|
285
|
+
// Remove from local registry
|
|
286
|
+
delete stores[name];
|
|
287
|
+
saveStores(stores);
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
message: `Store '${name}' deleted successfully`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// File Search - Indexing
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Supported file extensions
|
|
297
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
298
|
+
// Code files
|
|
299
|
+
'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.rb',
|
|
300
|
+
'.php', '.swift', '.kt', '.scala', '.c', '.cpp', '.h', '.hpp',
|
|
301
|
+
'.cs', '.vb', '.fs', '.r', '.m', '.mm', '.pl', '.pm', '.lua',
|
|
302
|
+
'.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
|
|
303
|
+
// Config/data files
|
|
304
|
+
'.json', '.yaml', '.yml', '.xml', '.toml', '.ini', '.cfg', '.conf',
|
|
305
|
+
'.properties',
|
|
306
|
+
// Documentation
|
|
307
|
+
'.md', '.markdown', '.txt', '.rst', '.adoc', '.tex',
|
|
308
|
+
// Web
|
|
309
|
+
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
310
|
+
// Database
|
|
311
|
+
'.sql',
|
|
312
|
+
// Other
|
|
313
|
+
'.csv', '.log', '.gitignore', '.dockerfile',
|
|
314
|
+
]);
|
|
315
|
+
// Directories to skip by default
|
|
316
|
+
const DEFAULT_EXCLUDES = new Set([
|
|
317
|
+
'node_modules', '.git', '.svn', '.hg', 'venv', '.venv', 'env',
|
|
318
|
+
'__pycache__', '.pytest_cache', '.mypy_cache', 'dist', 'build',
|
|
319
|
+
'.next', '.nuxt', 'vendor', 'target', 'bin', 'obj',
|
|
320
|
+
]);
|
|
321
|
+
// Sensitive paths to never index
|
|
322
|
+
const SENSITIVE_DIRS = new Set([
|
|
323
|
+
'.ssh', '.gnupg', '.gpg', '.aws', '.azure', '.gcloud',
|
|
324
|
+
'.env', '.secrets', 'secrets',
|
|
325
|
+
]);
|
|
326
|
+
const SENSITIVE_PATTERNS = [
|
|
327
|
+
/\.env(\..+)?$/i,
|
|
328
|
+
/\.pem$/i,
|
|
329
|
+
/\.key$/i,
|
|
330
|
+
/\.p12$/i,
|
|
331
|
+
/\.pfx$/i,
|
|
332
|
+
/id_rsa/i,
|
|
333
|
+
/id_ed25519/i,
|
|
334
|
+
/credentials\.json$/i,
|
|
335
|
+
/service[_-]?account.*\.json$/i,
|
|
336
|
+
/.*secret.*\.json$/i,
|
|
337
|
+
];
|
|
338
|
+
function isSensitivePath(filePath) {
|
|
339
|
+
const parts = filePath.split(path.sep).map(p => p.toLowerCase());
|
|
340
|
+
// Check for sensitive directories
|
|
341
|
+
for (const dir of SENSITIVE_DIRS) {
|
|
342
|
+
if (parts.includes(dir.toLowerCase())) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Check for sensitive file patterns
|
|
347
|
+
const filename = path.basename(filePath);
|
|
348
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
349
|
+
if (pattern.test(filename)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
async function indexFile(params) {
|
|
356
|
+
const { storeName, filePath } = params;
|
|
357
|
+
if (!storeName || typeof storeName !== 'string') {
|
|
358
|
+
throw new Error('storeName is required');
|
|
359
|
+
}
|
|
360
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
361
|
+
throw new Error('filePath is required');
|
|
362
|
+
}
|
|
363
|
+
if (!GOOGLE_API_KEY) {
|
|
364
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
365
|
+
}
|
|
366
|
+
// Check if file exists
|
|
367
|
+
if (!fs.existsSync(filePath)) {
|
|
368
|
+
throw new Error(`File not found: ${filePath}`);
|
|
369
|
+
}
|
|
370
|
+
// Security check
|
|
371
|
+
if (isSensitivePath(filePath)) {
|
|
372
|
+
throw new Error(`Refusing to index sensitive file: ${filePath}`);
|
|
373
|
+
}
|
|
374
|
+
const storeId = getStoreId(storeName);
|
|
375
|
+
// Read file content
|
|
376
|
+
const fileContent = fs.readFileSync(filePath);
|
|
377
|
+
const fileSize = fs.statSync(filePath).size;
|
|
378
|
+
const fileName = path.basename(filePath);
|
|
379
|
+
// Determine MIME type based on extension
|
|
380
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
381
|
+
let mimeType = 'text/plain';
|
|
382
|
+
if (['.json'].includes(ext)) {
|
|
383
|
+
mimeType = 'application/json';
|
|
384
|
+
}
|
|
385
|
+
else if (['.html', '.htm'].includes(ext)) {
|
|
386
|
+
mimeType = 'text/html';
|
|
387
|
+
}
|
|
388
|
+
else if (['.xml'].includes(ext)) {
|
|
389
|
+
mimeType = 'application/xml';
|
|
390
|
+
}
|
|
391
|
+
else if (['.md', '.markdown'].includes(ext)) {
|
|
392
|
+
mimeType = 'text/markdown';
|
|
393
|
+
}
|
|
394
|
+
else if (['.css'].includes(ext)) {
|
|
395
|
+
mimeType = 'text/css';
|
|
396
|
+
}
|
|
397
|
+
else if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
|
|
398
|
+
mimeType = 'text/javascript';
|
|
399
|
+
}
|
|
400
|
+
else if (['.py'].includes(ext)) {
|
|
401
|
+
mimeType = 'text/x-python';
|
|
402
|
+
}
|
|
403
|
+
// Step 1: Upload file to Files API first
|
|
404
|
+
// Use media upload endpoint
|
|
405
|
+
const uploadEndpoint = `https://generativelanguage.googleapis.com/upload/v1beta/files?key=${GOOGLE_API_KEY}`;
|
|
406
|
+
// Generate a safe file ID (alphanumeric and dashes only, max 40 chars)
|
|
407
|
+
const safeFileName = fileName
|
|
408
|
+
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
409
|
+
.replace(/-+/g, '-')
|
|
410
|
+
.replace(/^-|-$/g, '')
|
|
411
|
+
.toLowerCase()
|
|
412
|
+
.slice(0, 35);
|
|
413
|
+
const fileId = `${safeFileName}-${Date.now().toString(36)}`;
|
|
414
|
+
const uploadResponse = await fetch(uploadEndpoint, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: {
|
|
417
|
+
'Content-Type': mimeType,
|
|
418
|
+
'X-Goog-Upload-Protocol': 'raw',
|
|
419
|
+
'X-Goog-Upload-Command': 'upload, finalize',
|
|
420
|
+
},
|
|
421
|
+
body: fileContent,
|
|
422
|
+
});
|
|
423
|
+
if (!uploadResponse.ok) {
|
|
424
|
+
const errorText = await uploadResponse.text();
|
|
425
|
+
throw new Error(`Failed to upload file to Files API: ${errorText}`);
|
|
426
|
+
}
|
|
427
|
+
const uploadData = await uploadResponse.json();
|
|
428
|
+
const uploadedFileName = uploadData.file?.name;
|
|
429
|
+
if (!uploadedFileName) {
|
|
430
|
+
throw new Error('File upload succeeded but no file name returned');
|
|
431
|
+
}
|
|
432
|
+
// Step 2: Import the uploaded file into the FileSearchStore
|
|
433
|
+
const importEndpoint = `${BASE_URL}/${storeId}:importFile?key=${GOOGLE_API_KEY}`;
|
|
434
|
+
const importResponse = await fetch(importEndpoint, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: {
|
|
437
|
+
'Content-Type': 'application/json',
|
|
438
|
+
},
|
|
439
|
+
body: JSON.stringify({
|
|
440
|
+
fileName: uploadedFileName,
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
if (!importResponse.ok) {
|
|
444
|
+
const errorText = await importResponse.text();
|
|
445
|
+
throw new Error(`Failed to import file to store: ${errorText}`);
|
|
446
|
+
}
|
|
447
|
+
// Update local metadata
|
|
448
|
+
const stores = loadStores();
|
|
449
|
+
if (storeName in stores) {
|
|
450
|
+
const metadata = stores[storeName].metadata;
|
|
451
|
+
metadata.file_count = (metadata.file_count || 0) + 1;
|
|
452
|
+
metadata.total_size = (metadata.total_size || 0) + fileSize;
|
|
453
|
+
metadata.indexed_files = metadata.indexed_files || [];
|
|
454
|
+
if (!metadata.indexed_files.includes(filePath)) {
|
|
455
|
+
metadata.indexed_files.push(filePath);
|
|
456
|
+
}
|
|
457
|
+
metadata.last_indexed = new Date().toISOString();
|
|
458
|
+
saveStores(stores);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
success: true,
|
|
462
|
+
file: fileName,
|
|
463
|
+
size: fileSize,
|
|
464
|
+
uploadedAs: uploadedFileName,
|
|
465
|
+
message: `File '${fileName}' indexed successfully`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async function indexDirectory(params) {
|
|
469
|
+
const { storeName, directoryPath, extensions, excludeDirs } = params;
|
|
470
|
+
if (!storeName || typeof storeName !== 'string') {
|
|
471
|
+
throw new Error('storeName is required');
|
|
472
|
+
}
|
|
473
|
+
if (!directoryPath || typeof directoryPath !== 'string') {
|
|
474
|
+
throw new Error('directoryPath is required');
|
|
475
|
+
}
|
|
476
|
+
if (!fs.existsSync(directoryPath)) {
|
|
477
|
+
throw new Error(`Directory not found: ${directoryPath}`);
|
|
478
|
+
}
|
|
479
|
+
if (!fs.statSync(directoryPath).isDirectory()) {
|
|
480
|
+
throw new Error(`Not a directory: ${directoryPath}`);
|
|
481
|
+
}
|
|
482
|
+
// Build extension set
|
|
483
|
+
let includeExtensions = SUPPORTED_EXTENSIONS;
|
|
484
|
+
if (extensions && typeof extensions === 'string') {
|
|
485
|
+
includeExtensions = new Set(extensions.split(',').map(e => e.trim().toLowerCase()));
|
|
486
|
+
}
|
|
487
|
+
// Build exclusion set
|
|
488
|
+
const excludeSet = new Set(DEFAULT_EXCLUDES);
|
|
489
|
+
if (excludeDirs && typeof excludeDirs === 'string') {
|
|
490
|
+
excludeDirs.split(',').forEach(d => excludeSet.add(d.trim()));
|
|
491
|
+
}
|
|
492
|
+
// Collect files to index
|
|
493
|
+
const filesToIndex = [];
|
|
494
|
+
function walkDir(dir) {
|
|
495
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
496
|
+
for (const entry of entries) {
|
|
497
|
+
const fullPath = path.join(dir, entry.name);
|
|
498
|
+
if (entry.isDirectory()) {
|
|
499
|
+
// Skip excluded directories
|
|
500
|
+
if (excludeSet.has(entry.name) || isSensitivePath(fullPath)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
walkDir(fullPath);
|
|
504
|
+
}
|
|
505
|
+
else if (entry.isFile()) {
|
|
506
|
+
// Check extension
|
|
507
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
508
|
+
if (!includeExtensions.has(ext)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
// Skip sensitive files
|
|
512
|
+
if (isSensitivePath(fullPath)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
filesToIndex.push(fullPath);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
walkDir(directoryPath);
|
|
520
|
+
if (filesToIndex.length === 0) {
|
|
521
|
+
return {
|
|
522
|
+
success: true,
|
|
523
|
+
indexed: 0,
|
|
524
|
+
errors: [],
|
|
525
|
+
message: 'No supported files found to index',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// Index files one by one
|
|
529
|
+
const results = {
|
|
530
|
+
indexed: 0,
|
|
531
|
+
failed: 0,
|
|
532
|
+
errors: [],
|
|
533
|
+
files: [],
|
|
534
|
+
};
|
|
535
|
+
for (const file of filesToIndex) {
|
|
536
|
+
try {
|
|
537
|
+
await indexFile({ storeName, filePath: file });
|
|
538
|
+
results.indexed++;
|
|
539
|
+
results.files.push(path.basename(file));
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
results.failed++;
|
|
543
|
+
results.errors.push({
|
|
544
|
+
file: path.basename(file),
|
|
545
|
+
error: error instanceof Error ? error.message : String(error),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
indexed: results.indexed,
|
|
552
|
+
failed: results.failed,
|
|
553
|
+
errors: results.errors,
|
|
554
|
+
message: `Indexed ${results.indexed} files (${results.failed} failed)`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// ============================================================================
|
|
558
|
+
// File Search - Querying
|
|
559
|
+
// ============================================================================
|
|
560
|
+
async function queryStore(params) {
|
|
561
|
+
const { storeName, question, model = 'gemini-2.5-flash', filter } = params;
|
|
562
|
+
if (!storeName || typeof storeName !== 'string') {
|
|
563
|
+
throw new Error('storeName is required');
|
|
564
|
+
}
|
|
565
|
+
if (!question || typeof question !== 'string') {
|
|
566
|
+
throw new Error('question is required');
|
|
567
|
+
}
|
|
568
|
+
if (!GOOGLE_API_KEY) {
|
|
569
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
570
|
+
}
|
|
571
|
+
const storeId = getStoreId(storeName);
|
|
572
|
+
// Build the FileSearch tool configuration
|
|
573
|
+
const fileSearchConfig = {
|
|
574
|
+
fileSearchStoreNames: [storeId],
|
|
575
|
+
};
|
|
576
|
+
if (filter && typeof filter === 'string') {
|
|
577
|
+
fileSearchConfig.metadataFilter = filter;
|
|
578
|
+
}
|
|
579
|
+
// Call generateContent with File Search tool
|
|
580
|
+
const endpoint = `${BASE_URL}/models/${model}:generateContent?key=${GOOGLE_API_KEY}`;
|
|
581
|
+
const response = await fetch(endpoint, {
|
|
582
|
+
method: 'POST',
|
|
583
|
+
headers: {
|
|
584
|
+
'Content-Type': 'application/json',
|
|
585
|
+
},
|
|
586
|
+
body: JSON.stringify({
|
|
587
|
+
contents: [
|
|
588
|
+
{
|
|
589
|
+
parts: [{ text: question }],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
tools: [
|
|
593
|
+
{
|
|
594
|
+
fileSearch: fileSearchConfig,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
}),
|
|
598
|
+
});
|
|
599
|
+
if (!response.ok) {
|
|
600
|
+
const errorText = await response.text();
|
|
601
|
+
throw new Error(`Query failed: ${errorText}`);
|
|
602
|
+
}
|
|
603
|
+
const data = await response.json();
|
|
604
|
+
// Extract the answer
|
|
605
|
+
let answer = '';
|
|
606
|
+
if (data.candidates && data.candidates[0]?.content?.parts) {
|
|
607
|
+
for (const part of data.candidates[0].content.parts) {
|
|
608
|
+
if (part.text) {
|
|
609
|
+
answer += part.text;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Extract citations from grounding metadata
|
|
614
|
+
const citations = [];
|
|
615
|
+
const groundingMetadata = data.candidates?.[0]?.groundingMetadata;
|
|
616
|
+
if (groundingMetadata?.groundingChunks) {
|
|
617
|
+
for (const chunk of groundingMetadata.groundingChunks) {
|
|
618
|
+
const ctx = chunk.retrievedContext;
|
|
619
|
+
if (ctx) {
|
|
620
|
+
let fileName = 'Unknown';
|
|
621
|
+
if (ctx.uri) {
|
|
622
|
+
fileName = ctx.uri.split('/').pop() || ctx.uri;
|
|
623
|
+
}
|
|
624
|
+
else if (ctx.title) {
|
|
625
|
+
fileName = ctx.title;
|
|
626
|
+
}
|
|
627
|
+
citations.push({
|
|
628
|
+
file: fileName,
|
|
629
|
+
text: ctx.text,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
answer: answer.trim(),
|
|
637
|
+
citations,
|
|
638
|
+
model,
|
|
639
|
+
storeName,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Backend Export
|
|
644
|
+
// ============================================================================
|
|
645
|
+
export const geminiBackend = {
|
|
646
|
+
name: 'gemini',
|
|
647
|
+
description: 'Google Gemini services: Imagen 4 image generation and File Search RAG.',
|
|
648
|
+
actions,
|
|
649
|
+
async execute(action, params) {
|
|
650
|
+
switch (action) {
|
|
651
|
+
// Image Generation
|
|
652
|
+
case 'generate_image':
|
|
653
|
+
return generateImage(params);
|
|
654
|
+
// File Search - Store Management
|
|
655
|
+
case 'create_store':
|
|
656
|
+
return createStore(params);
|
|
657
|
+
case 'list_stores':
|
|
658
|
+
return listStores();
|
|
659
|
+
case 'delete_store':
|
|
660
|
+
return deleteStore(params);
|
|
661
|
+
// File Search - Indexing
|
|
662
|
+
case 'index_file':
|
|
663
|
+
return indexFile(params);
|
|
664
|
+
case 'index_directory':
|
|
665
|
+
return indexDirectory(params);
|
|
666
|
+
// File Search - Querying
|
|
667
|
+
case 'query_store':
|
|
668
|
+
return queryStore(params);
|
|
669
|
+
default:
|
|
670
|
+
throw new Error('Unknown action: ' + action);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
async healthCheck() {
|
|
674
|
+
const startTime = Date.now();
|
|
675
|
+
try {
|
|
676
|
+
if (!GOOGLE_API_KEY) {
|
|
677
|
+
return {
|
|
678
|
+
status: 'unavailable',
|
|
679
|
+
latency_ms: Date.now() - startTime,
|
|
680
|
+
error: 'GOOGLE_API_KEY environment variable is not set'
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
// Quick check that the API key format looks valid
|
|
684
|
+
if (GOOGLE_API_KEY.length < 20) {
|
|
685
|
+
return {
|
|
686
|
+
status: 'unavailable',
|
|
687
|
+
latency_ms: Date.now() - startTime,
|
|
688
|
+
error: 'GOOGLE_API_KEY appears to be invalid (too short)'
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return { status: 'healthy', latency_ms: Date.now() - startTime };
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
return {
|
|
695
|
+
status: 'unavailable',
|
|
696
|
+
latency_ms: Date.now() - startTime,
|
|
697
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
//# sourceMappingURL=gemini.js.map
|