tachibot-mcp 2.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/.env.example +260 -0
- package/CHANGELOG.md +54 -0
- package/CODE_OF_CONDUCT.md +56 -0
- package/CONTRIBUTING.md +54 -0
- package/Dockerfile +36 -0
- package/LICENSE +644 -0
- package/README.md +201 -0
- package/SECURITY.md +95 -0
- package/dist/personality/komaai-expressions.js +12 -0
- package/dist/profiles/balanced.json +33 -0
- package/dist/profiles/code_focus.json +33 -0
- package/dist/profiles/full.json +33 -0
- package/dist/profiles/minimal.json +33 -0
- package/dist/profiles/research_power.json +33 -0
- package/dist/scripts/build-profiles.js +46 -0
- package/dist/src/application/services/focus/FocusModeRegistry.js +46 -0
- package/dist/src/application/services/focus/FocusTool.service.js +109 -0
- package/dist/src/application/services/focus/ModeRegistry.js +46 -0
- package/dist/src/application/services/focus/modes/focus-deep.mode.js +27 -0
- package/dist/src/application/services/focus/modes/status.mode.js +50 -0
- package/dist/src/application/services/focus/modes/tachibot-status.mode.js +50 -0
- package/dist/src/collaborative-orchestrator.js +391 -0
- package/dist/src/config/model-constants.js +188 -0
- package/dist/src/config/model-defaults.js +57 -0
- package/dist/src/config/model-preferences.js +382 -0
- package/dist/src/config/timeout-config.js +130 -0
- package/dist/src/config.js +173 -0
- package/dist/src/domain/interfaces/IFocusMode.js +5 -0
- package/dist/src/domain/interfaces/IProvider.js +6 -0
- package/dist/src/domain/interfaces/ITool.js +5 -0
- package/dist/src/focus-deep.js +245 -0
- package/dist/src/infrastructure/ascii/art/robots.ascii.js +16 -0
- package/dist/src/mcp-client.js +90 -0
- package/dist/src/memory/index.js +17 -0
- package/dist/src/memory/memory-config.js +135 -0
- package/dist/src/memory/memory-interface.js +174 -0
- package/dist/src/memory/memory-manager.js +383 -0
- package/dist/src/memory/providers/devlog-provider.js +385 -0
- package/dist/src/memory/providers/hybrid-provider.js +399 -0
- package/dist/src/memory/providers/local-provider.js +388 -0
- package/dist/src/memory/providers/mem0-provider.js +337 -0
- package/dist/src/modes/architect.js +477 -0
- package/dist/src/modes/auditor.js +362 -0
- package/dist/src/modes/challenger.js +841 -0
- package/dist/src/modes/code-reviewer.js +382 -0
- package/dist/src/modes/commit-guardian.js +424 -0
- package/dist/src/modes/documentation-writer.js +572 -0
- package/dist/src/modes/scout.js +587 -0
- package/dist/src/modes/shared/helpers/challenger-helpers.js +454 -0
- package/dist/src/modes/shared/helpers/index.js +17 -0
- package/dist/src/modes/shared/helpers/scout-helpers.js +270 -0
- package/dist/src/modes/shared/helpers/verifier-helpers.js +332 -0
- package/dist/src/modes/test-architect.js +767 -0
- package/dist/src/modes/verifier.js +378 -0
- package/dist/src/monitoring/performance-monitor.js +435 -0
- package/dist/src/optimization/batch-executor.js +121 -0
- package/dist/src/optimization/context-pruner.js +196 -0
- package/dist/src/optimization/cost-monitor.js +338 -0
- package/dist/src/optimization/index.js +65 -0
- package/dist/src/optimization/model-router.js +264 -0
- package/dist/src/optimization/result-cache.js +114 -0
- package/dist/src/optimization/token-optimizer.js +257 -0
- package/dist/src/optimization/token-tracker.js +118 -0
- package/dist/src/orchestrator-instructions.js +128 -0
- package/dist/src/orchestrator-lite.js +139 -0
- package/dist/src/orchestrator.js +191 -0
- package/dist/src/orchestrators/collaborative/interfaces/IToolExecutionEngine.js +1 -0
- package/dist/src/orchestrators/collaborative/interfaces/IToolExecutionStrategy.js +5 -0
- package/dist/src/orchestrators/collaborative/interfaces/IVisualizationRenderer.js +1 -0
- package/dist/src/orchestrators/collaborative/registries/ModelProviderRegistry.js +95 -0
- package/dist/src/orchestrators/collaborative/registries/ToolAdapterRegistry.js +64 -0
- package/dist/src/orchestrators/collaborative/services/tool-execution/ToolExecutionService.js +502 -0
- package/dist/src/orchestrators/collaborative/services/visualization/VisualizationService.js +206 -0
- package/dist/src/orchestrators/collaborative/types/session-types.js +5 -0
- package/dist/src/profiles/balanced.js +37 -0
- package/dist/src/profiles/code_focus.js +37 -0
- package/dist/src/profiles/debug_intensive.js +59 -0
- package/dist/src/profiles/full.js +37 -0
- package/dist/src/profiles/minimal.js +37 -0
- package/dist/src/profiles/research_code.js +59 -0
- package/dist/src/profiles/research_power.js +37 -0
- package/dist/src/profiles/types.js +5 -0
- package/dist/src/profiles/workflow_builder.js +53 -0
- package/dist/src/prompt-engineer-lite.js +78 -0
- package/dist/src/prompt-engineer.js +399 -0
- package/dist/src/reasoning-chain.js +508 -0
- package/dist/src/sequential-thinking.js +291 -0
- package/dist/src/server-diagnostic.js +74 -0
- package/dist/src/server-raw.js +158 -0
- package/dist/src/server-simple.js +58 -0
- package/dist/src/server.js +514 -0
- package/dist/src/session/session-logger.js +617 -0
- package/dist/src/session/session-manager.js +571 -0
- package/dist/src/session/session-tools.js +400 -0
- package/dist/src/tools/advanced-modes.js +200 -0
- package/dist/src/tools/claude-integration.js +356 -0
- package/dist/src/tools/consolidated/ai-router.js +174 -0
- package/dist/src/tools/consolidated/ai-tool.js +48 -0
- package/dist/src/tools/consolidated/brainstorm-tool.js +87 -0
- package/dist/src/tools/consolidated/environment-detector.js +80 -0
- package/dist/src/tools/consolidated/index.js +50 -0
- package/dist/src/tools/consolidated/search-tool.js +110 -0
- package/dist/src/tools/consolidated/workflow-tool.js +238 -0
- package/dist/src/tools/gemini-tools.js +329 -0
- package/dist/src/tools/grok-enhanced.js +376 -0
- package/dist/src/tools/grok-tools.js +299 -0
- package/dist/src/tools/lmstudio-tools.js +223 -0
- package/dist/src/tools/openai-tools.js +498 -0
- package/dist/src/tools/openrouter-tools.js +317 -0
- package/dist/src/tools/optimized-wrapper.js +204 -0
- package/dist/src/tools/perplexity-tools.js +294 -0
- package/dist/src/tools/pingpong-tool.js +343 -0
- package/dist/src/tools/qwen-wrapper.js +74 -0
- package/dist/src/tools/tool-router.js +444 -0
- package/dist/src/tools/unified-ai-provider.js +260 -0
- package/dist/src/tools/workflow-runner.js +425 -0
- package/dist/src/tools/workflow-validator-tool.js +107 -0
- package/dist/src/types.js +23 -0
- package/dist/src/utils/input-validator.js +130 -0
- package/dist/src/utils/model-router.js +91 -0
- package/dist/src/utils/progress-stream.js +255 -0
- package/dist/src/utils/provider-router.js +88 -0
- package/dist/src/utils/smart-api-client.js +146 -0
- package/dist/src/utils/table-builder.js +218 -0
- package/dist/src/utils/timestamp-formatter.js +134 -0
- package/dist/src/utils/tool-compressor.js +257 -0
- package/dist/src/utils/tool-config.js +201 -0
- package/dist/src/validators/dependency-graph-validator.js +147 -0
- package/dist/src/validators/interpolation-validator.js +222 -0
- package/dist/src/validators/output-usage-validator.js +151 -0
- package/dist/src/validators/syntax-validator.js +102 -0
- package/dist/src/validators/tool-registry-validator.js +123 -0
- package/dist/src/validators/tool-types.js +97 -0
- package/dist/src/validators/types.js +8 -0
- package/dist/src/validators/workflow-validator.js +134 -0
- package/dist/src/visualizer-lite.js +42 -0
- package/dist/src/visualizer.js +179 -0
- package/dist/src/workflows/circuit-breaker.js +199 -0
- package/dist/src/workflows/custom-workflows.js +451 -0
- package/dist/src/workflows/engine/AutoSynthesizer.js +97 -0
- package/dist/src/workflows/engine/StepParameterResolver.js +74 -0
- package/dist/src/workflows/engine/VariableInterpolator.js +123 -0
- package/dist/src/workflows/engine/WorkflowDiscovery.js +125 -0
- package/dist/src/workflows/engine/WorkflowExecutionEngine.js +485 -0
- package/dist/src/workflows/engine/WorkflowExecutor.js +113 -0
- package/dist/src/workflows/engine/WorkflowFileManager.js +244 -0
- package/dist/src/workflows/engine/WorkflowHelpers.js +114 -0
- package/dist/src/workflows/engine/WorkflowOutputFormatter.js +83 -0
- package/dist/src/workflows/engine/events/WorkflowEventBus.js +132 -0
- package/dist/src/workflows/engine/events/interfaces/IEventBus.js +5 -0
- package/dist/src/workflows/engine/handlers/ErrorRecoveryHandler.js +162 -0
- package/dist/src/workflows/engine/handlers/PromptEnhancementHandler.js +115 -0
- package/dist/src/workflows/engine/handlers/SessionPersistenceHandler.js +167 -0
- package/dist/src/workflows/engine/handlers/StepExecutionHandler.js +231 -0
- package/dist/src/workflows/engine/handlers/ToolInvocationHandler.js +46 -0
- package/dist/src/workflows/engine/interfaces/IAutoSynthesizer.js +5 -0
- package/dist/src/workflows/engine/interfaces/IStepParameterResolver.js +5 -0
- package/dist/src/workflows/engine/interfaces/IVariableInterpolator.js +5 -0
- package/dist/src/workflows/engine/interfaces/IWorkflowDiscovery.js +4 -0
- package/dist/src/workflows/engine/interfaces/IWorkflowFileManager.js +5 -0
- package/dist/src/workflows/engine/interfaces/IWorkflowOutputFormatter.js +5 -0
- package/dist/src/workflows/engine/state/WorkflowStateMachine.js +194 -0
- package/dist/src/workflows/engine/state/interfaces/IStateMachine.js +17 -0
- package/dist/src/workflows/fallback-strategies.js +373 -0
- package/dist/src/workflows/message-queue.js +455 -0
- package/dist/src/workflows/model-router.js +189 -0
- package/dist/src/workflows/orchestrator-examples.js +174 -0
- package/dist/src/workflows/orchestrator-integration.js +200 -0
- package/dist/src/workflows/self-healing.js +524 -0
- package/dist/src/workflows/tool-mapper.js +407 -0
- package/dist/src/workflows/tool-orchestrator.js +796 -0
- package/dist/src/workflows/workflow-engine.js +573 -0
- package/dist/src/workflows/workflow-parser.js +283 -0
- package/dist/src/workflows/workflow-types.js +95 -0
- package/dist/src/workflows.js +568 -0
- package/dist/test-workflow-file-output.js +93 -0
- package/docs/API_KEYS.md +570 -0
- package/docs/CLAUDE_CODE_SETUP.md +181 -0
- package/docs/CLAUDE_DESKTOP_MANUAL.md +127 -0
- package/docs/CONFIGURATION.md +745 -0
- package/docs/FOCUS_MODES.md +240 -0
- package/docs/INSTALLATION_BOTH.md +145 -0
- package/docs/TERMS.md +352 -0
- package/docs/TOOLS_REFERENCE.md +1622 -0
- package/docs/TOOL_PARAMETERS.md +496 -0
- package/docs/TOOL_PROFILES.md +236 -0
- package/docs/WORKFLOWS.md +987 -0
- package/docs/WORKFLOW_OUTPUT.md +198 -0
- package/docs/WORKFLOW_PROGRESS_TRACKING.md +305 -0
- package/docs/workflows/design-brainstorm.md +335 -0
- package/package.json +97 -0
- package/profiles/balanced.json +37 -0
- package/profiles/code_focus.json +37 -0
- package/profiles/debug_intensive.json +34 -0
- package/profiles/full.json +37 -0
- package/profiles/minimal.json +37 -0
- package/profiles/research_power.json +37 -0
- package/profiles/workflow_builder.json +37 -0
- package/smithery.yaml +66 -0
- package/start.sh +8 -0
- package/tools.config.json +81 -0
- package/tsconfig.json +18 -0
- package/workflows/accessibility-code-audit.yaml +92 -0
- package/workflows/code-architecture-review.yaml +202 -0
- package/workflows/code-review.yaml +142 -0
- package/workflows/core/iterative-problem-solver.yaml +283 -0
- package/workflows/creative-brainstorm-yaml.yaml +215 -0
- package/workflows/pingpong.yaml +141 -0
- package/workflows/system/README.md +412 -0
- package/workflows/system/challenger.yaml +175 -0
- package/workflows/system/scout.yaml +164 -0
- package/workflows/system/verifier.yaml +133 -0
- package/workflows/ultra-creative-brainstorm.yaml +318 -0
- package/workflows/ux-research-flow.yaml +92 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow validator MCP tool
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { workflowValidator } from '../validators/workflow-validator.js';
|
|
6
|
+
// Get all registered tools from the server
|
|
7
|
+
// We'll treat ALL known tools as "enabled" for validation purposes
|
|
8
|
+
// The tool validator will distinguish between "unknown" (error) and "disabled" (warning)
|
|
9
|
+
function getAllKnownTools() {
|
|
10
|
+
// Return all known tool names
|
|
11
|
+
return new Set([
|
|
12
|
+
// Core
|
|
13
|
+
'think', 'focus', 'nextThought',
|
|
14
|
+
// Perplexity
|
|
15
|
+
'perplexity_ask', 'perplexity_reason', 'perplexity_research',
|
|
16
|
+
// Grok
|
|
17
|
+
'grok_reason', 'grok_code', 'grok_debug', 'grok_architect', 'grok_brainstorm', 'grok_search',
|
|
18
|
+
// OpenAI
|
|
19
|
+
'openai_compare', 'openai_brainstorm', 'openai_gpt5_reason', 'openai_code_review', 'openai_explain',
|
|
20
|
+
// Gemini
|
|
21
|
+
'gemini_brainstorm', 'gemini_analyze_code', 'gemini_analyze_text',
|
|
22
|
+
// Qwen
|
|
23
|
+
'qwen_coder',
|
|
24
|
+
// Kimi
|
|
25
|
+
'kimi_thinking',
|
|
26
|
+
// Advanced modes
|
|
27
|
+
'verifier', 'scout', 'challenger', 'hunter',
|
|
28
|
+
// Workflow
|
|
29
|
+
'workflow', 'list_workflows', 'create_workflow', 'visualize_workflow',
|
|
30
|
+
// Collaborative
|
|
31
|
+
'pingpong', 'qwen_competitive'
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
export const validateWorkflowTool = {
|
|
35
|
+
name: 'validate_workflow',
|
|
36
|
+
description: `Validates workflow YAML/JSON files for correctness.
|
|
37
|
+
|
|
38
|
+
Checks:
|
|
39
|
+
- Valid YAML/JSON syntax
|
|
40
|
+
- Interpolation references (\${step.output}, \${variable}) point to valid steps/variables
|
|
41
|
+
- Tool names exist and are enabled in tools.config.json
|
|
42
|
+
- No circular dependencies in step execution order
|
|
43
|
+
- Variable names follow snake_case convention
|
|
44
|
+
|
|
45
|
+
Returns detailed error messages with suggestions for fixing issues.`,
|
|
46
|
+
parameters: z.object({
|
|
47
|
+
workflowContent: z.string()
|
|
48
|
+
.describe('The YAML or JSON content of the workflow to validate'),
|
|
49
|
+
isJson: z.boolean()
|
|
50
|
+
.optional()
|
|
51
|
+
.default(false)
|
|
52
|
+
.describe('Set to true if the content is JSON instead of YAML'),
|
|
53
|
+
format: z.enum(['text', 'json'])
|
|
54
|
+
.optional()
|
|
55
|
+
.default('text')
|
|
56
|
+
.describe('Output format: "text" for human-readable, "json" for structured data')
|
|
57
|
+
}),
|
|
58
|
+
execute: async (args) => {
|
|
59
|
+
try {
|
|
60
|
+
const result = await workflowValidator.validate(args.workflowContent, args.isJson || false, getAllKnownTools());
|
|
61
|
+
if (args.format === 'json') {
|
|
62
|
+
return workflowValidator.formatResultsJSON(result);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return workflowValidator.formatResults(result);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return `❌ Validation error: ${error.message}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export const validateWorkflowFileTool = {
|
|
74
|
+
name: 'validate_workflow_file',
|
|
75
|
+
description: `Validates a workflow file from the filesystem.
|
|
76
|
+
|
|
77
|
+
Checks:
|
|
78
|
+
- Valid YAML/JSON syntax
|
|
79
|
+
- Interpolation references (\${step.output}, \${variable}) point to valid steps/variables
|
|
80
|
+
- Tool names exist and are enabled in tools.config.json
|
|
81
|
+
- No circular dependencies in step execution order
|
|
82
|
+
- Variable names follow snake_case convention
|
|
83
|
+
|
|
84
|
+
Returns detailed error messages with suggestions for fixing issues.`,
|
|
85
|
+
parameters: z.object({
|
|
86
|
+
filePath: z.string()
|
|
87
|
+
.describe('Path to the workflow file (YAML or JSON)'),
|
|
88
|
+
format: z.enum(['text', 'json'])
|
|
89
|
+
.optional()
|
|
90
|
+
.default('text')
|
|
91
|
+
.describe('Output format: "text" for human-readable, "json" for structured data')
|
|
92
|
+
}),
|
|
93
|
+
execute: async (args) => {
|
|
94
|
+
try {
|
|
95
|
+
const result = await workflowValidator.validateFile(args.filePath, getAllKnownTools());
|
|
96
|
+
if (args.format === 'json') {
|
|
97
|
+
return workflowValidator.formatResultsJSON(result);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return workflowValidator.formatResults(result);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return `❌ Validation error: ${error.message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export var WorkflowType;
|
|
2
|
+
(function (WorkflowType) {
|
|
3
|
+
WorkflowType["CREATIVE_DISCOVERY"] = "creative_discovery";
|
|
4
|
+
WorkflowType["DEEP_RESEARCH"] = "deep_research";
|
|
5
|
+
WorkflowType["PROBLEM_SOLVING"] = "problem_solving";
|
|
6
|
+
WorkflowType["SYNTHESIS"] = "synthesis";
|
|
7
|
+
WorkflowType["FACT_CHECK"] = "fact_check";
|
|
8
|
+
// New Developer-Focused Workflows
|
|
9
|
+
WorkflowType["RAPID_PROTOTYPE"] = "rapid_prototype";
|
|
10
|
+
WorkflowType["CODE_QUALITY"] = "code_quality";
|
|
11
|
+
WorkflowType["SECURE_DEPLOYMENT"] = "secure_deployment";
|
|
12
|
+
WorkflowType["FULL_STACK_DEVELOPMENT"] = "full_stack_development";
|
|
13
|
+
WorkflowType["TEST_DRIVEN_DEVELOPMENT"] = "test_driven_development";
|
|
14
|
+
WorkflowType["CODE_REVIEW_WORKFLOW"] = "code_review_workflow";
|
|
15
|
+
WorkflowType["DOCUMENTATION_GENERATION"] = "documentation_generation";
|
|
16
|
+
})(WorkflowType || (WorkflowType = {}));
|
|
17
|
+
export var ToolStatus;
|
|
18
|
+
(function (ToolStatus) {
|
|
19
|
+
ToolStatus["IDLE"] = "idle";
|
|
20
|
+
ToolStatus["PROCESSING"] = "processing";
|
|
21
|
+
ToolStatus["COMPLETE"] = "complete";
|
|
22
|
+
ToolStatus["ERROR"] = "error";
|
|
23
|
+
})(ToolStatus || (ToolStatus = {}));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation and Sanitization Utilities
|
|
3
|
+
* Prevents prompt injection, XSS, and other security issues
|
|
4
|
+
*/
|
|
5
|
+
// Security constants
|
|
6
|
+
export const MAX_INPUT_LENGTH = 50000; // 50k chars max
|
|
7
|
+
export const MAX_PROMPT_LENGTH = 20000;
|
|
8
|
+
export const MAX_SYSTEM_PROMPT_LENGTH = 5000;
|
|
9
|
+
// Patterns to detect potential injection attempts
|
|
10
|
+
// NOTE: Role injection (user:/assistant:/system:) pattern removed due to false positives
|
|
11
|
+
// with legitimate LLM-generated content. For LLM-to-LLM calls, use skipValidation flag.
|
|
12
|
+
const SUSPICIOUS_PATTERNS = [
|
|
13
|
+
/<\s*script/gi, // XSS attempts
|
|
14
|
+
/\b(exec|eval|require)\s*\(/gi, // Code execution attempts (must be function calls)
|
|
15
|
+
/;\s*(rm|del|format|drop\s+table)/gi, // Command/SQL injection
|
|
16
|
+
/\.\.\//g, // Path traversal
|
|
17
|
+
/\x00/g, // Null byte injection
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Sanitize user input - remove control characters and limit length
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizeInput(input, maxLength = MAX_INPUT_LENGTH) {
|
|
23
|
+
if (typeof input !== 'string') {
|
|
24
|
+
throw new Error('Input must be a string');
|
|
25
|
+
}
|
|
26
|
+
// Remove control characters (except newlines, tabs)
|
|
27
|
+
let sanitized = input.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '');
|
|
28
|
+
// Limit length
|
|
29
|
+
if (sanitized.length > maxLength) {
|
|
30
|
+
sanitized = sanitized.slice(0, maxLength);
|
|
31
|
+
}
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate and sanitize tool input
|
|
36
|
+
*/
|
|
37
|
+
export function validateToolInput(input) {
|
|
38
|
+
try {
|
|
39
|
+
if (typeof input === 'string') {
|
|
40
|
+
// Check for empty input
|
|
41
|
+
if (input.trim().length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: 'Input cannot be empty'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Check length before sanitization
|
|
48
|
+
if (input.length > MAX_INPUT_LENGTH) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
error: `Input too long (max ${MAX_INPUT_LENGTH} characters)`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Check for suspicious patterns
|
|
55
|
+
for (const pattern of SUSPICIOUS_PATTERNS) {
|
|
56
|
+
if (pattern.test(input)) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
error: 'Input contains potentially malicious content'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
valid: true,
|
|
65
|
+
sanitized: sanitizeInput(input)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (typeof input === 'object' && input !== null) {
|
|
69
|
+
// Recursively sanitize object properties
|
|
70
|
+
const sanitized = Array.isArray(input) ? [] : {};
|
|
71
|
+
for (const [key, value] of Object.entries(input)) {
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
const result = validateToolInput(value);
|
|
74
|
+
if (!result.valid)
|
|
75
|
+
return result;
|
|
76
|
+
sanitized[key] = result.sanitized;
|
|
77
|
+
}
|
|
78
|
+
else if (typeof value === 'object') {
|
|
79
|
+
const result = validateToolInput(value);
|
|
80
|
+
if (!result.valid)
|
|
81
|
+
return result;
|
|
82
|
+
sanitized[key] = result.sanitized;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
sanitized[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { valid: true, sanitized };
|
|
89
|
+
}
|
|
90
|
+
return { valid: true, sanitized: input };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
error: error instanceof Error ? error.message : 'Validation error'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Redact secrets from strings before logging
|
|
101
|
+
*/
|
|
102
|
+
export function sanitizeForLogging(text) {
|
|
103
|
+
let sanitized = text;
|
|
104
|
+
// Redact API keys
|
|
105
|
+
sanitized = sanitized.replace(/Bearer\s+[\w-]+/gi, 'Bearer [REDACTED]');
|
|
106
|
+
sanitized = sanitized.replace(/sk-[\w-]+/g, 'sk-[REDACTED]');
|
|
107
|
+
sanitized = sanitized.replace(/gsk_[\w-]+/g, 'gsk_[REDACTED]');
|
|
108
|
+
// Redact environment variable values
|
|
109
|
+
sanitized = sanitized.replace(/(API_KEY|TOKEN|SECRET)=[\w-]+/gi, '$1=[REDACTED]');
|
|
110
|
+
// Redact potential tokens in errors
|
|
111
|
+
sanitized = sanitized.replace(/["'][\w-]{32,}["']/g, '"[REDACTED]"');
|
|
112
|
+
return sanitized;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validate model name to prevent injection
|
|
116
|
+
*/
|
|
117
|
+
export function validateModelName(model) {
|
|
118
|
+
// Allow only alphanumeric, hyphens, underscores, dots
|
|
119
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
120
|
+
return validPattern.test(model) && model.length < 100;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sanitize error for safe logging
|
|
124
|
+
*/
|
|
125
|
+
export function sanitizeError(error) {
|
|
126
|
+
if (error instanceof Error) {
|
|
127
|
+
return sanitizeForLogging(error.message);
|
|
128
|
+
}
|
|
129
|
+
return sanitizeForLogging(String(error));
|
|
130
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Model Router Utility
|
|
3
|
+
*
|
|
4
|
+
* Centralizes AI provider routing logic to eliminate duplication across
|
|
5
|
+
* Challenger, Verifier, Scout, and other tools.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Routes model requests to appropriate AI provider based on model name
|
|
9
|
+
*/
|
|
10
|
+
export class SharedModelRouter {
|
|
11
|
+
/**
|
|
12
|
+
* Call any AI model with automatic provider routing
|
|
13
|
+
*/
|
|
14
|
+
async callModel(options) {
|
|
15
|
+
const { model, prompt, maxTokens = 2000, temperature = 0.7, messages } = options;
|
|
16
|
+
const requestMessages = messages || [{ role: 'user', content: prompt }];
|
|
17
|
+
try {
|
|
18
|
+
let content;
|
|
19
|
+
let provider;
|
|
20
|
+
// Route to appropriate provider based on model name
|
|
21
|
+
if (model.includes('gemini')) {
|
|
22
|
+
const { callGemini } = await import('../tools/gemini-tools.js');
|
|
23
|
+
content = await callGemini(prompt, model, undefined, temperature);
|
|
24
|
+
provider = 'Google Gemini';
|
|
25
|
+
}
|
|
26
|
+
else if (model.includes('grok')) {
|
|
27
|
+
const { callGrok } = await import('../tools/grok-tools.js');
|
|
28
|
+
content = await callGrok(requestMessages, model, temperature, maxTokens);
|
|
29
|
+
provider = 'xAI Grok';
|
|
30
|
+
}
|
|
31
|
+
else if (model.includes('qwen') || model.includes('qwq')) {
|
|
32
|
+
// Qwen models go through OpenRouter
|
|
33
|
+
const { callOpenRouter } = await import('../tools/openrouter-tools.js');
|
|
34
|
+
content = await callOpenRouter(requestMessages, model, temperature, maxTokens);
|
|
35
|
+
provider = 'OpenRouter';
|
|
36
|
+
}
|
|
37
|
+
else if (model.includes('gpt')) {
|
|
38
|
+
const { callOpenAI } = await import('../tools/openai-tools.js');
|
|
39
|
+
content = await callOpenAI(requestMessages, model, temperature, maxTokens);
|
|
40
|
+
provider = 'OpenAI';
|
|
41
|
+
}
|
|
42
|
+
else if (model.includes('perplexity') || model.includes('sonar')) {
|
|
43
|
+
const { callPerplexity } = await import('../tools/perplexity-tools.js');
|
|
44
|
+
content = await callPerplexity(requestMessages, model);
|
|
45
|
+
provider = 'Perplexity';
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new Error(`Unknown model: ${model}. Supported providers: Gemini, Grok, OpenRouter (qwen/qwq), OpenAI (gpt), Perplexity (sonar)`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content,
|
|
52
|
+
tokens: Math.floor(content.length / 4),
|
|
53
|
+
provider
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(`Error calling model ${model}:`, error);
|
|
58
|
+
throw new Error(`Failed to call ${model}: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get provider name for a model (useful for logging)
|
|
63
|
+
*/
|
|
64
|
+
getProviderName(model) {
|
|
65
|
+
if (model.includes('gemini'))
|
|
66
|
+
return 'Google Gemini';
|
|
67
|
+
if (model.includes('grok'))
|
|
68
|
+
return 'xAI Grok';
|
|
69
|
+
if (model.includes('qwen') || model.includes('qwq'))
|
|
70
|
+
return 'OpenRouter';
|
|
71
|
+
if (model.includes('gpt'))
|
|
72
|
+
return 'OpenAI';
|
|
73
|
+
if (model.includes('perplexity') || model.includes('sonar'))
|
|
74
|
+
return 'Perplexity';
|
|
75
|
+
return 'Unknown';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if a model is supported
|
|
79
|
+
*/
|
|
80
|
+
isModelSupported(model) {
|
|
81
|
+
return (model.includes('gemini') ||
|
|
82
|
+
model.includes('grok') ||
|
|
83
|
+
model.includes('gpt') ||
|
|
84
|
+
model.includes('qwen') ||
|
|
85
|
+
model.includes('qwq') ||
|
|
86
|
+
model.includes('perplexity') ||
|
|
87
|
+
model.includes('sonar'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Singleton instance
|
|
91
|
+
export const modelRouter = new SharedModelRouter();
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Streaming Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time progress updates for long-running MCP operations.
|
|
5
|
+
* Users see incremental output instead of waiting for final result.
|
|
6
|
+
*/
|
|
7
|
+
export class ProgressStream {
|
|
8
|
+
constructor(totalSteps = 1) {
|
|
9
|
+
this.updates = [];
|
|
10
|
+
this.currentStep = 0;
|
|
11
|
+
this.startTime = new Date();
|
|
12
|
+
this.totalSteps = totalSteps;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Emit progress update to stderr (visible to user in real-time)
|
|
16
|
+
*/
|
|
17
|
+
emit(update) {
|
|
18
|
+
this.updates.push(update);
|
|
19
|
+
// Format for CLI output
|
|
20
|
+
const elapsed = Math.floor((update.timestamp.getTime() - this.startTime.getTime()) / 1000);
|
|
21
|
+
const timeStr = `[${elapsed}s]`;
|
|
22
|
+
let output = '';
|
|
23
|
+
switch (update.type) {
|
|
24
|
+
case 'start':
|
|
25
|
+
output = `\n🚀 ${update.message}`;
|
|
26
|
+
break;
|
|
27
|
+
case 'progress':
|
|
28
|
+
const progressBar = this.renderProgressBar(update.percentage || 0);
|
|
29
|
+
output = `\r${timeStr} ${progressBar} ${update.message}`;
|
|
30
|
+
break;
|
|
31
|
+
case 'step':
|
|
32
|
+
output = `\n${timeStr} ⚙️ Step ${this.currentStep}/${this.totalSteps}: ${update.message}`;
|
|
33
|
+
break;
|
|
34
|
+
case 'complete':
|
|
35
|
+
output = `\n\n✅ ${update.message} (completed in ${elapsed}s)`;
|
|
36
|
+
break;
|
|
37
|
+
case 'error':
|
|
38
|
+
output = `\n\n❌ ${update.message}`;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
// Write to stderr so it doesn't interfere with MCP JSON-RPC on stdout
|
|
42
|
+
console.error(output);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Render ASCII progress bar
|
|
46
|
+
*/
|
|
47
|
+
renderProgressBar(percentage) {
|
|
48
|
+
const width = 20;
|
|
49
|
+
const filled = Math.floor(width * (percentage / 100));
|
|
50
|
+
const empty = width - filled;
|
|
51
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${percentage.toFixed(0)}%`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Mark operation start
|
|
55
|
+
*/
|
|
56
|
+
start(message) {
|
|
57
|
+
this.emit({
|
|
58
|
+
type: 'start',
|
|
59
|
+
message,
|
|
60
|
+
timestamp: new Date()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update progress
|
|
65
|
+
*/
|
|
66
|
+
progress(message, step, total) {
|
|
67
|
+
if (step !== undefined && total !== undefined) {
|
|
68
|
+
this.currentStep = step;
|
|
69
|
+
this.totalSteps = total;
|
|
70
|
+
const percentage = (step / total) * 100;
|
|
71
|
+
this.emit({
|
|
72
|
+
type: 'progress',
|
|
73
|
+
message,
|
|
74
|
+
percentage,
|
|
75
|
+
timestamp: new Date()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.emit({
|
|
80
|
+
type: 'progress',
|
|
81
|
+
message,
|
|
82
|
+
percentage: undefined,
|
|
83
|
+
timestamp: new Date()
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mark step completion
|
|
89
|
+
*/
|
|
90
|
+
step(message, stepNumber) {
|
|
91
|
+
if (stepNumber !== undefined) {
|
|
92
|
+
this.currentStep = stepNumber;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.currentStep++;
|
|
96
|
+
}
|
|
97
|
+
this.emit({
|
|
98
|
+
type: 'step',
|
|
99
|
+
message,
|
|
100
|
+
metadata: { stepNumber: this.currentStep, totalSteps: this.totalSteps },
|
|
101
|
+
timestamp: new Date()
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Mark operation complete
|
|
106
|
+
*/
|
|
107
|
+
complete(message) {
|
|
108
|
+
this.emit({
|
|
109
|
+
type: 'complete',
|
|
110
|
+
message,
|
|
111
|
+
timestamp: new Date()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Mark operation error
|
|
116
|
+
*/
|
|
117
|
+
error(message) {
|
|
118
|
+
this.emit({
|
|
119
|
+
type: 'error',
|
|
120
|
+
message,
|
|
121
|
+
timestamp: new Date()
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get all updates
|
|
126
|
+
*/
|
|
127
|
+
getUpdates() {
|
|
128
|
+
return [...this.updates];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get duration in seconds
|
|
132
|
+
*/
|
|
133
|
+
getDuration() {
|
|
134
|
+
return Math.floor((new Date().getTime() - this.startTime.getTime()) / 1000);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create progress stream for operation
|
|
139
|
+
*/
|
|
140
|
+
export function createProgressStream(totalSteps = 1) {
|
|
141
|
+
return new ProgressStream(totalSteps);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Wrap async operation with progress tracking
|
|
145
|
+
*/
|
|
146
|
+
export async function withProgress(operation, operationName, totalSteps) {
|
|
147
|
+
const stream = new ProgressStream(totalSteps || 1);
|
|
148
|
+
stream.start(operationName);
|
|
149
|
+
try {
|
|
150
|
+
const result = await operation(stream);
|
|
151
|
+
stream.complete(`${operationName} finished`);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
156
|
+
stream.error(`${operationName} failed: ${errorMsg}`);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Progress reporter for multi-model operations
|
|
162
|
+
*/
|
|
163
|
+
export class MultiModelProgressReporter {
|
|
164
|
+
constructor(models, operationName) {
|
|
165
|
+
this.models = models;
|
|
166
|
+
this.stream = new ProgressStream(models.length);
|
|
167
|
+
this.results = new Map(models.map(m => [m, { status: 'pending' }]));
|
|
168
|
+
this.stream.start(`${operationName} with ${models.length} models`);
|
|
169
|
+
this.printModelTable();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Print table of models and their status
|
|
173
|
+
*/
|
|
174
|
+
printModelTable() {
|
|
175
|
+
console.error('\n📊 Model Status:');
|
|
176
|
+
console.error('┌' + '─'.repeat(50) + '┐');
|
|
177
|
+
this.models.forEach(model => {
|
|
178
|
+
const status = this.results.get(model)?.status || 'pending';
|
|
179
|
+
const icon = this.getStatusIcon(status);
|
|
180
|
+
const padding = ' '.repeat(Math.max(0, 40 - model.length));
|
|
181
|
+
console.error(`│ ${icon} ${model}${padding} │`);
|
|
182
|
+
});
|
|
183
|
+
console.error('└' + '─'.repeat(50) + '┘\n');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get icon for status
|
|
187
|
+
*/
|
|
188
|
+
getStatusIcon(status) {
|
|
189
|
+
switch (status) {
|
|
190
|
+
case 'pending': return '⏳';
|
|
191
|
+
case 'running': return '🔄';
|
|
192
|
+
case 'complete': return '✅';
|
|
193
|
+
case 'error': return '❌';
|
|
194
|
+
default: return '�';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Mark model as running
|
|
199
|
+
*/
|
|
200
|
+
modelStarted(model) {
|
|
201
|
+
const result = this.results.get(model);
|
|
202
|
+
if (result) {
|
|
203
|
+
result.status = 'running';
|
|
204
|
+
this.stream.step(`${model} processing...`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Mark model as complete
|
|
209
|
+
*/
|
|
210
|
+
modelCompleted(model, output) {
|
|
211
|
+
const result = this.results.get(model);
|
|
212
|
+
if (result) {
|
|
213
|
+
result.status = 'complete';
|
|
214
|
+
result.output = output;
|
|
215
|
+
const completed = Array.from(this.results.values()).filter(r => r.status === 'complete').length;
|
|
216
|
+
this.stream.progress(`${completed}/${this.models.length} models completed`, completed, this.models.length);
|
|
217
|
+
// Show preview of output
|
|
218
|
+
const preview = output.substring(0, 100).replace(/\n/g, ' ');
|
|
219
|
+
console.error(` 📝 Preview: ${preview}${output.length > 100 ? '...' : ''}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Mark model as error
|
|
224
|
+
*/
|
|
225
|
+
modelFailed(model, error) {
|
|
226
|
+
const result = this.results.get(model);
|
|
227
|
+
if (result) {
|
|
228
|
+
result.status = 'error';
|
|
229
|
+
console.error(` ⚠️ ${model} failed: ${error}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Complete all operations
|
|
234
|
+
*/
|
|
235
|
+
complete(message) {
|
|
236
|
+
const completed = Array.from(this.results.values()).filter(r => r.status === 'complete').length;
|
|
237
|
+
const failed = Array.from(this.results.values()).filter(r => r.status === 'error').length;
|
|
238
|
+
const summary = message || `${completed} models completed${failed > 0 ? `, ${failed} failed` : ''}`;
|
|
239
|
+
this.stream.complete(summary);
|
|
240
|
+
// Print final table
|
|
241
|
+
this.printModelTable();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get progress stream
|
|
245
|
+
*/
|
|
246
|
+
getStream() {
|
|
247
|
+
return this.stream;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Create progress reporter for multi-model operation
|
|
252
|
+
*/
|
|
253
|
+
export function createMultiModelReporter(models, operationName) {
|
|
254
|
+
return new MultiModelProgressReporter(models, operationName);
|
|
255
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderRouter - Smart routing and failover between multiple providers
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Automatic failover on errors
|
|
6
|
+
* - Priority-based provider selection
|
|
7
|
+
* - Integration with SmartAPIClient for retries
|
|
8
|
+
* - NO metrics collection - privacy-first design
|
|
9
|
+
*/
|
|
10
|
+
import { smartAPIClient } from './smart-api-client.js';
|
|
11
|
+
export class ProviderRouter {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.smartClient = smartAPIClient;
|
|
14
|
+
}
|
|
15
|
+
static getInstance() {
|
|
16
|
+
if (!ProviderRouter.instance) {
|
|
17
|
+
ProviderRouter.instance = new ProviderRouter();
|
|
18
|
+
}
|
|
19
|
+
return ProviderRouter.instance;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Route request through providers with automatic failover
|
|
23
|
+
*/
|
|
24
|
+
async route(providers, request, apiConfig) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const failedProviders = [];
|
|
27
|
+
let attempts = 0;
|
|
28
|
+
// Filter and sort providers
|
|
29
|
+
const availableProviders = this.selectAvailableProviders(providers);
|
|
30
|
+
if (availableProviders.length === 0) {
|
|
31
|
+
throw new Error('[ProviderRouter] No providers available or enabled');
|
|
32
|
+
}
|
|
33
|
+
console.log(`[ProviderRouter] Available providers: ${availableProviders.map(p => p.name).join(', ')}`);
|
|
34
|
+
// Try each provider in order
|
|
35
|
+
for (const provider of availableProviders) {
|
|
36
|
+
attempts++;
|
|
37
|
+
try {
|
|
38
|
+
console.log(`[ProviderRouter] Attempting ${provider.name} (attempt ${attempts}/${availableProviders.length})`);
|
|
39
|
+
const result = await this.smartClient.callWithRetries(provider.callable, {
|
|
40
|
+
provider: provider.name,
|
|
41
|
+
...apiConfig
|
|
42
|
+
});
|
|
43
|
+
const totalTime = Date.now() - startTime;
|
|
44
|
+
console.log(`[ProviderRouter] Success with ${provider.name} ` +
|
|
45
|
+
`(total time: ${totalTime}ms, attempts: ${attempts})`);
|
|
46
|
+
return {
|
|
47
|
+
result,
|
|
48
|
+
provider: provider.name,
|
|
49
|
+
attempts,
|
|
50
|
+
totalTime,
|
|
51
|
+
failedProviders
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`[ProviderRouter] Provider ${provider.name} failed: ${error.message}`);
|
|
56
|
+
failedProviders.push(provider.name);
|
|
57
|
+
// If this was the last provider, throw
|
|
58
|
+
if (attempts >= availableProviders.length) {
|
|
59
|
+
const totalTime = Date.now() - startTime;
|
|
60
|
+
throw new Error(`[ProviderRouter] All providers exhausted (${availableProviders.map(p => p.name).join(', ')}) ` +
|
|
61
|
+
`after ${attempts} attempts in ${totalTime}ms. Last error: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
// Otherwise continue to next provider
|
|
64
|
+
console.log(`[ProviderRouter] Failing over to next provider...`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Should never reach here, but TypeScript needs it
|
|
68
|
+
throw new Error('[ProviderRouter] Unexpected routing failure');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Select and order available providers based on priority
|
|
72
|
+
*/
|
|
73
|
+
selectAvailableProviders(providers) {
|
|
74
|
+
// Filter enabled providers
|
|
75
|
+
const enabled = providers.filter(p => p.enabled);
|
|
76
|
+
if (enabled.length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
// Sort by priority (lower first)
|
|
80
|
+
return enabled.sort((a, b) => {
|
|
81
|
+
const priorityA = a.priority ?? 999;
|
|
82
|
+
const priorityB = b.priority ?? 999;
|
|
83
|
+
return priorityA - priorityB;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Export singleton instance
|
|
88
|
+
export const providerRouter = ProviderRouter.getInstance();
|