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.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ❄ RETRIEVER ❄
3
+ * Retrieval logic for RAG - handles similarity search and result formatting
4
+ */
5
+
6
+ import { VectorStore } from './vector-store.js';
7
+
8
+ /**
9
+ * Retriever class - handles document retrieval
10
+ */
11
+ export class Retriever {
12
+ constructor(config, vectorStore) {
13
+ this.config = config;
14
+ this.vectorStore = vectorStore;
15
+ }
16
+
17
+ /**
18
+ * Retrieve relevant documents for a query
19
+ * @param {string} query - User query
20
+ * @param {object} options - Retrieval options
21
+ * @returns {object} Retrieval results
22
+ */
23
+ async retrieve(query, options = {}) {
24
+ const {
25
+ topK = 5,
26
+ threshold = 0.5,
27
+ includeMetadata = true,
28
+ } = options;
29
+
30
+ if (!query || query.trim().length === 0) {
31
+ return {
32
+ query,
33
+ results: [],
34
+ count: 0,
35
+ };
36
+ }
37
+
38
+ const results = await this.vectorStore.search(query, topK, threshold);
39
+
40
+ const formattedResults = results.map(doc => ({
41
+ id: doc.id,
42
+ text: doc.text,
43
+ source: doc.source,
44
+ score: doc.score,
45
+ metadata: includeMetadata ? doc.metadata : undefined,
46
+ }));
47
+
48
+ return {
49
+ query,
50
+ results: formattedResults,
51
+ count: formattedResults.length,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Format retrieval results as context string
57
+ * @param {object} retrievalResult - Result from retrieve()
58
+ * @param {object} options - Formatting options
59
+ * @returns {string} Formatted context
60
+ */
61
+ formatContext(retrievalResult, options = {}) {
62
+ const { maxChars = 4000, showScores = true } = options;
63
+
64
+ if (retrievalResult.count === 0) {
65
+ return '';
66
+ }
67
+
68
+ let context = '## Relevant Context\n\n';
69
+
70
+ for (const doc of retrievalResult.results) {
71
+ const scoreStr = showScores ? ` [score: ${doc.score.toFixed(3)}]` : '';
72
+ const sourceStr = doc.source ? ` (${doc.source})` : '';
73
+
74
+ context += `---${sourceStr}${scoreStr}---\n`;
75
+ context += doc.text + '\n\n';
76
+
77
+ // Truncate if too long
78
+ if (context.length > maxChars) {
79
+ context = context.slice(0, maxChars) + '\n... (truncated)';
80
+ break;
81
+ }
82
+ }
83
+
84
+ return context;
85
+ }
86
+
87
+ /**
88
+ * Augment prompt with retrieved context
89
+ * @param {string} originalPrompt - Original user prompt
90
+ * @param {object} options - Retrieval and formatting options
91
+ * @returns {string} Augmented prompt
92
+ */
93
+ async augmentPrompt(originalPrompt, options = {}) {
94
+ const retrievalResult = await this.retrieve(originalPrompt, {
95
+ topK: options.topK || 5,
96
+ threshold: options.threshold || 0.5,
97
+ });
98
+
99
+ if (retrievalResult.count === 0) {
100
+ return originalPrompt;
101
+ }
102
+
103
+ const context = this.formatContext(retrievalResult, {
104
+ maxChars: options.maxChars || 4000,
105
+ showScores: options.showScores || false,
106
+ });
107
+
108
+ return `${context}\n\n## User Question\n${originalPrompt}`;
109
+ }
110
+ }
111
+
112
+ export default Retriever;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * ❄ VECTOR STORE ❄
3
+ * File-based vector storage with cosine similarity search
4
+ */
5
+
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import { getEmbeddingConfig, createEmbeddingRequest, parseEmbeddingResponse, getEmbeddingBaseURL, getEmbeddingHeaders } from './embeddings.js';
9
+
10
+ const DEFAULT_VECTOR_STORE_FILE = path.join('.winter', 'rag', 'vector-store.json');
11
+
12
+ /**
13
+ * Vector Store class
14
+ */
15
+ export class VectorStore {
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.projectPath = path.resolve(config.projectPath || config.project?.current || process.cwd());
19
+ this.storeFile = path.resolve(this.projectPath, config.rag?.storeFile || DEFAULT_VECTOR_STORE_FILE);
20
+ this.embeddings = [];
21
+ this.meta = {
22
+ provider: config.rag?.provider || 'qwen',
23
+ dimension: 0,
24
+ createdAt: null,
25
+ updatedAt: null,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Initialize vector store (load from disk)
31
+ */
32
+ async init() {
33
+ try {
34
+ const data = await fs.readFile(this.storeFile, 'utf-8');
35
+ const parsed = JSON.parse(data);
36
+ this.embeddings = parsed.embeddings || [];
37
+ this.meta = parsed.meta || this.meta;
38
+ } catch {
39
+ // No existing store, start fresh
40
+ this.embeddings = [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Save vector store to disk
46
+ */
47
+ async save() {
48
+ const dir = path.dirname(this.storeFile);
49
+ await fs.mkdir(dir, { recursive: true });
50
+
51
+ this.meta.updatedAt = new Date().toISOString();
52
+
53
+ await fs.writeFile(this.storeFile, JSON.stringify({
54
+ embeddings: this.embeddings,
55
+ meta: this.meta,
56
+ }, null, 2));
57
+ }
58
+
59
+ /**
60
+ * Embed text using configured provider
61
+ * @param {string} text - Text to embed
62
+ * @returns {number[]} Embedding vector
63
+ */
64
+ async embedText(text) {
65
+ const providerName = this.meta.provider;
66
+ const cfg = getEmbeddingConfig(providerName, this.config);
67
+
68
+ const baseURL = getEmbeddingBaseURL(providerName, cfg);
69
+ const headers = getEmbeddingHeaders(providerName, cfg);
70
+ const body = createEmbeddingRequest(providerName, text, cfg);
71
+
72
+ const response = await fetch(baseURL, {
73
+ method: 'POST',
74
+ headers,
75
+ body: JSON.stringify(body),
76
+ });
77
+
78
+ if (!response.ok) {
79
+ const errorText = await response.text();
80
+ throw new Error(`Embedding request failed: ${response.status} ${errorText}`);
81
+ }
82
+
83
+ const data = await response.json();
84
+ const embedding = parseEmbeddingResponse(providerName, data);
85
+
86
+ if (!embedding || embedding.length === 0) {
87
+ throw new Error('Failed to generate embedding');
88
+ }
89
+
90
+ return embedding;
91
+ }
92
+
93
+ /**
94
+ * Add document to vector store
95
+ * @param {object} doc - Document { id, text, source, metadata }
96
+ */
97
+ async addDocument(doc) {
98
+ const embedding = await this.embedText(doc.text);
99
+
100
+ const vectorDoc = {
101
+ id: doc.id || `doc_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
102
+ text: doc.text,
103
+ source: doc.source || 'unknown',
104
+ metadata: doc.metadata || {},
105
+ embedding,
106
+ createdAt: new Date().toISOString(),
107
+ };
108
+
109
+ this.embeddings.push(vectorDoc);
110
+
111
+ if (this.meta.dimension === 0) {
112
+ this.meta.dimension = embedding.length;
113
+ }
114
+
115
+ await this.save();
116
+
117
+ return vectorDoc.id;
118
+ }
119
+
120
+ /**
121
+ * Add multiple documents
122
+ * @param {object[]} docs - Array of documents
123
+ */
124
+ async addDocuments(docs) {
125
+ const ids = [];
126
+
127
+ for (const doc of docs) {
128
+ const id = await this.addDocument(doc);
129
+ ids.push(id);
130
+ }
131
+
132
+ return ids;
133
+ }
134
+
135
+ /**
136
+ * Cosine similarity between two vectors
137
+ * @param {number[]} a - Vector A
138
+ * @param {number[]} b - Vector B
139
+ * @returns {number} Similarity score (0-1)
140
+ */
141
+ cosineSimilarity(a, b) {
142
+ if (a.length !== b.length) return 0;
143
+
144
+ let dotProduct = 0;
145
+ let normA = 0;
146
+ let normB = 0;
147
+
148
+ for (let i = 0; i < a.length; i++) {
149
+ dotProduct += a[i] * b[i];
150
+ normA += a[i] * a[i];
151
+ normB += b[i] * b[i];
152
+ }
153
+
154
+ if (normA === 0 || normB === 0) return 0;
155
+
156
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
157
+ }
158
+
159
+ /**
160
+ * Search for similar documents
161
+ * @param {string} query - Query text
162
+ * @param {number} topK - Number of results (default: 5)
163
+ * @param {number} threshold - Similarity threshold (default: 0.5)
164
+ * @returns {object[]} Similar documents
165
+ */
166
+ async search(query, topK = 5, threshold = 0.5) {
167
+ if (this.embeddings.length === 0) {
168
+ return [];
169
+ }
170
+
171
+ const queryEmbedding = await this.embedText(query);
172
+
173
+ const results = this.embeddings
174
+ .map(doc => ({
175
+ ...doc,
176
+ score: this.cosineSimilarity(queryEmbedding, doc.embedding),
177
+ }))
178
+ .filter(doc => doc.score >= threshold)
179
+ .sort((a, b) => b.score - a.score)
180
+ .slice(0, topK);
181
+
182
+ return results;
183
+ }
184
+
185
+ /**
186
+ * Get all documents
187
+ * @returns {object[]} All documents
188
+ */
189
+ getAll() {
190
+ return this.embeddings.map(doc => ({
191
+ id: doc.id,
192
+ text: doc.text,
193
+ source: doc.source,
194
+ metadata: doc.metadata,
195
+ createdAt: doc.createdAt,
196
+ }));
197
+ }
198
+
199
+ /**
200
+ * Clear all documents
201
+ */
202
+ async clear() {
203
+ this.embeddings = [];
204
+ this.meta.dimension = 0;
205
+ this.meta.createdAt = null;
206
+ this.meta.updatedAt = null;
207
+ await this.save();
208
+ }
209
+
210
+ /**
211
+ * Get statistics
212
+ */
213
+ getStats() {
214
+ return {
215
+ totalDocuments: this.embeddings.length,
216
+ dimension: this.meta.dimension,
217
+ provider: this.meta.provider,
218
+ createdAt: this.meta.createdAt,
219
+ updatedAt: this.meta.updatedAt,
220
+ storeFile: this.storeFile,
221
+ };
222
+ }
223
+ }
224
+
225
+ export default VectorStore;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { promises as fs } from 'fs';
7
7
  import path from 'path';
8
- import { exec, execFile } from 'child_process';
8
+ import { exec, execFile, spawn } from 'child_process';
9
9
  import { promisify } from 'util';
10
10
  import { diffLines } from 'diff';
11
11
  import { withRetry } from './retry.js';
@@ -207,11 +207,11 @@ export class ToolExecutor {
207
207
  {
208
208
  type: 'function',
209
209
  name: 'MCP',
210
- description: 'Call a configured MCP server tool by name. Use for external integrations and IDE-like tools. Discover available MCP tools via the MCP tool with server name and tool=list. Also, tools from MCP servers are exposed with mcp__<server>__<tool> naming for direct IDE integration (e.g. mcp__vscode__open_file).',
210
+ description: 'Call a configured MCP server tool by name. Use for external integrations and IDE-like tools. Discover available MCP tools via the MCP tool with server name and tool=list. For live Chrome debugging, use server chrome-devtools with tools such as new_page, navigate_page, take_snapshot, take_screenshot, evaluate_script, list_console_messages, list_network_requests, and performance trace tools. Also, tools from MCP servers are exposed with mcp__<server>__<tool> naming for direct IDE integration (e.g. mcp__vscode__open_file).',
211
211
  parameters: {
212
212
  type: 'object',
213
213
  properties: {
214
- server: { type: 'string', description: 'Configured MCP server name (e.g. vscode)' },
214
+ server: { type: 'string', description: 'Configured MCP server name (e.g. vscode, chrome-devtools)' },
215
215
  tool: { type: 'string', description: 'MCP tool name, or set to "list" to discover all tools from a server' },
216
216
  arguments: { type: 'object', description: 'Tool arguments' },
217
217
  },
@@ -388,6 +388,18 @@ export class ToolExecutor {
388
388
  required: ['url']
389
389
  }
390
390
  },
391
+ {
392
+ type: 'function',
393
+ name: 'OpenBrowser',
394
+ description: 'Open Chrome or the default browser visibly for the user. Use this for requests like "mở chrome", "open Chrome", or "open this URL in browser". Do not use Bash/Start-Process for this.',
395
+ parameters: {
396
+ type: 'object',
397
+ properties: {
398
+ url: { type: 'string', description: 'URL to open. Defaults to about:blank.' },
399
+ browser: { type: 'string', description: 'chrome or default. Defaults to chrome.' },
400
+ }
401
+ }
402
+ },
391
403
  {
392
404
  type: 'function',
393
405
  name: 'WebFetch',
@@ -532,6 +544,8 @@ export class ToolExecutor {
532
544
  return await this.parallelExecute(input.tools ?? input.calls ?? [], { cwd });
533
545
  case 'BrowserDebug':
534
546
  return await this.browserDebug(input.url ?? input.uri, input.action);
547
+ case 'OpenBrowser':
548
+ return await this.openBrowser(input.url ?? input.uri ?? input.href, input.browser);
535
549
  case 'WebFetch':
536
550
  return await this.webFetch(input.url ?? input.uri ?? input.href, input.prompt ?? input.query ?? input.extract);
537
551
  case 'WebSearch':
@@ -572,7 +586,7 @@ export class ToolExecutor {
572
586
  return {
573
587
  success: false,
574
588
  error: `Unknown tool: ${toolName}`,
575
- availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'BrowserDebug', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'InsertText', 'StrReplaceAll'],
589
+ availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'OpenBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'InsertText', 'StrReplaceAll'],
576
590
  recovery: 'Call one of the available tools. For file writes use Write with { "file_path": "...", "content": "..." }. For shell commands use Bash with { "command": "..." }.',
577
591
  };
578
592
  }
@@ -725,6 +739,12 @@ export class ToolExecutor {
725
739
  return { success: true, coerced: true, args: next };
726
740
  }
727
741
 
742
+ if (toolName === 'OpenBrowser') {
743
+ const url = pick('url', 'uri', 'href') || 'about:blank';
744
+ const browser = pick('browser', 'app') || 'chrome';
745
+ return { success: true, coerced: true, args: { ...args, url, browser } };
746
+ }
747
+
728
748
  if (toolName === 'WebSearch') {
729
749
  const query = pick('query', 'q', 'search', 'search_query', 'searchQuery');
730
750
  if (!query) {
@@ -752,6 +772,14 @@ export class ToolExecutor {
752
772
  const baseCommand = this.getBaseCommand(text);
753
773
  if (!baseCommand) return { success: true };
754
774
 
775
+ if (/^(?:get-command|start-process|start|open|xdg-open)$/i.test(baseCommand) && /\b(chrome|browser|google chrome)\b/i.test(text)) {
776
+ return {
777
+ success: false,
778
+ error: `Use OpenBrowser instead of shell command for browser launch: ${baseCommand}`,
779
+ recovery: 'Call OpenBrowser {"browser":"chrome","url":"about:blank"} for "mở chrome", or OpenBrowser {"browser":"chrome","url":"https://example.com"} for a specific URL.',
780
+ };
781
+ }
782
+
755
783
  const cfg = await this.getRuntimeConfig();
756
784
  const permissionCommands = cfg.permissions?.allowlist?.commands || [];
757
785
  const sandbox = cfg.sandbox || {};
@@ -974,6 +1002,11 @@ export class ToolExecutor {
974
1002
  searchweb: 'WebSearch',
975
1003
  internetsearch: 'WebSearch',
976
1004
  googlesearch: 'WebSearch',
1005
+ openbrowser: 'OpenBrowser',
1006
+ open_browser: 'OpenBrowser',
1007
+ browseropen: 'OpenBrowser',
1008
+ openchrome: 'OpenBrowser',
1009
+ launchchrome: 'OpenBrowser',
977
1010
  browserdebug: 'BrowserDebug',
978
1011
  browser: 'BrowserDebug',
979
1012
  browserinspect: 'BrowserDebug',
@@ -2076,6 +2109,72 @@ export class ToolExecutor {
2076
2109
  }
2077
2110
  }
2078
2111
 
2112
+ buildBrowserLaunchCommand(url = 'about:blank', browser = 'chrome', platform = process.platform) {
2113
+ const targetUrl = String(url || 'about:blank');
2114
+ const targetBrowser = String(browser || 'chrome').toLowerCase();
2115
+
2116
+ if (platform === 'win32') {
2117
+ if (targetBrowser === 'default') {
2118
+ return { command: 'cmd', args: ['/c', 'start', '', targetUrl] };
2119
+ }
2120
+ return { command: 'cmd', args: ['/c', 'start', '', 'chrome', targetUrl] };
2121
+ }
2122
+
2123
+ if (platform === 'darwin') {
2124
+ if (targetBrowser === 'default') {
2125
+ return { command: 'open', args: [targetUrl] };
2126
+ }
2127
+ return { command: 'open', args: ['-a', 'Google Chrome', targetUrl] };
2128
+ }
2129
+
2130
+ if (targetBrowser === 'default') {
2131
+ return { command: 'xdg-open', args: [targetUrl] };
2132
+ }
2133
+ return { command: 'google-chrome', args: [targetUrl] };
2134
+ }
2135
+
2136
+ async openBrowser(url = 'about:blank', browser = 'chrome') {
2137
+ const targetUrl = String(url || 'about:blank');
2138
+ const targetBrowser = String(browser || 'chrome').toLowerCase();
2139
+ const launch = this.buildBrowserLaunchCommand(targetUrl, targetBrowser);
2140
+
2141
+ return await new Promise(resolve => {
2142
+ let child;
2143
+ try {
2144
+ child = spawn(launch.command, launch.args, {
2145
+ detached: true,
2146
+ stdio: 'ignore',
2147
+ windowsHide: false,
2148
+ });
2149
+ } catch (error) {
2150
+ resolve({
2151
+ success: false,
2152
+ error: error.message,
2153
+ recovery: 'Install Chrome or retry with OpenBrowser {"browser":"default","url":"about:blank"}.',
2154
+ });
2155
+ return;
2156
+ }
2157
+
2158
+ child.once('error', error => {
2159
+ resolve({
2160
+ success: false,
2161
+ error: error.message,
2162
+ recovery: 'Install Chrome or retry with OpenBrowser {"browser":"default","url":"about:blank"}.',
2163
+ });
2164
+ });
2165
+ child.once('spawn', () => {
2166
+ child.unref();
2167
+ resolve({
2168
+ success: true,
2169
+ browser: targetBrowser,
2170
+ url: targetUrl,
2171
+ command: launch.command,
2172
+ args: launch.args,
2173
+ });
2174
+ });
2175
+ });
2176
+ }
2177
+
2079
2178
  async htmlEffectivenessCompile(input, cwd) {
2080
2179
  const inputPath = this.resolveInputPath(input.input_path ?? input.inputPath ?? input.input, cwd);
2081
2180
  const outputPath = this.resolveInputPath(input.output_path ?? input.outputPath ?? input.output, cwd);