osai-agent 4.0.0
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/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/sound.js +33 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { printError, printSuccess, printInfo, printNotLoggedIn } from '../ui/terminal.js';
|
|
6
|
+
import { toHttpUrl } from '../services/server-url.js';
|
|
7
|
+
import { encrypt, decrypt, deriveKey } from '../services/crypto.js';
|
|
8
|
+
import pkg from 'node-machine-id';
|
|
9
|
+
const { machineIdSync } = pkg;
|
|
10
|
+
|
|
11
|
+
const getConfig = () => new Conf({ projectName: 'osai-agent' });
|
|
12
|
+
const getServer = () => toHttpUrl(getConfig().get('server'));
|
|
13
|
+
const getToken = () => getConfig().get('token');
|
|
14
|
+
const getHeaders = () => ({ 'Authorization': `Bearer ${getToken()}`, 'Content-Type': 'application/json' });
|
|
15
|
+
|
|
16
|
+
const isLocalMode = () => {
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
return argv.includes('--local');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const maskKey = (key) => {
|
|
22
|
+
if (!key) return null;
|
|
23
|
+
if (key.length <= 4) return key;
|
|
24
|
+
return key.slice(0, 3) + '...' + key.slice(-4);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function migrateLocalProvider() {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
const old = config.get('localProvider');
|
|
30
|
+
if (!old || config.get('localProviders')) return;
|
|
31
|
+
const parsed = typeof old === 'string' ? JSON.parse(old) : old;
|
|
32
|
+
const { type, apiKeyEnc, model, baseUrl } = parsed;
|
|
33
|
+
if (type) {
|
|
34
|
+
config.set('localProviders', { [type]: { apiKeyEnc: apiKeyEnc || null, model: model || null, baseUrl: baseUrl || null } });
|
|
35
|
+
config.set('currentLocalProvider', type);
|
|
36
|
+
}
|
|
37
|
+
config.delete('localProvider');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function saveLocalProvider(type, apiKey, model, baseUrl) {
|
|
41
|
+
migrateLocalProvider();
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const key = deriveKey(machineIdSync());
|
|
44
|
+
const providers = config.get('localProviders', {});
|
|
45
|
+
const existing = providers[type] || {};
|
|
46
|
+
providers[type] = {
|
|
47
|
+
apiKeyEnc: apiKey ? encrypt(apiKey, key) : (existing.apiKeyEnc || null),
|
|
48
|
+
model: model || existing.model || null,
|
|
49
|
+
baseUrl: baseUrl || existing.baseUrl || null,
|
|
50
|
+
};
|
|
51
|
+
config.set('localProviders', providers);
|
|
52
|
+
config.set('currentLocalProvider', type);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getLocalProvider() {
|
|
56
|
+
migrateLocalProvider();
|
|
57
|
+
const config = getConfig();
|
|
58
|
+
const current = config.get('currentLocalProvider');
|
|
59
|
+
if (!current) return null;
|
|
60
|
+
const providers = config.get('localProviders', {});
|
|
61
|
+
const entry = providers[current];
|
|
62
|
+
if (!entry) return null;
|
|
63
|
+
const key = deriveKey(machineIdSync());
|
|
64
|
+
const apiKey = entry.apiKeyEnc ? decrypt(entry.apiKeyEnc, key) : null;
|
|
65
|
+
return { type: current, apiKey, model: entry.model || null, baseUrl: entry.baseUrl || null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getAllLocalProviders() {
|
|
69
|
+
migrateLocalProvider();
|
|
70
|
+
const config = getConfig();
|
|
71
|
+
const current = config.get('currentLocalProvider');
|
|
72
|
+
const providers = config.get('localProviders', {});
|
|
73
|
+
return Object.entries(providers).map(([type, data]) => {
|
|
74
|
+
const key = deriveKey(machineIdSync());
|
|
75
|
+
const apiKey = data.apiKeyEnc ? decrypt(data.apiKeyEnc, key) : null;
|
|
76
|
+
return { type, apiKey, model: data.model || null, baseUrl: data.baseUrl || null, active: type === current };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function removeLocalProvider(type) {
|
|
81
|
+
migrateLocalProvider();
|
|
82
|
+
const config = getConfig();
|
|
83
|
+
if (type) {
|
|
84
|
+
const providers = config.get('localProviders', {});
|
|
85
|
+
delete providers[type];
|
|
86
|
+
config.set('localProviders', providers);
|
|
87
|
+
if (config.get('currentLocalProvider') === type) {
|
|
88
|
+
const remaining = Object.keys(providers);
|
|
89
|
+
config.set('currentLocalProvider', remaining.length > 0 ? remaining[0] : null);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
config.delete('localProviders');
|
|
93
|
+
config.delete('currentLocalProvider');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const listProviders = async () => {
|
|
98
|
+
const token = getToken();
|
|
99
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
100
|
+
const spinner = ora('Fetching provider catalog...').start();
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`${getServer()}/api/provider/catalog`, { headers: getHeaders() });
|
|
103
|
+
if (!response.ok) { spinner.fail('Failed to fetch catalog'); const e = await response.json(); printError(e.error || 'Unknown error'); return; }
|
|
104
|
+
const catalog = await response.json();
|
|
105
|
+
spinner.stop();
|
|
106
|
+
|
|
107
|
+
const active = catalog.find(p => p.active);
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.hex('#7aa2f7').bold(' Provider Catalog'));
|
|
110
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(48)));
|
|
111
|
+
console.log(chalk.hex('#565f89')(` ${'Provider'.padEnd(20)} SDK Type Free Models`));
|
|
112
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(48)));
|
|
113
|
+
|
|
114
|
+
for (const p of catalog) {
|
|
115
|
+
const activeMark = p.active ? chalk.green(' ◀') : '';
|
|
116
|
+
const freeMark = p.free_tier ? chalk.green('Yes') : chalk.red('No ');
|
|
117
|
+
const name = p.id === active?.id ? chalk.white.bold(p.name) : chalk.white(p.name);
|
|
118
|
+
const sdk = p.sdk_type === 'anthropic' ? 'native ' : 'OpenAI ';
|
|
119
|
+
console.log(` ${(name + activeMark).padEnd(22)} ${sdk} ${freeMark} ${chalk.gray(p.models_count + ' models')}`);
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(48)));
|
|
122
|
+
console.log(chalk.gray(` ${active?.name || 'OS AI Agent'} is currently active\n`));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
spinner.fail('Network error');
|
|
125
|
+
printError(err.message);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const manualModelSelection = async (providerType) => {
|
|
130
|
+
printInfo(`Fetching available models for ${providerType}...`);
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(`${getServer()}/api/provider/models/${providerType}`, { headers: getHeaders() });
|
|
133
|
+
if (!response.ok) return null;
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
if (!data.models || data.models.length === 0) return null;
|
|
136
|
+
|
|
137
|
+
const modelNames = data.models.map(m => (typeof m === 'string' ? m : (m.id || m.name))).filter(Boolean);
|
|
138
|
+
if (modelNames.length === 0) return null;
|
|
139
|
+
|
|
140
|
+
console.log(chalk.hex('#7aa2f7').bold(`\n Select a model for ${providerType}:`));
|
|
141
|
+
console.log();
|
|
142
|
+
|
|
143
|
+
const choices = modelNames.slice(0, 50).map((name, i) => ({
|
|
144
|
+
name: `${String(i + 1).padStart(2)}. ${name}`,
|
|
145
|
+
value: name
|
|
146
|
+
}));
|
|
147
|
+
choices.push({ name: ` 0. Enter model ID manually`, value: '__manual__' });
|
|
148
|
+
|
|
149
|
+
for (const c of choices) {
|
|
150
|
+
console.log(chalk.hex('#565f89')(` ${c.name}`));
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
|
|
154
|
+
const { selection } = await inquirer.prompt({
|
|
155
|
+
type: 'input',
|
|
156
|
+
name: 'selection',
|
|
157
|
+
message: 'Enter number (0 for manual)',
|
|
158
|
+
validate: (v) => {
|
|
159
|
+
const num = parseInt(v);
|
|
160
|
+
if (isNaN(num) || num < 0 || num > choices.length - 1) return `Enter a number between 0 and ${choices.length - 1}`;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const num = parseInt(selection);
|
|
166
|
+
if (num === 0) {
|
|
167
|
+
const { manual } = await inquirer.prompt({
|
|
168
|
+
type: 'input',
|
|
169
|
+
name: 'manual',
|
|
170
|
+
message: 'Enter model ID',
|
|
171
|
+
validate: (v) => v.trim().length > 0 || 'Model ID required'
|
|
172
|
+
});
|
|
173
|
+
return manual.trim();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return choices[num].value;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const showOllamaTip = () => {
|
|
183
|
+
console.log(chalk.hex('#565f89')(' Tip: Make sure Ollama is running: ollama serve'));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const setProvider = async (provider, options) => {
|
|
187
|
+
if (!provider) { printError('Usage: osai-agent provider set <provider> [--key <key>] [--model <model>] [--url <url>]'); return; }
|
|
188
|
+
|
|
189
|
+
const keylessProviders = ['ollama', 'lmstudio', 'vllm'];
|
|
190
|
+
|
|
191
|
+
if (isLocalMode()) {
|
|
192
|
+
const key = options.key;
|
|
193
|
+
const model = options.model;
|
|
194
|
+
const url = options.url;
|
|
195
|
+
saveLocalProvider(provider, key, model, url);
|
|
196
|
+
const keyDisplay = key ? ` | Key: ${maskKey(key)}` : '';
|
|
197
|
+
const modelDisplay = model ? ` | Model: ${model}` : '';
|
|
198
|
+
const urlDisplay = url ? ` | URL: ${url}` : '';
|
|
199
|
+
console.log();
|
|
200
|
+
printSuccess(`Local provider set to ${provider}${modelDisplay}${keyDisplay}${urlDisplay}`);
|
|
201
|
+
if (keylessProviders.includes(provider)) {
|
|
202
|
+
printInfo(`Tip: Make sure ${provider} is running locally`);
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const token = getToken();
|
|
209
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
210
|
+
|
|
211
|
+
if (keylessProviders.includes(provider) && !options.key && !options.model) {
|
|
212
|
+
printInfo(`No API key required for ${provider} (local)`);
|
|
213
|
+
const spinner = ora(`Fetching installed models from local server...`).start();
|
|
214
|
+
try {
|
|
215
|
+
const catResp = await fetch(`${getServer()}/api/provider/catalog`, { headers: getHeaders() });
|
|
216
|
+
const catalog = catResp.ok ? await catResp.json() : [];
|
|
217
|
+
const entry = catalog.find(p => p.id === provider);
|
|
218
|
+
const defaultUrl = entry?.base_url || 'http://localhost:11434/v1';
|
|
219
|
+
|
|
220
|
+
if (!options.url) {
|
|
221
|
+
options.url = defaultUrl;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Save provider first with no key
|
|
225
|
+
const saveResp = await fetch(`${getServer()}/api/provider`, {
|
|
226
|
+
method: 'PUT',
|
|
227
|
+
headers: getHeaders(),
|
|
228
|
+
body: JSON.stringify({ type: provider, base_url: options.url })
|
|
229
|
+
});
|
|
230
|
+
if (!saveResp.ok) { spinner.fail('Failed to set provider'); const e = await saveResp.json(); printError(e.error || 'Unknown error'); return; }
|
|
231
|
+
|
|
232
|
+
spinner.succeed(`Provider set to ${provider}`);
|
|
233
|
+
|
|
234
|
+
const model = await manualModelSelection(provider);
|
|
235
|
+
if (model) {
|
|
236
|
+
const spinner2 = ora('Saving model selection...').start();
|
|
237
|
+
await fetch(`${getServer()}/api/provider`, {
|
|
238
|
+
method: 'PUT',
|
|
239
|
+
headers: getHeaders(),
|
|
240
|
+
body: JSON.stringify({ type: provider, model, base_url: options.url })
|
|
241
|
+
});
|
|
242
|
+
spinner2.succeed('Model updated');
|
|
243
|
+
printSuccess(`Provider set to ${provider} | Model: ${model}`);
|
|
244
|
+
} else {
|
|
245
|
+
if (provider === 'ollama') showOllamaTip();
|
|
246
|
+
printSuccess(`Provider set to ${provider}`);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
spinner.fail('Could not connect to local provider');
|
|
250
|
+
if (provider === 'ollama') {
|
|
251
|
+
printError(`Could not connect to Ollama at ${options.url || 'http://localhost:11434'}`);
|
|
252
|
+
showOllamaTip();
|
|
253
|
+
printInfo('Showing empty model list — run \'ollama pull llama3.3\' to install models');
|
|
254
|
+
} else {
|
|
255
|
+
printError(err.message);
|
|
256
|
+
}
|
|
257
|
+
// Still save the provider selection even if models can't be fetched
|
|
258
|
+
const saveResp = await fetch(`${getServer()}/api/provider`, {
|
|
259
|
+
method: 'PUT',
|
|
260
|
+
headers: getHeaders(),
|
|
261
|
+
body: JSON.stringify({ type: provider, base_url: options.url })
|
|
262
|
+
});
|
|
263
|
+
if (saveResp.ok) printSuccess(`Provider set to ${provider}`);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Providers that need an API key
|
|
269
|
+
const spinner = ora(`Setting provider to ${provider}...`).start();
|
|
270
|
+
try {
|
|
271
|
+
const body = { type: provider };
|
|
272
|
+
if (options.key) body.api_key = options.key;
|
|
273
|
+
if (options.model) body.model = options.model;
|
|
274
|
+
if (options.url) body.base_url = options.url;
|
|
275
|
+
|
|
276
|
+
const response = await fetch(`${getServer()}/api/provider`, {
|
|
277
|
+
method: 'PUT',
|
|
278
|
+
headers: getHeaders(),
|
|
279
|
+
body: JSON.stringify(body)
|
|
280
|
+
});
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
if (!response.ok) { spinner.fail('Failed to set provider'); printError(data.error || 'Unknown error'); return; }
|
|
283
|
+
spinner.succeed('Provider updated');
|
|
284
|
+
|
|
285
|
+
const keyDisplay = data.key ? ` | Key: ${data.key}` : '';
|
|
286
|
+
printSuccess(`API key saved for ${data.type}${keyDisplay}`);
|
|
287
|
+
|
|
288
|
+
// Auto-discover models if no model was specified
|
|
289
|
+
if (!options.model) {
|
|
290
|
+
const model = await manualModelSelection(provider);
|
|
291
|
+
if (model) {
|
|
292
|
+
const spinner2 = ora('Saving model selection...').start();
|
|
293
|
+
const body2 = { type: provider, model };
|
|
294
|
+
if (options.key) body2.api_key = options.key;
|
|
295
|
+
if (options.url) body2.base_url = options.url;
|
|
296
|
+
await fetch(`${getServer()}/api/provider`, {
|
|
297
|
+
method: 'PUT',
|
|
298
|
+
headers: getHeaders(),
|
|
299
|
+
body: JSON.stringify(body2)
|
|
300
|
+
});
|
|
301
|
+
spinner2.succeed('Model updated');
|
|
302
|
+
printSuccess(`Provider set to ${data.type} | Model: ${model}`);
|
|
303
|
+
} else {
|
|
304
|
+
printSuccess(`Provider set to ${data.type}`);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
const modelDisplay = data.model ? ` | Model: ${data.model}` : '';
|
|
308
|
+
printSuccess(`Provider set to ${data.type}${modelDisplay}${keyDisplay}`);
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
spinner.fail('Network error');
|
|
312
|
+
printError(err.message);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const showProviderModels = async (provider) => {
|
|
317
|
+
const token = getToken();
|
|
318
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
319
|
+
if (!provider) { printError('Usage: osai-agent provider models <provider>'); return; }
|
|
320
|
+
|
|
321
|
+
const spinner = ora(`Fetching models for ${provider}...`).start();
|
|
322
|
+
try {
|
|
323
|
+
const response = await fetch(`${getServer()}/api/provider/models/${provider}`, { headers: getHeaders() });
|
|
324
|
+
if (!response.ok) { spinner.fail('Failed to fetch models'); const e = await response.json(); printError(e.error || 'Unknown error'); return; }
|
|
325
|
+
const data = await response.json();
|
|
326
|
+
spinner.stop();
|
|
327
|
+
|
|
328
|
+
const sourceColors = {
|
|
329
|
+
'live (local)': chalk.green,
|
|
330
|
+
'live (public)': chalk.cyan,
|
|
331
|
+
'live (authenticated)': chalk.blue,
|
|
332
|
+
cached: chalk.magenta,
|
|
333
|
+
static: chalk.yellow
|
|
334
|
+
};
|
|
335
|
+
const colorFn = sourceColors[data.source] || chalk.gray;
|
|
336
|
+
const sourceLabel = colorFn(data.source);
|
|
337
|
+
|
|
338
|
+
console.log();
|
|
339
|
+
console.log(chalk.hex('#7aa2f7').bold(` ${data.provider} — Available Models`));
|
|
340
|
+
console.log(chalk.hex('#565f89')(` Source: ${sourceLabel}`));
|
|
341
|
+
if (data.warning) {
|
|
342
|
+
console.log(chalk.hex('#ff9e64')(` ⚠ ${data.warning}`));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (provider === 'ollama') {
|
|
346
|
+
const hasSize = data.models.some(m => typeof m === 'object' && m.size);
|
|
347
|
+
if (hasSize) {
|
|
348
|
+
const totalGb = data.models.reduce((sum, m) => sum + (m.size ? m.size / 1e9 : 0), 0);
|
|
349
|
+
console.log(chalk.hex('#565f89')(` Installed models (${data.models.length}):`));
|
|
350
|
+
console.log();
|
|
351
|
+
for (const m of data.models) {
|
|
352
|
+
const sizeGb = m.size ? (m.size / 1e9).toFixed(1) : '?';
|
|
353
|
+
console.log(` ${chalk.white(m.id.padEnd(30))} ${chalk.gray(`(${sizeGb} GB)`)}`);
|
|
354
|
+
}
|
|
355
|
+
console.log(chalk.gray(` Total: ${totalGb.toFixed(1)} GB used`));
|
|
356
|
+
} else {
|
|
357
|
+
data.models.forEach((m, i) => {
|
|
358
|
+
const name = typeof m === 'string' ? m : (m.id || m.name || m);
|
|
359
|
+
console.log(chalk.hex('#565f89')(` ${String(i + 1).padStart(2)}.`) + ` ${chalk.white(name)}`);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk.gray(` Tip: Install more: ollama pull <model>`));
|
|
363
|
+
} else if (provider === 'openrouter') {
|
|
364
|
+
const totalModels = data.models?.length || 0;
|
|
365
|
+
console.log(chalk.hex('#565f89')(` ${totalModels} models available`));
|
|
366
|
+
console.log(chalk.hex('#565f89')(' Tip: Use vendor/model:free for free models (e.g. meta-llama/llama-4-maverick:free)'));
|
|
367
|
+
console.log(chalk.hex('#565f89')(' Browse all: https://openrouter.ai/models'));
|
|
368
|
+
console.log();
|
|
369
|
+
const displayLimit = 50;
|
|
370
|
+
const showAll = process.argv.includes('--all');
|
|
371
|
+
const toShow = showAll ? data.models : data.models.slice(0, displayLimit);
|
|
372
|
+
toShow.forEach((m, i) => {
|
|
373
|
+
console.log(chalk.hex('#565f89')(` ${String(i + 1).padStart(2)}.`) + ` ${chalk.white(m)}`);
|
|
374
|
+
});
|
|
375
|
+
if (!showAll && data.models.length > displayLimit) {
|
|
376
|
+
console.log(chalk.gray(` ... and ${data.models.length - displayLimit} more (use --all to see all)`));
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
console.log();
|
|
380
|
+
data.models.forEach((m, i) => {
|
|
381
|
+
const name = typeof m === 'string' ? m : (m.id || m.name || m);
|
|
382
|
+
console.log(chalk.hex('#565f89')(` ${String(i + 1).padStart(2)}.`) + ` ${chalk.white(name)}`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
console.log();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
spinner.fail('Network error');
|
|
388
|
+
printError(err.message);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export const showProvider = async () => {
|
|
393
|
+
if (isLocalMode()) {
|
|
394
|
+
const all = getAllLocalProviders();
|
|
395
|
+
console.log();
|
|
396
|
+
console.log(chalk.hex('#7aa2f7').bold(' Local Provider Configuration'));
|
|
397
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(40)));
|
|
398
|
+
if (all.length === 0) {
|
|
399
|
+
console.log(` ${chalk.gray('No providers configured.')}`);
|
|
400
|
+
console.log(` ${chalk.gray(' Use: osai-agent provider set <type> --key <key> --local')}`);
|
|
401
|
+
} else {
|
|
402
|
+
for (const p of all) {
|
|
403
|
+
const activeMark = p.active ? chalk.green(' ◀ active') : '';
|
|
404
|
+
console.log(` ${chalk.white(p.type.padEnd(16))}${activeMark}`);
|
|
405
|
+
console.log(` ${chalk.gray(` Model : ${p.model || 'auto'}`)}`);
|
|
406
|
+
console.log(` ${chalk.gray(` Key : ${p.apiKey ? '(encrypted, stored locally)' : 'N/A'}`)}`);
|
|
407
|
+
console.log(` ${chalk.gray(` Base : ${p.baseUrl || 'default'}`)}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
console.log();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const token = getToken();
|
|
415
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
416
|
+
|
|
417
|
+
const spinner = ora('Fetching provider config...').start();
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch(`${getServer()}/api/provider`, { headers: getHeaders() });
|
|
420
|
+
if (!response.ok) { spinner.fail('Failed to fetch provider'); const e = await response.json(); printError(e.error || 'Unknown error'); return; }
|
|
421
|
+
const data = await response.json();
|
|
422
|
+
spinner.stop();
|
|
423
|
+
|
|
424
|
+
console.log();
|
|
425
|
+
console.log(chalk.hex('#7aa2f7').bold(' Provider Configuration'));
|
|
426
|
+
console.log(chalk.hex('#565f89')(' ─'.repeat(32)));
|
|
427
|
+
|
|
428
|
+
if (data.type === 'osai') {
|
|
429
|
+
console.log(` ${chalk.white('Current provider :')} ${chalk.green('OS AI Agent')}`);
|
|
430
|
+
console.log(` ${chalk.white('Model :')} ${chalk.gray('auto')}`);
|
|
431
|
+
console.log(` ${chalk.white('API Key :')} ${chalk.gray('N/A (OS AI managed)')}`);
|
|
432
|
+
console.log(` ${chalk.white('Free tier :')} ${chalk.green('Yes')}`);
|
|
433
|
+
console.log(` ${chalk.white('Quota :')} ${chalk.yellow('50 commands/day (free plan)')}`);
|
|
434
|
+
} else {
|
|
435
|
+
const catalogResponse = await fetch(`${getServer()}/api/provider/catalog`, { headers: getHeaders() });
|
|
436
|
+
const catalog = catalogResponse.ok ? await catalogResponse.json() : [];
|
|
437
|
+
const entry = catalog.find(p => p.id === data.type);
|
|
438
|
+
|
|
439
|
+
console.log(` ${chalk.white('Current provider :')} ${chalk.green(data.type)}`);
|
|
440
|
+
console.log(` ${chalk.white('Model :')} ${chalk.white(data.model || 'N/A')}`);
|
|
441
|
+
console.log(` ${chalk.white('API Key :')} ${chalk.gray(maskKey(data.key) || 'N/A')}`);
|
|
442
|
+
console.log(` ${chalk.white('Base URL :')} ${chalk.gray(data.base_url || 'N/A')}`);
|
|
443
|
+
console.log(` ${chalk.white('Custom name :')} ${chalk.gray(data.custom_name || 'N/A')}`);
|
|
444
|
+
console.log(` ${chalk.white('Free tier :')} ${entry?.free_tier ? chalk.green('Yes') : chalk.red('No')}`);
|
|
445
|
+
console.log(` ${chalk.white('Quota :')} ${chalk.green('N/A (BYOK)')}`);
|
|
446
|
+
}
|
|
447
|
+
console.log();
|
|
448
|
+
} catch (err) {
|
|
449
|
+
spinner.fail('Network error');
|
|
450
|
+
printError(err.message);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
export const resetProvider = async () => {
|
|
455
|
+
if (isLocalMode()) {
|
|
456
|
+
removeLocalProvider();
|
|
457
|
+
printSuccess('All local providers reset');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const token = getToken();
|
|
462
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
463
|
+
|
|
464
|
+
const { confirm } = await inquirer.prompt({
|
|
465
|
+
type: 'confirm',
|
|
466
|
+
name: 'confirm',
|
|
467
|
+
message: 'Reset to OS AI Agent provider?',
|
|
468
|
+
default: false
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (!confirm) { printInfo('Reset cancelled'); return; }
|
|
472
|
+
|
|
473
|
+
const spinner = ora('Resetting provider...').start();
|
|
474
|
+
try {
|
|
475
|
+
const response = await fetch(`${getServer()}/api/provider`, {
|
|
476
|
+
method: 'DELETE',
|
|
477
|
+
headers: getHeaders()
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) { spinner.fail('Failed to reset'); const e = await response.json(); printError(e.error || 'Unknown error'); return; }
|
|
480
|
+
spinner.succeed('Provider reset');
|
|
481
|
+
printSuccess('Provider reset to OS AI Agent');
|
|
482
|
+
} catch (err) {
|
|
483
|
+
spinner.fail('Network error');
|
|
484
|
+
printError(err.message);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
export const addCustomProvider = async (options) => {
|
|
489
|
+
const token = getToken();
|
|
490
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
491
|
+
|
|
492
|
+
const name = options.name;
|
|
493
|
+
const url = options.url;
|
|
494
|
+
const key = options.key;
|
|
495
|
+
const model = options.model;
|
|
496
|
+
|
|
497
|
+
if (!name || !url || !key) {
|
|
498
|
+
printError('Usage: osai-agent provider add custom --name <name> --url <base-url> --key <api-key> [--model <model>]');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const spinner = ora('Adding custom provider...').start();
|
|
503
|
+
try {
|
|
504
|
+
const response = await fetch(`${getServer()}/api/provider`, {
|
|
505
|
+
method: 'PUT',
|
|
506
|
+
headers: getHeaders(),
|
|
507
|
+
body: JSON.stringify({ type: 'custom', api_key: key, base_url: url, model: model || null, custom_name: name })
|
|
508
|
+
});
|
|
509
|
+
const data = await response.json();
|
|
510
|
+
if (!response.ok) { spinner.fail('Failed to add custom provider'); printError(data.error || 'Unknown error'); return; }
|
|
511
|
+
spinner.succeed('Custom provider added');
|
|
512
|
+
printSuccess(`Custom provider "${name}" configured | Key: ${maskKey(key)}`);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
spinner.fail('Network error');
|
|
515
|
+
printError(err.message);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// ── listConfiguredProviders — Show all saved providers ──────────────
|
|
520
|
+
export const listConfiguredProviders = async () => {
|
|
521
|
+
const token = getToken();
|
|
522
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
523
|
+
|
|
524
|
+
const spinner = ora('Fetching configured providers...').start();
|
|
525
|
+
try {
|
|
526
|
+
const res = await fetch(`${getServer()}/api/provider/keys`, { headers: getHeaders() });
|
|
527
|
+
if (!res.ok) { spinner.fail('Failed to fetch providers'); const e = await res.json(); printError(e.error || 'Unknown error'); return; }
|
|
528
|
+
const providers = await res.json();
|
|
529
|
+
spinner.stop();
|
|
530
|
+
|
|
531
|
+
if (providers.length === 0) {
|
|
532
|
+
console.log();
|
|
533
|
+
console.log(chalk.hex('#565f89')(' No providers configured yet.'));
|
|
534
|
+
console.log(chalk.hex('#565f89')(' Use: osai-agent provider set <type> --key <key>'));
|
|
535
|
+
console.log();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.log();
|
|
540
|
+
console.log(chalk.hex('#7aa2f7').bold(' Configured Providers'));
|
|
541
|
+
console.log(chalk.hex('#565f89')(' ---'));
|
|
542
|
+
for (const p of providers) {
|
|
543
|
+
const activeMark = p.active ? chalk.green(' (active)') : '';
|
|
544
|
+
const modelStr = p.model ? ` | Model: ${chalk.white(p.model)}` : '';
|
|
545
|
+
const keyStr = p.key_masked ? ` | Key: ${chalk.gray(p.key_masked)}` : '';
|
|
546
|
+
console.log(` ${chalk.white(p.name.padEnd(22))}${activeMark}${modelStr}${keyStr}`);
|
|
547
|
+
}
|
|
548
|
+
console.log(chalk.hex('#565f89')(' ---'));
|
|
549
|
+
console.log(chalk.gray(' Use: osai-agent provider switch <type> to activate'));
|
|
550
|
+
console.log();
|
|
551
|
+
} catch (err) {
|
|
552
|
+
spinner.fail('Network error');
|
|
553
|
+
printError(err.message);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// ── changeModel — Change model of a configured provider ────────────
|
|
558
|
+
export const changeModel = async (type, model) => {
|
|
559
|
+
const token = getToken();
|
|
560
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
561
|
+
|
|
562
|
+
const spinner = ora(`Updating model for ${type}...`).start();
|
|
563
|
+
try {
|
|
564
|
+
const res = await fetch(`${getServer()}/api/provider/model`, {
|
|
565
|
+
method: 'PATCH',
|
|
566
|
+
headers: getHeaders(),
|
|
567
|
+
body: JSON.stringify({ type, model })
|
|
568
|
+
});
|
|
569
|
+
const data = await res.json();
|
|
570
|
+
if (!res.ok) { spinner.fail('Failed to update model'); printError(data.error || 'Unknown error'); return; }
|
|
571
|
+
spinner.succeed('Model updated');
|
|
572
|
+
printSuccess(`Model set to ${data.model} for ${data.type}`);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
spinner.fail('Network error');
|
|
575
|
+
printError(err.message);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// ── changeKey — Change API key of a configured provider ────────────
|
|
580
|
+
export const changeKey = async (type, newKey) => {
|
|
581
|
+
const token = getToken();
|
|
582
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
583
|
+
|
|
584
|
+
if (!newKey) {
|
|
585
|
+
const { key } = await inquirer.prompt({
|
|
586
|
+
type: 'password',
|
|
587
|
+
name: 'key',
|
|
588
|
+
message: `Enter new API key for ${type}:`,
|
|
589
|
+
validate: (v) => v.trim().length > 0 || 'API key required'
|
|
590
|
+
});
|
|
591
|
+
newKey = key;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const spinner = ora(`Updating API key for ${type}...`).start();
|
|
595
|
+
try {
|
|
596
|
+
const res = await fetch(`${getServer()}/api/provider/key`, {
|
|
597
|
+
method: 'PATCH',
|
|
598
|
+
headers: getHeaders(),
|
|
599
|
+
body: JSON.stringify({ type, api_key: newKey })
|
|
600
|
+
});
|
|
601
|
+
const data = await res.json();
|
|
602
|
+
if (!res.ok) { spinner.fail('Failed to update API key'); printError(data.error || 'Unknown error'); return; }
|
|
603
|
+
spinner.succeed('API key updated');
|
|
604
|
+
printSuccess(`API key updated for ${data.type} | Key: ${data.key}`);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
spinner.fail('Network error');
|
|
607
|
+
printError(err.message);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// ── switchProvider — Switch to a configured provider ───────────────
|
|
612
|
+
export const switchProvider = async (type) => {
|
|
613
|
+
const token = getToken();
|
|
614
|
+
if (!token) { printNotLoggedIn(); return; }
|
|
615
|
+
|
|
616
|
+
const spinner = ora(`Switching to ${type}...`).start();
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch(`${getServer()}/api/provider/switch`, {
|
|
619
|
+
method: 'POST',
|
|
620
|
+
headers: getHeaders(),
|
|
621
|
+
body: JSON.stringify({ type })
|
|
622
|
+
});
|
|
623
|
+
const data = await res.json();
|
|
624
|
+
if (!res.ok) { spinner.fail('Failed to switch provider'); printError(data.error || 'Unknown error'); return; }
|
|
625
|
+
spinner.succeed('Provider switched');
|
|
626
|
+
const modelDisplay = data.model ? ` | Model: ${data.model}` : '';
|
|
627
|
+
const keyDisplay = data.key ? ` | Key: ${data.key}` : '';
|
|
628
|
+
printSuccess(`Switched to ${data.type}${modelDisplay}${keyDisplay}`);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
spinner.fail('Network error');
|
|
631
|
+
printError(err.message);
|
|
632
|
+
}
|
|
633
|
+
};
|