@mixio-pro/kalaasetu-mcp 2.0.8-beta → 2.0.10-beta
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/package.json +2 -2
- package/src/index.ts +30 -29
- package/src/tools/fal/config.ts +23 -49
- package/src/utils/prompt-enhancer-presets.ts +97 -6
- package/src/utils/prompt-enhancer.ts +77 -0
- package/src/utils/remote-sync.ts +89 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mixio-pro/kalaasetu-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10-beta",
|
|
4
4
|
"description": "A powerful Model Context Protocol server providing AI tools for content generation and analysis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@google/genai": "^1.28.0",
|
|
53
53
|
"@types/node": "^24.10.1",
|
|
54
54
|
"@types/wav": "^1.0.4",
|
|
55
|
-
"fastmcp": "
|
|
55
|
+
"fastmcp": "3.26.8",
|
|
56
56
|
"form-data": "^4.0.5",
|
|
57
57
|
"google-auth-library": "^10.5.0",
|
|
58
58
|
"wav": "^1.0.2",
|
package/src/index.ts
CHANGED
|
@@ -11,46 +11,47 @@ import { geminiEditImage, geminiTextToImage } from "./tools/gemini";
|
|
|
11
11
|
import { imageToVideo } from "./tools/image-to-video";
|
|
12
12
|
import { getGenerationStatus } from "./tools/get-status";
|
|
13
13
|
|
|
14
|
+
import { syncFalConfig } from "./tools/fal/config";
|
|
15
|
+
import { syncPromptEnhancerConfigs } from "./utils/prompt-enhancer-presets";
|
|
16
|
+
|
|
14
17
|
const server = new FastMCP({
|
|
15
18
|
name: "Kalaasetu MCP Server",
|
|
16
19
|
version: pkg.version as any,
|
|
17
20
|
});
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
server.addTool(geminiEditImage);
|
|
22
|
-
// server.addTool(geminiAnalyzeImages);
|
|
23
|
-
|
|
24
|
-
// Gemini TTS Tool
|
|
25
|
-
// server.addTool(geminiSingleSpeakerTts);
|
|
22
|
+
async function main() {
|
|
23
|
+
console.log("🚀 Initializing Kalaasetu MCP Server...");
|
|
26
24
|
|
|
27
|
-
//
|
|
28
|
-
|
|
25
|
+
// 1. Sync Remote Configs
|
|
26
|
+
await Promise.all([syncFalConfig(), syncPromptEnhancerConfigs()]);
|
|
29
27
|
|
|
30
|
-
//
|
|
31
|
-
|
|
28
|
+
// 2. Add Gemini Tools
|
|
29
|
+
server.addTool(geminiTextToImage);
|
|
30
|
+
server.addTool(geminiEditImage);
|
|
31
|
+
server.addTool(imageToVideo);
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
server.addTool(
|
|
33
|
+
// 3. Add Discovery Tools
|
|
34
|
+
server.addTool(falListPresets);
|
|
35
|
+
server.addTool(falGetPresetDetails);
|
|
36
|
+
server.addTool(falUploadFile);
|
|
35
37
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
38
|
+
// 4. Register Dynamic FAL AI Tools
|
|
39
|
+
// These are now based on potentially synced remote config
|
|
40
|
+
const falTools = createAllFalTools();
|
|
41
|
+
for (const tool of falTools) {
|
|
42
|
+
server.addTool(tool);
|
|
43
|
+
}
|
|
39
44
|
|
|
40
|
-
//
|
|
41
|
-
server.addTool(
|
|
42
|
-
server.addTool(falGetPresetDetails);
|
|
43
|
-
server.addTool(falUploadFile);
|
|
45
|
+
// 5. Add Status Tool
|
|
46
|
+
server.addTool(getGenerationStatus);
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
console.log("✅ Starting server transport...");
|
|
49
|
+
server.start({
|
|
50
|
+
transportType: "stdio",
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
server.start({
|
|
55
|
-
transportType: "stdio",
|
|
54
|
+
main().catch((err) => {
|
|
55
|
+
console.error("❌ Failed to start server:", err);
|
|
56
|
+
process.exit(1);
|
|
56
57
|
});
|
package/src/tools/fal/config.ts
CHANGED
|
@@ -129,58 +129,32 @@ export const DEFAULT_PRESETS: FalPresetConfig[] = [
|
|
|
129
129
|
},
|
|
130
130
|
];
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
* Load the FAL configuration from a JSON file.
|
|
134
|
-
* Defaults to DEFAULT_PRESETS if no path is provided or if the file cannot be read.
|
|
135
|
-
*/
|
|
136
|
-
export function loadFalConfig(): FalConfig {
|
|
137
|
-
let configPath = process.env.FAL_CONFIG_JSON_PATH;
|
|
138
|
-
|
|
139
|
-
// Fallback to default file search if no env var
|
|
140
|
-
if (!configPath) {
|
|
141
|
-
const potentialPaths = [
|
|
142
|
-
"fal-config.json", // in CWD
|
|
143
|
-
path.join(__dirname, "../../fal-config.json"), // from src/tools/fal in source
|
|
144
|
-
path.join(__dirname, "../../../fal-config.json"), // from dist/tools/fal in build
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
for (const p of potentialPaths) {
|
|
148
|
-
if (fs.existsSync(p)) {
|
|
149
|
-
configPath = p;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (!configPath) {
|
|
156
|
-
console.error(
|
|
157
|
-
"FAL_CONFIG_JSON_PATH not set and no fal-config.json found. Using internal default presets."
|
|
158
|
-
);
|
|
159
|
-
return { presets: DEFAULT_PRESETS };
|
|
160
|
-
}
|
|
132
|
+
import { syncRemoteConfig } from "../../utils/remote-sync";
|
|
161
133
|
|
|
162
|
-
|
|
163
|
-
const absolutePath = path.resolve(configPath);
|
|
134
|
+
let currentConfig: FalConfig = { presets: DEFAULT_PRESETS };
|
|
164
135
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Syncs the FAL configuration with the remote server.
|
|
138
|
+
*/
|
|
139
|
+
export async function syncFalConfig(): Promise<FalConfig> {
|
|
140
|
+
currentConfig = await syncRemoteConfig<FalConfig>({
|
|
141
|
+
name: "fal-config",
|
|
142
|
+
remoteUrl: "https://config.mixio.pro/mcp/fal/config.json",
|
|
143
|
+
envVar: "FAL_CONFIG_JSON_PATH",
|
|
144
|
+
fallback: { presets: DEFAULT_PRESETS },
|
|
145
|
+
validate: (data: any): data is FalConfig => {
|
|
146
|
+
return data && Array.isArray(data.presets);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
return currentConfig;
|
|
150
|
+
}
|
|
178
151
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Load the FAL configuration.
|
|
154
|
+
* Returns the currently synced config or internal defaults.
|
|
155
|
+
*/
|
|
156
|
+
export function loadFalConfig(): FalConfig {
|
|
157
|
+
return currentConfig;
|
|
184
158
|
}
|
|
185
159
|
|
|
186
160
|
/**
|
|
@@ -126,7 +126,38 @@ export const VIDEO_PRESETS: Record<string, PromptEnhancerConfig> = {
|
|
|
126
126
|
styleGuide:
|
|
127
127
|
"cinematic composition, present tense, flowing narrative, clear camera language, atmospheric lighting, emotive expressions through physical cues",
|
|
128
128
|
negativeElements:
|
|
129
|
-
"text, logos, signage, brand names, complex physics, chaotic motion, jumping, juggling, conflicting light sources, overloaded scene, too many characters, internal emotional states without visual cues",
|
|
129
|
+
"text, logos, signage, brand names, complex physics, chaotic motion, jumping, juggling, conflicting light sources, overloaded scene, too many characters, internal emotional states without visual cues, high-frequency patterns, brick walls, mesh, micro-check fabrics, moiré",
|
|
130
|
+
guidelines:
|
|
131
|
+
"LTX-2 requires a narrative, screenplay-like approach. 1. Establish the shot (cinematography, scale, focal length). 2. Set the scene (lighting, atmosphere, textures). 3. Describe subject and action as a natural sequence flowing from start to end. 4. Specify camera movements relative to the subject. Describe what happens after the movement for coherence.",
|
|
132
|
+
dos: [
|
|
133
|
+
"Write in single flowing paragraphs (4-8 sentences)",
|
|
134
|
+
"Use present tense for all actions (e.g., 'glides', 'turns')",
|
|
135
|
+
"Describe cinematography first to anchor the frame",
|
|
136
|
+
"Express emotion through physical cues (e.g., 'furrows brow', 'eyes widen')",
|
|
137
|
+
"Match detail level to shot scale (close-ups need more detail)",
|
|
138
|
+
"Use lens and shutter language (e.g., '50mm', '180° shutter equivalent')",
|
|
139
|
+
"Describe audio as SFX or spoken dialogue in quotation marks",
|
|
140
|
+
],
|
|
141
|
+
donts: [
|
|
142
|
+
"Avoid 'vibe dumping' (e.g., 'epic amazing 4K')",
|
|
143
|
+
"Don't use internal emotional labels (e.g., 'feels sad')",
|
|
144
|
+
"Avoid high-frequency patterns that cause moiré (bricks, mesh)",
|
|
145
|
+
"Don't request abrupt reframing or chaotic handheld motion",
|
|
146
|
+
"Avoid complex physics or multiple interacting subjects",
|
|
147
|
+
"No readable text or logos",
|
|
148
|
+
],
|
|
149
|
+
examples: [
|
|
150
|
+
{
|
|
151
|
+
input: "A girl puppet singing in the rain",
|
|
152
|
+
output:
|
|
153
|
+
"A close-up of a cheerful girl puppet with curly auburn yarn hair and wide button eyes, holding a small red umbrella above her head. Rain falls gently around her. She looks upward and begins to sing with joy in English: 'It's raining, it's raining, I love it when its raining.' Her fabric mouth opening and closing to a melodic tune. Her hands grip the umbrella handle as she sways slightly from side to side in rhythm. The camera holds steady as the rain sparkles against the soft lighting. Her eyes blink occasionally as she sings.",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
input: "A woman exploring an abandoned town",
|
|
157
|
+
output:
|
|
158
|
+
"A cinematic medium-wide shot of an abandoned town street at dawn with light mist clinging to the cracked pavement. An explorer in a leather satchel and messy brown hair walks slowly along the empty street. The camera begins slightly behind her, then performs a smooth tracking shot as she turns her head to look at the boarded-up windows. The camera pulls out as she continues walking, revealing the scale of the quiet, empty town. Soft primary colors and Kodak 2383 film look.",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
130
161
|
},
|
|
131
162
|
|
|
132
163
|
/**
|
|
@@ -223,20 +254,80 @@ export const VIDEO_PRESETS: Record<string, PromptEnhancerConfig> = {
|
|
|
223
254
|
negativeElements:
|
|
224
255
|
"wide shot, distant, cold lighting, static expression, fast movement",
|
|
225
256
|
},
|
|
257
|
+
/**
|
|
258
|
+
* Google Veo specific preset based on official prompting guidelines.
|
|
259
|
+
* Focuses on subject, action, context, and cinematography.
|
|
260
|
+
*/
|
|
261
|
+
veo: {
|
|
262
|
+
prefix: "A cinematic video.",
|
|
263
|
+
styleGuide:
|
|
264
|
+
"high-fidelity, fluid motion, accurate physics, complex lighting, detailed textures, consistent subjects",
|
|
265
|
+
negativeElements:
|
|
266
|
+
"warped limbs, flickering artifacts, low resolution, jumping frames, blurry faces, text, logo",
|
|
267
|
+
guidelines:
|
|
268
|
+
"Veo excels at detailed cinematography. Follow the structure: 1. Subject (Specific) 2. Action (Precise) 3. Context (Environment/Lighting) 4. Cinematography (Movement/Angle). Optionally add SFX: descriptions at the end.",
|
|
269
|
+
dos: [
|
|
270
|
+
"Use 'cinematic' and 'photorealistic' for high quality",
|
|
271
|
+
"Clearly define camera movements like 'arc shot' or 'dolly zoom'",
|
|
272
|
+
"Add specific lighting like 'golden hour' or 'neon glow'",
|
|
273
|
+
"Describe audio using 'SFX:' prefix if needed",
|
|
274
|
+
"Use (no subtitles) if you want to avoid text overlays",
|
|
275
|
+
],
|
|
276
|
+
donts: [
|
|
277
|
+
"Don't be vague about the setting",
|
|
278
|
+
"Avoid repetitive words",
|
|
279
|
+
"Don't overload the frame with too many subjects",
|
|
280
|
+
"Minimize the use of negative words like 'no' within the prompt (use the negative elements section instead)",
|
|
281
|
+
],
|
|
282
|
+
examples: [
|
|
283
|
+
{
|
|
284
|
+
input: "A cowboy in a desert",
|
|
285
|
+
output:
|
|
286
|
+
"A cinematic medium shot of a rugged cowboy in a weathered leather hat standing in a vast, sun-scorched desert. The camera performs a slow 180-degree arc shot around him as dust swirls at his feet. SFX: The howling wind and the distant clink of spurs.",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
input: "A futuristic city",
|
|
290
|
+
output:
|
|
291
|
+
"A wide aerial tracking shot over a sprawling neon-lit futuristic city at night. Rain slicks the metallic surfaces, reflecting the vibrant purple and teal lights. Flying vehicles navigate between towering skyscrapers with seamless, fluid motion. SFX: The ambient hum of a high-tech metropolis.",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
226
295
|
};
|
|
227
296
|
|
|
228
|
-
|
|
229
|
-
// COMBINED PRESETS DICTIONARY
|
|
230
|
-
// =============================================================================
|
|
297
|
+
import { syncRemoteConfig } from "./remote-sync";
|
|
231
298
|
|
|
232
299
|
/**
|
|
233
|
-
*
|
|
300
|
+
* Dynamic registry for prompt enhancer presets.
|
|
234
301
|
*/
|
|
235
|
-
export
|
|
302
|
+
export let PROMPT_ENHANCER_PRESETS: Record<string, PromptEnhancerConfig> = {
|
|
236
303
|
...IMAGE_PRESETS,
|
|
237
304
|
...VIDEO_PRESETS,
|
|
238
305
|
};
|
|
239
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Syncs the prompt enhancer configurations with the remote server.
|
|
309
|
+
*/
|
|
310
|
+
export async function syncPromptEnhancerConfigs(): Promise<void> {
|
|
311
|
+
const remoteConfig = await syncRemoteConfig<Record<string, PromptEnhancerConfig>>({
|
|
312
|
+
name: "prompt-enhancers",
|
|
313
|
+
remoteUrl: "https://config.mixio.pro/mcp/prompt-enhancers.json",
|
|
314
|
+
envVar: "PROMPT_ENHANCER_CONFIG_PATH",
|
|
315
|
+
fallback: {},
|
|
316
|
+
validate: (data: any): data is Record<string, PromptEnhancerConfig> => {
|
|
317
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Merge remote settings into the defaults
|
|
322
|
+
PROMPT_ENHANCER_PRESETS = {
|
|
323
|
+
...IMAGE_PRESETS,
|
|
324
|
+
...VIDEO_PRESETS,
|
|
325
|
+
...remoteConfig,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
console.log(`[Sync] Prompt Enhancer Presets updated. Total: ${Object.keys(PROMPT_ENHANCER_PRESETS).length}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
240
331
|
// =============================================================================
|
|
241
332
|
// HELPER FUNCTIONS
|
|
242
333
|
// =============================================================================
|
|
@@ -24,6 +24,16 @@ export interface PromptEnhancerConfig {
|
|
|
24
24
|
* Example: "A cinematic shot of {prompt}, dramatic lighting"
|
|
25
25
|
*/
|
|
26
26
|
wrapTemplate?: string;
|
|
27
|
+
/** Detailed system instructions for LLM-based enhancement */
|
|
28
|
+
llmSystemPrompt?: string;
|
|
29
|
+
/** Do's for the prompt (e.g. ['Use active voice', 'Describe motion']) */
|
|
30
|
+
dos?: string[];
|
|
31
|
+
/** Don'ts for the prompt (e.g. ['Avoid jargon', 'No technical camera terms']) */
|
|
32
|
+
donts?: string[];
|
|
33
|
+
/** Generic guidelines or reference text */
|
|
34
|
+
guidelines?: string;
|
|
35
|
+
/** Few-shot examples for the LLM { input: string, output: string } */
|
|
36
|
+
examples?: Array<{ input: string; output: string }>;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -177,6 +187,73 @@ export class PromptEnhancer {
|
|
|
177
187
|
);
|
|
178
188
|
}
|
|
179
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Generates a comprehensive system prompt for an LLM to follow when enhancing a prompt.
|
|
192
|
+
*/
|
|
193
|
+
getSystemPrompt(): string {
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
|
|
196
|
+
if (this.config.llmSystemPrompt) {
|
|
197
|
+
lines.push(this.config.llmSystemPrompt);
|
|
198
|
+
} else {
|
|
199
|
+
lines.push(
|
|
200
|
+
"You are an expert prompt engineer specializing in AI image and video generation."
|
|
201
|
+
);
|
|
202
|
+
lines.push(
|
|
203
|
+
"Your task is to expand and enhance the user's input prompt into a high-quality, detailed description."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.config.guidelines) {
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("### GUIDELINES");
|
|
210
|
+
lines.push(this.config.guidelines);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.config.styleGuide) {
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push("### STYLE AND QUALITY");
|
|
216
|
+
lines.push(`Incorporate these elements: ${this.config.styleGuide}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.config.dos && this.config.dos.length > 0) {
|
|
220
|
+
lines.push("");
|
|
221
|
+
lines.push("### DO'S");
|
|
222
|
+
this.config.dos.forEach((item) => lines.push(`- ${item}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (this.config.donts && this.config.donts.length > 0) {
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push("### DON'TS");
|
|
228
|
+
this.config.donts.forEach((item) => lines.push(`- ${item}`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.config.negativeElements) {
|
|
232
|
+
lines.push("");
|
|
233
|
+
lines.push("### NEGATIVE PROMPT (AVOID)");
|
|
234
|
+
lines.push(this.config.negativeElements);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this.config.examples && this.config.examples.length > 0) {
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push("### EXAMPLES");
|
|
240
|
+
this.config.examples.forEach((example, index) => {
|
|
241
|
+
lines.push(`Example ${index + 1}:`);
|
|
242
|
+
lines.push(`Input: ${example.input}`);
|
|
243
|
+
lines.push(`Output: ${example.output}`);
|
|
244
|
+
lines.push("");
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push("### OUTPUT FORMAT");
|
|
250
|
+
lines.push(
|
|
251
|
+
"Return ONLY the enhanced prompt text. No explanations or conversational filler."
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return lines.join("\n").trim();
|
|
255
|
+
}
|
|
256
|
+
|
|
180
257
|
/**
|
|
181
258
|
* Export the enhancer's configuration.
|
|
182
259
|
*/
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = path.join(process.cwd(), ".cache");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for remote configuration syncing.
|
|
9
|
+
*/
|
|
10
|
+
interface SyncOptions<T> {
|
|
11
|
+
/** The name of the config (used for file naming in cache) */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The remote URL to fetch from */
|
|
14
|
+
remoteUrl: string;
|
|
15
|
+
/** Environment variable that overrides the remote URL or providing a local path */
|
|
16
|
+
envVar?: string;
|
|
17
|
+
/** Internal default to use if both remote and cache fail */
|
|
18
|
+
fallback: T;
|
|
19
|
+
/** Optional validation function */
|
|
20
|
+
validate?: (data: any) => data is T;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Syncs a configuration from a remote source, local file, or environment variable.
|
|
25
|
+
* Implements a caching strategy to ensure availability even when the remote is down.
|
|
26
|
+
*/
|
|
27
|
+
export async function syncRemoteConfig<T>(options: SyncOptions<T>): Promise<T> {
|
|
28
|
+
const { name, remoteUrl, envVar, fallback, validate } = options;
|
|
29
|
+
const cachePath = path.join(CACHE_DIR, `${name}.json`);
|
|
30
|
+
|
|
31
|
+
// 1. Check for Environment Variable Override (Local Path)
|
|
32
|
+
if (envVar && process.env[envVar]) {
|
|
33
|
+
const localPath = process.env[envVar]!;
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(localPath)) {
|
|
36
|
+
const content = await readFile(localPath, "utf-8");
|
|
37
|
+
const data = JSON.parse(content);
|
|
38
|
+
if (!validate || validate(data)) {
|
|
39
|
+
console.log(`[Sync] Using local override from ${envVar}: ${localPath}`);
|
|
40
|
+
return data as T;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.warn(`[Sync] Failed to read local override from ${envVar}: ${e}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Attempt to Fetch from Remote
|
|
49
|
+
try {
|
|
50
|
+
console.log(`[Sync] Attempting to fetch ${name} from ${remoteUrl}...`);
|
|
51
|
+
const response = await fetch(remoteUrl, {
|
|
52
|
+
signal: AbortSignal.timeout(5000), // 5s timeout
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
if (!validate || validate(data)) {
|
|
58
|
+
// Cache the successful fetch
|
|
59
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
60
|
+
await writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
61
|
+
console.log(`[Sync] Successfully updated ${name} and cached at ${cachePath}`);
|
|
62
|
+
return data as T;
|
|
63
|
+
}
|
|
64
|
+
console.warn(`[Sync] Remote data for ${name} failed validation.`);
|
|
65
|
+
} else {
|
|
66
|
+
console.warn(`[Sync] Remote fetch for ${name} failed with status: ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(`[Sync] Error fetching ${name} from remote: ${e}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Fallback to Cache
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(cachePath)) {
|
|
75
|
+
const cacheContent = await readFile(cachePath, "utf-8");
|
|
76
|
+
const data = JSON.parse(cacheContent);
|
|
77
|
+
if (data && (!validate || validate(data))) {
|
|
78
|
+
console.log(`[Sync] Using cached version of ${name} from ${cachePath}`);
|
|
79
|
+
return data as T;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.warn(`[Sync] Failed to read cache for ${name}: ${e}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Final Fallback to Internal Defaults
|
|
87
|
+
console.warn(`[Sync] All sync methods failed for ${name}. Using internal defaults.`);
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|