winter-super-cli 2026.5.25 → 2026.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.5.25",
3
+ "version": "2026.5.26",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -129,14 +129,38 @@ export class AIProviderManager {
129
129
  return null;
130
130
  }
131
131
 
132
+ normalizeProviderName(name) {
133
+ return String(name || '').trim().toLowerCase();
134
+ }
135
+
136
+ async reload() {
137
+ this.providers = {};
138
+ this.activeProvider = null;
139
+ this.initialized = false;
140
+ await this.init();
141
+ }
142
+
132
143
  setProvider(name) {
133
- if (this.providers[name]) {
134
- this.activeProvider = name;
144
+ const providerName = this.normalizeProviderName(name);
145
+ if (this.providers[providerName]) {
146
+ this.activeProvider = providerName;
135
147
  return true;
136
148
  }
137
149
  return false;
138
150
  }
139
151
 
152
+ async switchProvider(name) {
153
+ const providerName = this.normalizeProviderName(name);
154
+ await this.init();
155
+
156
+ if (this.setProvider(providerName)) {
157
+ return providerName;
158
+ }
159
+
160
+ await this.reload();
161
+ return this.setProvider(providerName) ? providerName : null;
162
+ }
163
+
140
164
  getActiveProvider() {
141
165
  return this.activeProvider;
142
166
  }
@@ -100,3 +100,38 @@ test('callAllProviders calls every ready configured provider', async () => {
100
100
  globalThis.fetch = originalFetch;
101
101
  }
102
102
  });
103
+
104
+ test('switchProvider reloads config before rejecting a provider', async () => {
105
+ let cfg = {
106
+ defaultProvider: 'ollama',
107
+ ollama: {
108
+ baseURL: 'http://ollama.test/v1',
109
+ model: 'llama3',
110
+ },
111
+ };
112
+
113
+ const ai = new AIProviderManager({
114
+ async load() {
115
+ return cfg;
116
+ },
117
+ });
118
+ ai.loadAuthToken = async () => null;
119
+
120
+ await ai.init();
121
+ assert.equal(ai.getActiveProvider(), 'ollama');
122
+
123
+ cfg = {
124
+ ...cfg,
125
+ custom: {
126
+ baseURL: 'http://custom.test/v1',
127
+ apiKey: 'not-required',
128
+ model: 'custom-model',
129
+ },
130
+ };
131
+
132
+ const switched = await ai.switchProvider(' CUSTOM ');
133
+
134
+ assert.equal(switched, 'custom');
135
+ assert.equal(ai.getActiveProvider(), 'custom');
136
+ assert.equal(ai.providers.custom.model, 'custom-model');
137
+ });
@@ -86,6 +86,8 @@ export class CommandParser {
86
86
  '/memories': () => this.showMemories(),
87
87
  '/plans': () => this.showPlans(),
88
88
  '/cache': () => this.handleCache(args),
89
+ '/provider': () => this.handleProvider(args),
90
+ '/providers': () => this.showProviders(),
89
91
  '/exit': () => process.exit(0),
90
92
  };
91
93
 
@@ -280,6 +282,41 @@ export class CommandParser {
280
282
  }
281
283
  }
282
284
 
285
+ async handleProvider(args) {
286
+ const providerName = args[0]?.trim().toLowerCase();
287
+
288
+ if (!providerName) {
289
+ await this.ai.init?.();
290
+ console.log(`${colors.cyan}Provider: ${this.ai.getActiveProvider()}${colors.reset}`);
291
+ return;
292
+ }
293
+
294
+ const switched = typeof this.ai.switchProvider === 'function'
295
+ ? await this.ai.switchProvider(providerName)
296
+ : (this.ai.setProvider(providerName) ? providerName : null);
297
+
298
+ if (switched) {
299
+ await this.config.setDefaultProvider(switched);
300
+ console.log(`${statusIcons.success} Provider: ${switched}`);
301
+ return;
302
+ }
303
+
304
+ const available = this.ai.listProviders?.().map(p => p.name).join(', ') || 'none';
305
+ console.log(`${colors.red}${statusIcons.error} Unknown provider: ${providerName}${colors.reset}`);
306
+ console.log(`${colors.dim}Available providers: ${available}${colors.reset}`);
307
+ }
308
+
309
+ async showProviders() {
310
+ await this.ai.init?.();
311
+ const providers = this.ai.listProviders?.() || [];
312
+ console.log(`\n${colors.cyan}Providers:${colors.reset}`);
313
+ providers.forEach(p => {
314
+ const status = p.ready ? statusIcons.online : statusIcons.offline;
315
+ const active = p.name === this.ai.getActiveProvider?.() ? ` ${colors.green}< active${colors.reset}` : '';
316
+ console.log(` ${status} ${p.name} (${p.model})${active}`);
317
+ });
318
+ }
319
+
283
320
  async showMemories() {
284
321
  const memories = this.session.getMemory();
285
322
  console.log(`\n${colors.cyan}Memories:${colors.reset}`);
@@ -90,10 +90,73 @@ test('skill and plugin commands default to list output', async () => {
90
90
  }
91
91
 
92
92
  assert(logs.some(line => line.includes('Available Skills')));
93
- assert(logs.some(line => line.includes('Installed Plugins')));
94
- });
95
-
96
- test('plugin manager loads local plugin files via file URLs', async () => {
93
+ assert(logs.some(line => line.includes('Installed Plugins')));
94
+ });
95
+
96
+ test('slash provider command switches and persists provider', async () => {
97
+ const session = {
98
+ getSessionId: () => '12345678-1234-1234-1234-123456789abc',
99
+ addToMemory: async () => {},
100
+ getMemory: () => [],
101
+ getPlans: () => [],
102
+ };
103
+ const saved = [];
104
+ const ai = {
105
+ active: 'ollama',
106
+ providers: {
107
+ custom: { model: 'custom-model', ready: true },
108
+ ollama: { model: 'llama3', ready: true },
109
+ },
110
+ async switchProvider(name) {
111
+ if (!this.providers[name]) return null;
112
+ this.active = name;
113
+ return name;
114
+ },
115
+ getActiveProvider() {
116
+ return this.active;
117
+ },
118
+ listProviders() {
119
+ return Object.entries(this.providers).map(([name, provider]) => ({ name, ...provider }));
120
+ },
121
+ };
122
+ const parser = new CommandParser({
123
+ session,
124
+ ai,
125
+ config: {
126
+ setDefaultProvider: async provider => saved.push(provider),
127
+ },
128
+ });
129
+
130
+ await parser.parse(['/provider', 'custom']);
131
+
132
+ assert.equal(ai.getActiveProvider(), 'custom');
133
+ assert.deepEqual(saved, ['custom']);
134
+ });
135
+
136
+ test('slash providers command lists available providers', async () => {
137
+ const parser = createParser();
138
+ parser.ai = {
139
+ init: async () => {},
140
+ getActiveProvider: () => 'custom',
141
+ listProviders: () => [
142
+ { name: 'custom', ready: true, model: 'custom-model' },
143
+ ],
144
+ };
145
+ const logs = [];
146
+ const originalLog = console.log;
147
+
148
+ console.log = (...args) => logs.push(args.join(' '));
149
+ try {
150
+ await parser.parse(['/providers']);
151
+ } finally {
152
+ console.log = originalLog;
153
+ }
154
+
155
+ assert(logs.some(line => line.includes('custom')));
156
+ assert(logs.some(line => line.includes('custom-model')));
157
+ });
158
+
159
+ test('plugin manager loads local plugin files via file URLs', async () => {
97
160
  const root = await mkdtemp(path.join(tmpdir(), 'winter-plugin-load-'));
98
161
  const pluginsDir = path.join(root, '.winter', 'plugins');
99
162
  await mkdir(pluginsDir, { recursive: true });
package/src/cli/repl.js CHANGED
@@ -1094,12 +1094,18 @@ export class WinterREPL {
1094
1094
  // Provider commands
1095
1095
  case '/provider':
1096
1096
  if (args[0]) {
1097
- const providerName = args[0];
1098
- if (this.ai.setProvider(providerName)) {
1099
- await this.config.setDefaultProvider(providerName);
1100
- console.log(`${colors.green}✓ Provider: ${providerName}${colors.reset}`);
1097
+ const providerName = args[0].trim().toLowerCase();
1098
+ const switched = typeof this.ai.switchProvider === 'function'
1099
+ ? await this.ai.switchProvider(providerName)
1100
+ : (this.ai.setProvider(providerName) ? providerName : null);
1101
+
1102
+ if (switched) {
1103
+ await this.config.setDefaultProvider(switched);
1104
+ console.log(`${colors.green}✓ Provider: ${switched}${colors.reset}`);
1101
1105
  } else {
1106
+ const available = this.ai.listProviders().map(p => p.name).join(', ') || 'none';
1102
1107
  console.log(`${colors.red}Unknown provider: ${providerName}${colors.reset}`);
1108
+ console.log(`${colors.dim}Available providers: ${available}${colors.reset}`);
1103
1109
  }
1104
1110
  } else {
1105
1111
  console.log(`${colors.cyan}Provider: ${this.ai.getActiveProvider()}${colors.reset}`);
@@ -169,6 +169,37 @@ test('readCachedModels returns bundled cache model ids', async () => {
169
169
  assert(!models.includes('priority'));
170
170
  });
171
171
 
172
+ test('provider slash command switches and persists configured provider', async () => {
173
+ const repl = new WinterREPL({ projectPath: process.cwd() });
174
+ const saved = [];
175
+ repl.config = {
176
+ setDefaultProvider: async provider => saved.push(provider),
177
+ };
178
+ repl.ai = {
179
+ active: 'ollama',
180
+ providers: {
181
+ custom: { model: 'custom-model', ready: true },
182
+ ollama: { model: 'llama3', ready: true },
183
+ },
184
+ async switchProvider(name) {
185
+ if (!this.providers[name]) return null;
186
+ this.active = name;
187
+ return name;
188
+ },
189
+ getActiveProvider() {
190
+ return this.active;
191
+ },
192
+ listProviders() {
193
+ return Object.entries(this.providers).map(([name, provider]) => ({ name, ...provider }));
194
+ },
195
+ };
196
+
197
+ await repl.handleSlashCommand('/provider custom');
198
+
199
+ assert.equal(repl.ai.getActiveProvider(), 'custom');
200
+ assert.deepEqual(saved, ['custom']);
201
+ });
202
+
172
203
  test('shouldUseTools keeps agent mode enabled by default', () => {
173
204
  const repl = new WinterREPL({ projectPath: process.cwd() });
174
205