winter-super-cli 2026.6.23 → 2026.6.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/bin/winter.js +23 -1
- package/extensions/vscode/src/extension.js +34 -7
- package/package.json +3 -2
- package/skill.md +4 -1
- package/src/agent/agent-definitions.js +4 -4
- package/src/ai/prompts/system-prompt.js +2 -0
- package/src/cli/commands.js +32 -1
- package/src/cli/config.js +4 -3
- package/src/cli/prompt-builder.js +32 -8
- package/src/cli/repl-commands.js +6 -0
- package/src/cli/repl.js +269 -13
- package/src/cli/slash-commands.js +2 -1
- package/src/mcp/client.js +8 -6
- package/src/mcp/ide-server.js +114 -2
- package/src/mcp/presets.js +114 -0
- package/src/rag/cli.js +195 -0
- package/src/rag/embeddings.js +183 -0
- package/src/rag/rag-engine.js +187 -0
- package/src/rag/retriever.js +112 -0
- package/src/rag/vector-store.js +225 -0
- package/src/tools/executor.js +103 -4
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const CHROME_DEVTOOLS_MCP_NAME = 'chrome-devtools';
|
|
2
|
+
|
|
3
|
+
const CHROME_DEVTOOLS_PACKAGE = 'chrome-devtools-mcp@latest';
|
|
4
|
+
const CHROME_DEVTOOLS_SOURCE = 'https://github.com/ChromeDevTools/chrome-devtools-mcp';
|
|
5
|
+
|
|
6
|
+
const CHROME_DEVTOOLS_FLAGS_WITH_VALUES = new Set([
|
|
7
|
+
'--browser-url',
|
|
8
|
+
'--channel',
|
|
9
|
+
'--executablePath',
|
|
10
|
+
'--logFile',
|
|
11
|
+
'--viewport',
|
|
12
|
+
'--proxy-server',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const CHROME_DEVTOOLS_BOOLEAN_FLAGS = new Set([
|
|
16
|
+
'--headless',
|
|
17
|
+
'--isolated',
|
|
18
|
+
'--acceptInsecureCerts',
|
|
19
|
+
'--help',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function normalizeMcpPresetName(name = '') {
|
|
23
|
+
return String(name || '').trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isChromeDevtoolsPreset(name = '') {
|
|
27
|
+
return ['chrome-devtools', 'chromedevtools', 'chrome', 'devtools', 'cdp'].includes(normalizeMcpPresetName(name));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildChromeDevtoolsArgs(options = []) {
|
|
31
|
+
const input = Array.isArray(options) ? [...options] : [];
|
|
32
|
+
const args = ['-y', CHROME_DEVTOOLS_PACKAGE];
|
|
33
|
+
|
|
34
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
35
|
+
const flag = input[index];
|
|
36
|
+
if (!String(flag || '').startsWith('--')) continue;
|
|
37
|
+
|
|
38
|
+
if (CHROME_DEVTOOLS_BOOLEAN_FLAGS.has(flag)) {
|
|
39
|
+
args.push(flag);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (CHROME_DEVTOOLS_FLAGS_WITH_VALUES.has(flag)) {
|
|
44
|
+
const value = input[index + 1];
|
|
45
|
+
if (value === undefined || String(value).startsWith('--')) {
|
|
46
|
+
throw new Error(`Missing value for ${flag}`);
|
|
47
|
+
}
|
|
48
|
+
args.push(flag, String(value));
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createChromeDevtoolsMcpServer(options = [], platform = process.platform, env = process.env) {
|
|
57
|
+
const npxArgs = buildChromeDevtoolsArgs(options);
|
|
58
|
+
const common = {
|
|
59
|
+
name: CHROME_DEVTOOLS_MCP_NAME,
|
|
60
|
+
enabled: true,
|
|
61
|
+
requestTimeoutMs: 60000,
|
|
62
|
+
metadata: {
|
|
63
|
+
preset: CHROME_DEVTOOLS_MCP_NAME,
|
|
64
|
+
source: CHROME_DEVTOOLS_SOURCE,
|
|
65
|
+
purpose: 'Chrome DevTools MCP for live browser automation, debugging, screenshots, console, network, and performance traces.',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (platform === 'win32') {
|
|
70
|
+
return {
|
|
71
|
+
...common,
|
|
72
|
+
command: 'cmd',
|
|
73
|
+
args: ['/c', 'npx', ...npxArgs],
|
|
74
|
+
env: {
|
|
75
|
+
SystemRoot: env.SystemRoot || 'C:\\Windows',
|
|
76
|
+
PROGRAMFILES: env.PROGRAMFILES || 'C:\\Program Files',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...common,
|
|
83
|
+
command: 'npx',
|
|
84
|
+
args: npxArgs,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getMcpPreset(name, options = []) {
|
|
89
|
+
if (isChromeDevtoolsPreset(name)) {
|
|
90
|
+
return createChromeDevtoolsMcpServer(options);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Unknown MCP preset: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function ensureMcpConfigShape(config = {}) {
|
|
96
|
+
config.mcp = config.mcp || { servers: [] };
|
|
97
|
+
config.mcp.servers = Array.isArray(config.mcp.servers) ? config.mcp.servers : [];
|
|
98
|
+
config.permissions = config.permissions || { allowlist: {} };
|
|
99
|
+
config.permissions.allowlist = config.permissions.allowlist || {};
|
|
100
|
+
config.permissions.allowlist.tools = config.permissions.allowlist.tools || [];
|
|
101
|
+
config.permissions.allowlist.commands = config.permissions.allowlist.commands || [];
|
|
102
|
+
config.permissions.allowlist.mcpServers = config.permissions.allowlist.mcpServers || [];
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function upsertMcpServer(config, server) {
|
|
107
|
+
ensureMcpConfigShape(config);
|
|
108
|
+
config.mcp.servers = config.mcp.servers.filter(item => item.name !== server.name);
|
|
109
|
+
config.mcp.servers.push(server);
|
|
110
|
+
config.permissions.allowlist.mcpServers = [
|
|
111
|
+
...new Set([...(config.permissions.allowlist.mcpServers || []), server.name]),
|
|
112
|
+
];
|
|
113
|
+
return config;
|
|
114
|
+
}
|
package/src/rag/cli.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG CLI commands: /rag query, /rag index, /rag status, /rag reset.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RAGEngine } from './rag-engine.js';
|
|
6
|
+
import { colors, statusIcons } from '../cli/snowflake-logo.js';
|
|
7
|
+
|
|
8
|
+
let ragEngine = null;
|
|
9
|
+
|
|
10
|
+
export function initRAG(config) {
|
|
11
|
+
ragEngine = new RAGEngine(config);
|
|
12
|
+
return ragEngine;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getRAGEngine() {
|
|
16
|
+
return ragEngine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getEngine(config) {
|
|
20
|
+
if (!ragEngine) {
|
|
21
|
+
ragEngine = new RAGEngine(config);
|
|
22
|
+
await ragEngine.init();
|
|
23
|
+
}
|
|
24
|
+
return ragEngine;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleRagCommand(query, config) {
|
|
28
|
+
const engine = await getEngine(config);
|
|
29
|
+
const results = await engine.search(query, { topK: 5, threshold: 0.5 });
|
|
30
|
+
|
|
31
|
+
if (results.count === 0) {
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
message: 'No relevant documents found. Try running /rag index first.',
|
|
35
|
+
results: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
query: results.query,
|
|
42
|
+
count: results.count,
|
|
43
|
+
results: results.results.map(doc => ({
|
|
44
|
+
id: doc.id,
|
|
45
|
+
source: doc.source,
|
|
46
|
+
score: doc.score.toFixed(3),
|
|
47
|
+
text: doc.text.slice(0, 500) + (doc.text.length > 500 ? '...' : ''),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function handleRagCommandFromRepl(repl, args = []) {
|
|
53
|
+
const config = repl.config ? await repl.config.load() : {};
|
|
54
|
+
config.projectPath = repl.projectPath;
|
|
55
|
+
await printRagCommand(args, config);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function handleRagCommandFromParser(parser, args = []) {
|
|
59
|
+
const config = parser.config ? await parser.config.load() : {};
|
|
60
|
+
config.projectPath = parser.projectPath || process.cwd();
|
|
61
|
+
await printRagCommand(args, config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function printRagCommand(args, config) {
|
|
65
|
+
const [action = 'query', ...rest] = args;
|
|
66
|
+
const normalizedAction = String(action || 'query').toLowerCase();
|
|
67
|
+
|
|
68
|
+
switch (normalizedAction) {
|
|
69
|
+
case 'query':
|
|
70
|
+
case 'search': {
|
|
71
|
+
const query = rest.join(' ');
|
|
72
|
+
if (!query) {
|
|
73
|
+
console.log(`${colors.yellow}Usage: /rag query <search-term>${colors.reset}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await handleRagCommand(query, config);
|
|
78
|
+
console.log(`${colors.cyan}RAG Search: "${query}"${colors.reset}`);
|
|
79
|
+
console.log(`${colors.dim}Found ${result.count || 0} results${colors.reset}`);
|
|
80
|
+
result.results?.forEach((r, i) => {
|
|
81
|
+
console.log(`\n${colors.green}[${i + 1}] ${r.source}${colors.reset} ${colors.dim}(score: ${r.score})${colors.reset}`);
|
|
82
|
+
console.log(`${r.text}\n`);
|
|
83
|
+
});
|
|
84
|
+
if (result.message) console.log(`${colors.dim}${result.message}${colors.reset}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'index': {
|
|
89
|
+
console.log(`${colors.cyan}RAG Indexing...${colors.reset}`);
|
|
90
|
+
const force = rest.includes('--force');
|
|
91
|
+
const result = await handleRagIndexCommand(config, { force });
|
|
92
|
+
const color = result.success ? colors.green : colors.yellow;
|
|
93
|
+
const icon = result.success ? `${statusIcons.success} ` : '';
|
|
94
|
+
console.log(`${color}${icon}${result.message}${colors.reset}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'status': {
|
|
99
|
+
const result = await handleRagStatusCommand(config);
|
|
100
|
+
console.log(`${colors.cyan}RAG Status:${colors.reset}`);
|
|
101
|
+
console.log(` Enabled: ${result.enabled}`);
|
|
102
|
+
console.log(` Documents: ${result.totalDocuments}`);
|
|
103
|
+
console.log(` Dimension: ${result.dimension}`);
|
|
104
|
+
console.log(` Provider: ${result.provider}`);
|
|
105
|
+
console.log(` Store: ${result.storeFile || 'default'}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'reset': {
|
|
110
|
+
console.log(`${colors.cyan}RAG Resetting...${colors.reset}`);
|
|
111
|
+
const result = await handleRagResetCommand(config);
|
|
112
|
+
console.log(`${colors.green}${statusIcons.success} ${result.message}${colors.reset}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'help':
|
|
117
|
+
default:
|
|
118
|
+
console.log(`${colors.cyan}RAG Commands:${colors.reset}`);
|
|
119
|
+
console.log(' /rag query <term> - Semantic search');
|
|
120
|
+
console.log(' /rag index [--force] - Index codebase');
|
|
121
|
+
console.log(' /rag status - Show RAG status');
|
|
122
|
+
console.log(' /rag reset - Clear vector store');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function handleRagIndexCommand(config, options = {}) {
|
|
127
|
+
const engine = await getEngine(config);
|
|
128
|
+
const stats = engine.getStatus();
|
|
129
|
+
|
|
130
|
+
if (stats.totalDocuments > 0 && !options.force) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
message: `Already indexed ${stats.totalDocuments} documents. Use /rag index --force to re-index.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.force) {
|
|
138
|
+
await engine.reset();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await engine.indexCodebase(options);
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
message: `Indexed ${result.totalDocuments} documents from ${result.totalFiles} files.`,
|
|
145
|
+
...result,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function handleRagResetCommand(config) {
|
|
150
|
+
const engine = await getEngine(config);
|
|
151
|
+
await engine.reset();
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message: 'Vector store cleared.',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function handleRagStatusCommand(config) {
|
|
159
|
+
const engine = await getEngine(config);
|
|
160
|
+
const status = engine.getStatus();
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
enabled: status.enabled,
|
|
165
|
+
totalDocuments: status.totalDocuments,
|
|
166
|
+
dimension: status.dimension,
|
|
167
|
+
provider: status.provider,
|
|
168
|
+
createdAt: status.createdAt,
|
|
169
|
+
updatedAt: status.updatedAt,
|
|
170
|
+
storeFile: status.storeFile,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function handleRagEnableCommand(enabled) {
|
|
175
|
+
if (ragEngine) {
|
|
176
|
+
ragEngine.setEnabled(enabled);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
message: `RAG ${enabled ? 'enabled' : 'disabled'}.`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default {
|
|
186
|
+
initRAG,
|
|
187
|
+
getRAGEngine,
|
|
188
|
+
handleRagCommand,
|
|
189
|
+
handleRagCommandFromRepl,
|
|
190
|
+
handleRagCommandFromParser,
|
|
191
|
+
handleRagIndexCommand,
|
|
192
|
+
handleRagResetCommand,
|
|
193
|
+
handleRagStatusCommand,
|
|
194
|
+
handleRagEnableCommand,
|
|
195
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ❄ EMBEDDING PROVIDERS ❄
|
|
3
|
+
* Abstraction layer for embedding models (Qwen, MiniMax, Ollama, OpenAI, custom)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getProviderPreset } from '../ai/provider-adapters.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_EMBED_MODEL = {
|
|
9
|
+
qwen: 'text-embedding-v3',
|
|
10
|
+
minimax: 'embedding-2',
|
|
11
|
+
ollama: 'nomic-embed-text',
|
|
12
|
+
openai: 'text-embedding-3-small',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get embedding provider config
|
|
17
|
+
* @param {string} providerName - Provider name (qwen, minimax, ollama, openai, custom)
|
|
18
|
+
* @param {object} config - Full Winter config
|
|
19
|
+
* @returns {object} Embedding config
|
|
20
|
+
*/
|
|
21
|
+
export function getEmbeddingConfig(providerName, config) {
|
|
22
|
+
const preset = getProviderPreset(providerName);
|
|
23
|
+
const configuredProvider = config?.providers?.[providerName] || config?.[providerName] || {};
|
|
24
|
+
|
|
25
|
+
if (!preset) {
|
|
26
|
+
// For custom providers, use the config directly
|
|
27
|
+
const customProvider = configuredProvider;
|
|
28
|
+
if (customProvider) {
|
|
29
|
+
return {
|
|
30
|
+
...customProvider,
|
|
31
|
+
embedModel: customProvider.embedModel || DEFAULT_EMBED_MODEL[providerName] || 'text-embedding-3-small',
|
|
32
|
+
embedProvider: providerName,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Provider '${providerName}' not found in config`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const model = configuredProvider.embedModel
|
|
39
|
+
|| DEFAULT_EMBED_MODEL[providerName]
|
|
40
|
+
|| preset.embedModel
|
|
41
|
+
|| preset.model;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...preset,
|
|
45
|
+
...configuredProvider,
|
|
46
|
+
embedModel: model,
|
|
47
|
+
embedProvider: providerName,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create embedding request body for a provider
|
|
53
|
+
* @param {string} providerName - Provider name
|
|
54
|
+
* @param {string} text - Text to embed
|
|
55
|
+
* @param {object} cfg - Provider config
|
|
56
|
+
* @returns {object} Request body
|
|
57
|
+
*/
|
|
58
|
+
export function createEmbeddingRequest(providerName, text, cfg) {
|
|
59
|
+
const normalizedText = text.replace(/\n/g, ' ').trim();
|
|
60
|
+
|
|
61
|
+
switch (providerName) {
|
|
62
|
+
case 'qwen':
|
|
63
|
+
return {
|
|
64
|
+
model: cfg.embedModel,
|
|
65
|
+
input: normalizedText,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
case 'minimax':
|
|
69
|
+
return {
|
|
70
|
+
model: cfg.embedModel,
|
|
71
|
+
text: normalizedText,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
case 'ollama':
|
|
75
|
+
return {
|
|
76
|
+
model: cfg.embedModel,
|
|
77
|
+
prompt: normalizedText,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
case 'openai':
|
|
81
|
+
case 'custom':
|
|
82
|
+
default:
|
|
83
|
+
return {
|
|
84
|
+
model: cfg.embedModel,
|
|
85
|
+
input: normalizedText,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse embedding response from provider
|
|
92
|
+
* @param {string} providerName - Provider name
|
|
93
|
+
* @param {object} data - Response data
|
|
94
|
+
* @returns {number[]} Embedding vector
|
|
95
|
+
*/
|
|
96
|
+
export function parseEmbeddingResponse(providerName, data) {
|
|
97
|
+
switch (providerName) {
|
|
98
|
+
case 'qwen':
|
|
99
|
+
return data.output?.embeddings?.[0]?.embedding || data.data?.[0]?.embedding;
|
|
100
|
+
|
|
101
|
+
case 'minimax':
|
|
102
|
+
return data.data?.embedding || data.data?.[0]?.embedding;
|
|
103
|
+
|
|
104
|
+
case 'ollama':
|
|
105
|
+
return data.embedding;
|
|
106
|
+
|
|
107
|
+
case 'openai':
|
|
108
|
+
case 'custom':
|
|
109
|
+
default:
|
|
110
|
+
return data.data?.[0]?.embedding;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get base URL for embedding provider
|
|
116
|
+
* @param {string} providerName - Provider name
|
|
117
|
+
* @param {object} cfg - Provider config
|
|
118
|
+
* @returns {string} Base URL
|
|
119
|
+
*/
|
|
120
|
+
export function getEmbeddingBaseURL(providerName, cfg) {
|
|
121
|
+
const baseURL = cfg.baseURL;
|
|
122
|
+
|
|
123
|
+
if (baseURL) {
|
|
124
|
+
const trimmed = String(baseURL).replace(/\/+$/, '');
|
|
125
|
+
if (/\/embeddings$/i.test(trimmed)) return trimmed;
|
|
126
|
+
if (providerName === 'ollama' && !/\/v1$/i.test(trimmed)) return `${trimmed}/api/embeddings`;
|
|
127
|
+
return `${trimmed}/embeddings`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const EMBED_ENDPOINTS = {
|
|
131
|
+
qwen: 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
|
|
132
|
+
minimax: 'https://api.minimax.chat/v1/text/embeddings',
|
|
133
|
+
ollama: 'http://localhost:11434/api/embeddings',
|
|
134
|
+
openai: 'https://api.openai.com/v1/embeddings',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return EMBED_ENDPOINTS[providerName] || EMBED_ENDPOINTS.openai;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get headers for embedding request
|
|
142
|
+
* @param {string} providerName - Provider name
|
|
143
|
+
* @param {object} cfg - Provider config
|
|
144
|
+
* @returns {object} Headers
|
|
145
|
+
*/
|
|
146
|
+
export function getEmbeddingHeaders(providerName, cfg) {
|
|
147
|
+
const headers = {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
switch (providerName) {
|
|
152
|
+
case 'qwen':
|
|
153
|
+
headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'minimax':
|
|
157
|
+
headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'ollama':
|
|
161
|
+
// No auth needed for local
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'openai':
|
|
165
|
+
headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'custom':
|
|
169
|
+
if (cfg.apiKey) headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
170
|
+
if (cfg.authToken) headers['Authorization'] = `Bearer ${cfg.authToken}`;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return headers;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default {
|
|
178
|
+
getEmbeddingConfig,
|
|
179
|
+
createEmbeddingRequest,
|
|
180
|
+
parseEmbeddingResponse,
|
|
181
|
+
getEmbeddingBaseURL,
|
|
182
|
+
getEmbeddingHeaders,
|
|
183
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ❄ RAG ENGINE ❄
|
|
3
|
+
* Core RAG engine - orchestrates retrieval + augmentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VectorStore } from './vector-store.js';
|
|
7
|
+
import { Retriever } from './retriever.js';
|
|
8
|
+
import { getEmbeddingConfig } from './embeddings.js';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import { promises as fs } from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* RAG Engine class
|
|
15
|
+
*/
|
|
16
|
+
export class RAGEngine {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.projectPath = path.resolve(config.projectPath || config.project?.current || process.cwd());
|
|
20
|
+
this.vectorStore = new VectorStore(config);
|
|
21
|
+
this.retriever = new Retriever(config, this.vectorStore);
|
|
22
|
+
this.enabled = config.rag?.enabled !== false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize RAG engine
|
|
27
|
+
*/
|
|
28
|
+
async init() {
|
|
29
|
+
await this.vectorStore.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if RAG is enabled
|
|
34
|
+
*/
|
|
35
|
+
isEnabled() {
|
|
36
|
+
return this.enabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Enable/disable RAG
|
|
41
|
+
*/
|
|
42
|
+
setEnabled(enabled) {
|
|
43
|
+
this.enabled = enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Index codebase
|
|
48
|
+
* @param {object} options - Indexing options
|
|
49
|
+
*/
|
|
50
|
+
async indexCodebase(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
patterns = ['src/**/*.js', 'src/**/*.ts', '*.md', 'docs/**/*.md'],
|
|
53
|
+
exclude = ['node_modules/**', 'dist/**', 'build/**', '.git/**', '.winter/**', 'resources/local/**', '**/*.test.js', '**/*.min.js'],
|
|
54
|
+
chunkSize = 1000,
|
|
55
|
+
chunkOverlap = 200,
|
|
56
|
+
} = options;
|
|
57
|
+
|
|
58
|
+
const files = await glob(patterns, {
|
|
59
|
+
cwd: this.projectPath,
|
|
60
|
+
ignore: exclude,
|
|
61
|
+
absolute: true,
|
|
62
|
+
nodir: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const docs = [];
|
|
66
|
+
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
70
|
+
|
|
71
|
+
// Chunk large files
|
|
72
|
+
if (content.length <= chunkSize) {
|
|
73
|
+
docs.push({
|
|
74
|
+
id: `file_${path.basename(file)}`,
|
|
75
|
+
text: content,
|
|
76
|
+
source: path.relative(this.projectPath, file).replace(/\\/g, '/'),
|
|
77
|
+
metadata: {
|
|
78
|
+
type: 'file',
|
|
79
|
+
name: path.basename(file),
|
|
80
|
+
ext: path.extname(file),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
// Split into chunks
|
|
85
|
+
const chunks = this.chunkText(content, chunkSize, chunkOverlap);
|
|
86
|
+
|
|
87
|
+
chunks.forEach((chunk, idx) => {
|
|
88
|
+
docs.push({
|
|
89
|
+
id: `chunk_${path.basename(file)}_${idx}`,
|
|
90
|
+
text: chunk,
|
|
91
|
+
source: path.relative(this.projectPath, file).replace(/\\/g, '/'),
|
|
92
|
+
metadata: {
|
|
93
|
+
type: 'chunk',
|
|
94
|
+
name: path.basename(file),
|
|
95
|
+
ext: path.extname(file),
|
|
96
|
+
chunkIndex: idx,
|
|
97
|
+
totalChunks: chunks.length,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn(`Failed to read ${file}: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ids = await this.vectorStore.addDocuments(docs);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
totalFiles: files.length,
|
|
111
|
+
totalDocuments: ids.length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Chunk text with overlap
|
|
117
|
+
* @param {string} text - Text to chunk
|
|
118
|
+
* @param {number} chunkSize - Chunk size in chars
|
|
119
|
+
* @param {number} overlap - Overlap between chunks
|
|
120
|
+
* @returns {string[]} Chunks
|
|
121
|
+
*/
|
|
122
|
+
chunkText(text, chunkSize, overlap) {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
const lines = text.split('\n');
|
|
125
|
+
let currentChunk = '';
|
|
126
|
+
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (currentChunk.length + line.length > chunkSize && currentChunk.length > 0) {
|
|
129
|
+
chunks.push(currentChunk.trim());
|
|
130
|
+
|
|
131
|
+
// Keep overlap
|
|
132
|
+
const overlapLines = currentChunk.split('\n').slice(-Math.floor(overlap / 50));
|
|
133
|
+
currentChunk = overlapLines.join('\n') + '\n' + line;
|
|
134
|
+
} else {
|
|
135
|
+
currentChunk += line + '\n';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentChunk.trim()) {
|
|
140
|
+
chunks.push(currentChunk.trim());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return chunks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Search
|
|
148
|
+
* @param {string} query - Query
|
|
149
|
+
* @param {object} options - Search options
|
|
150
|
+
*/
|
|
151
|
+
async search(query, options = {}) {
|
|
152
|
+
return this.retriever.retrieve(query, options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Augment prompt with context
|
|
157
|
+
* @param {string} prompt - Original prompt
|
|
158
|
+
* @param {object} options - Augmentation options
|
|
159
|
+
*/
|
|
160
|
+
async augmentPrompt(prompt, options = {}) {
|
|
161
|
+
if (!this.enabled) {
|
|
162
|
+
return prompt;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.retriever.augmentPrompt(prompt, options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get RAG status
|
|
170
|
+
*/
|
|
171
|
+
getStatus() {
|
|
172
|
+
const stats = this.vectorStore.getStats();
|
|
173
|
+
return {
|
|
174
|
+
enabled: this.enabled,
|
|
175
|
+
...stats,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reset vector store
|
|
181
|
+
*/
|
|
182
|
+
async reset() {
|
|
183
|
+
await this.vectorStore.clear();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default RAGEngine;
|