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 +1 -1
- package/src/ai/providers.js +26 -2
- package/src/ai/providers.test.js +35 -0
- package/src/cli/commands.js +37 -0
- package/src/cli/commands.test.js +67 -4
- package/src/cli/repl.js +10 -4
- package/src/cli/repl.test.js +31 -0
package/package.json
CHANGED
package/src/ai/providers.js
CHANGED
|
@@ -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
|
-
|
|
134
|
-
|
|
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
|
}
|
package/src/ai/providers.test.js
CHANGED
|
@@ -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
|
+
});
|
package/src/cli/commands.js
CHANGED
|
@@ -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}`);
|
package/src/cli/commands.test.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
1099
|
-
await this.
|
|
1100
|
-
|
|
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}`);
|
package/src/cli/repl.test.js
CHANGED
|
@@ -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
|
|