@benzsiangco/jarvis 1.0.2 → 1.1.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/dist/cli.js +478 -347
- package/dist/electron/main.js +160 -0
- package/dist/electron/preload.js +19 -0
- package/package.json +19 -6
- package/skills.md +147 -0
- package/src/agents/index.ts +248 -0
- package/src/brain/loader.ts +136 -0
- package/src/cli.ts +411 -0
- package/src/config/index.ts +363 -0
- package/src/core/executor.ts +222 -0
- package/src/core/plugins.ts +148 -0
- package/src/core/types.ts +217 -0
- package/src/electron/main.ts +192 -0
- package/src/electron/preload.ts +25 -0
- package/src/electron/types.d.ts +20 -0
- package/src/index.ts +12 -0
- package/src/providers/antigravity-loader.ts +233 -0
- package/src/providers/antigravity.ts +585 -0
- package/src/providers/index.ts +523 -0
- package/src/sessions/index.ts +194 -0
- package/src/tools/index.ts +436 -0
- package/src/tui/index.tsx +784 -0
- package/src/utils/auth-prompt.ts +394 -0
- package/src/utils/index.ts +180 -0
- package/src/utils/native-picker.ts +71 -0
- package/src/utils/skills.ts +99 -0
- package/src/utils/table-integration-examples.ts +617 -0
- package/src/utils/table-utils.ts +401 -0
- package/src/web/build-ui.ts +27 -0
- package/src/web/server.ts +674 -0
- package/src/web/ui/dist/.gitkeep +0 -0
- package/src/web/ui/dist/main.css +1 -0
- package/src/web/ui/dist/main.js +320 -0
- package/src/web/ui/dist/main.js.map +20 -0
- package/src/web/ui/index.html +46 -0
- package/src/web/ui/src/App.tsx +143 -0
- package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
- package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
- package/src/web/ui/src/components/Layout/Header.tsx +91 -0
- package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
- package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
- package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
- package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
- package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
- package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
- package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
- package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
- package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
- package/src/web/ui/src/config/models.ts +70 -0
- package/src/web/ui/src/main.tsx +13 -0
- package/src/web/ui/src/store/agentStore.ts +41 -0
- package/src/web/ui/src/store/uiStore.ts +64 -0
- package/src/web/ui/src/types/index.ts +54 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// Provider abstraction layer for Jarvis
|
|
2
|
+
// Supports: Anthropic, OpenAI, Google, Groq, Mistral, Cohere, and more
|
|
3
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
6
|
+
import type { LanguageModel } from 'ai';
|
|
7
|
+
import type { JarvisConfig } from '../core/types';
|
|
8
|
+
import { parseModelId } from '../config';
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import {
|
|
13
|
+
getNextAccount,
|
|
14
|
+
trackAccountUsage,
|
|
15
|
+
markAccountRateLimited,
|
|
16
|
+
clearExpiredRateLimits,
|
|
17
|
+
hasAntigravityAccounts,
|
|
18
|
+
getAccountWithValidToken,
|
|
19
|
+
refreshAccountToken,
|
|
20
|
+
type AntigravityAccount
|
|
21
|
+
} from './antigravity';
|
|
22
|
+
|
|
23
|
+
// Antigravity API endpoints
|
|
24
|
+
const ANTIGRAVITY_ENDPOINTS = [
|
|
25
|
+
'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
|
26
|
+
'https://autopush-cloudcode-pa.sandbox.googleapis.com',
|
|
27
|
+
'https://cloudcode-pa.googleapis.com',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Load saved API keys from config
|
|
31
|
+
const API_KEYS_FILE = join(homedir(), '.config', 'jarvis', 'api-keys.json');
|
|
32
|
+
|
|
33
|
+
function loadSavedApiKeys(): Record<string, string> {
|
|
34
|
+
if (!existsSync(API_KEYS_FILE)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(API_KEYS_FILE, 'utf-8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get API key from saved keys or environment
|
|
45
|
+
function getApiKey(envVar: string, savedKeyId?: string): string | undefined {
|
|
46
|
+
// Environment variable takes precedence
|
|
47
|
+
const envKey = process.env[envVar];
|
|
48
|
+
if (envKey) return envKey;
|
|
49
|
+
|
|
50
|
+
// Fall back to saved keys
|
|
51
|
+
if (savedKeyId) {
|
|
52
|
+
const savedKeys = loadSavedApiKeys();
|
|
53
|
+
return savedKeys[savedKeyId];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ProviderType =
|
|
60
|
+
| 'anthropic'
|
|
61
|
+
| 'openai'
|
|
62
|
+
| 'google'
|
|
63
|
+
| 'groq'
|
|
64
|
+
| 'mistral'
|
|
65
|
+
| 'cohere'
|
|
66
|
+
| 'together'
|
|
67
|
+
| 'perplexity'
|
|
68
|
+
| 'fireworks'
|
|
69
|
+
| 'deepseek'
|
|
70
|
+
| 'xai'
|
|
71
|
+
| 'local';
|
|
72
|
+
|
|
73
|
+
interface ProviderInstance {
|
|
74
|
+
id: ProviderType;
|
|
75
|
+
name: string;
|
|
76
|
+
createModel: (modelId: string, options?: Record<string, unknown>) => LanguageModel;
|
|
77
|
+
isAvailable: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Provider registry
|
|
81
|
+
const providers = new Map<ProviderType, ProviderInstance>();
|
|
82
|
+
|
|
83
|
+
// Track current Antigravity account for rotation
|
|
84
|
+
let currentAntigravityAccount: AntigravityAccount | undefined;
|
|
85
|
+
|
|
86
|
+
// Initialize providers based on available API keys
|
|
87
|
+
export async function initializeProviders(config: JarvisConfig): Promise<void> {
|
|
88
|
+
providers.clear();
|
|
89
|
+
clearExpiredRateLimits();
|
|
90
|
+
|
|
91
|
+
// Anthropic
|
|
92
|
+
const anthropicKey = getApiKey('ANTHROPIC_API_KEY', 'anthropic');
|
|
93
|
+
if (anthropicKey) {
|
|
94
|
+
const anthropic = createAnthropic({ apiKey: anthropicKey });
|
|
95
|
+
providers.set('anthropic', {
|
|
96
|
+
id: 'anthropic',
|
|
97
|
+
name: 'Anthropic',
|
|
98
|
+
createModel: (modelId, options) => anthropic(modelId, options),
|
|
99
|
+
isAvailable: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// OpenAI
|
|
104
|
+
const openaiKey = getApiKey('OPENAI_API_KEY', 'openai');
|
|
105
|
+
if (openaiKey) {
|
|
106
|
+
const openai = createOpenAI({ apiKey: openaiKey });
|
|
107
|
+
providers.set('openai', {
|
|
108
|
+
id: 'openai',
|
|
109
|
+
name: 'OpenAI',
|
|
110
|
+
createModel: (modelId, options) => openai(modelId, options),
|
|
111
|
+
isAvailable: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Google AI (standard API key) - only if no Antigravity accounts
|
|
116
|
+
const googleKey = getApiKey('GOOGLE_API_KEY', 'google') || process.env['GOOGLE_GENERATIVE_AI_API_KEY'];
|
|
117
|
+
|
|
118
|
+
// For now, prefer API key auth as Antigravity OAuth requires complex request transformation
|
|
119
|
+
if (googleKey) {
|
|
120
|
+
const google = createGoogleGenerativeAI({ apiKey: googleKey });
|
|
121
|
+
providers.set('google', {
|
|
122
|
+
id: 'google',
|
|
123
|
+
name: 'Google AI',
|
|
124
|
+
createModel: (modelId, options) => {
|
|
125
|
+
const mappedModel = mapAntigravityModel(modelId);
|
|
126
|
+
// Extract variant and apply thinking config if present
|
|
127
|
+
const variant = options?.variant as string | undefined;
|
|
128
|
+
let finalOptions = { ...options };
|
|
129
|
+
|
|
130
|
+
if (variant) {
|
|
131
|
+
const { providerId, modelId: baseModelId } = parseModelId(modelId);
|
|
132
|
+
const modelCfg = config.provider?.[providerId]?.models?.[baseModelId];
|
|
133
|
+
const variantCfg = modelCfg?.variants?.[variant];
|
|
134
|
+
if (variantCfg) {
|
|
135
|
+
finalOptions = { ...finalOptions, ...variantCfg };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return google(mappedModel, finalOptions);
|
|
140
|
+
},
|
|
141
|
+
isAvailable: true,
|
|
142
|
+
});
|
|
143
|
+
} else if (hasAntigravityAccounts()) {
|
|
144
|
+
// Antigravity OAuth - requires request transformation
|
|
145
|
+
// For now, show as available but warn user they need API key for full functionality
|
|
146
|
+
await initializeAntigravityProvider();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Groq
|
|
150
|
+
const groqKey = getApiKey('GROQ_API_KEY', 'groq');
|
|
151
|
+
if (groqKey) {
|
|
152
|
+
const groq = createOpenAI({
|
|
153
|
+
apiKey: groqKey,
|
|
154
|
+
baseURL: 'https://api.groq.com/openai/v1'
|
|
155
|
+
});
|
|
156
|
+
providers.set('groq', {
|
|
157
|
+
id: 'groq',
|
|
158
|
+
name: 'Groq',
|
|
159
|
+
createModel: (modelId, options) => groq(modelId, options),
|
|
160
|
+
isAvailable: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Mistral
|
|
165
|
+
const mistralKey = getApiKey('MISTRAL_API_KEY', 'mistral');
|
|
166
|
+
if (mistralKey) {
|
|
167
|
+
const mistral = createOpenAI({
|
|
168
|
+
apiKey: mistralKey,
|
|
169
|
+
baseURL: 'https://api.mistral.ai/v1'
|
|
170
|
+
});
|
|
171
|
+
providers.set('mistral', {
|
|
172
|
+
id: 'mistral',
|
|
173
|
+
name: 'Mistral',
|
|
174
|
+
createModel: (modelId, options) => mistral(modelId, options),
|
|
175
|
+
isAvailable: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Together AI
|
|
180
|
+
const togetherKey = getApiKey('TOGETHER_API_KEY', 'together');
|
|
181
|
+
if (togetherKey) {
|
|
182
|
+
const together = createOpenAI({
|
|
183
|
+
apiKey: togetherKey,
|
|
184
|
+
baseURL: 'https://api.together.xyz/v1'
|
|
185
|
+
});
|
|
186
|
+
providers.set('together', {
|
|
187
|
+
id: 'together',
|
|
188
|
+
name: 'Together AI',
|
|
189
|
+
createModel: (modelId, options) => together(modelId, options),
|
|
190
|
+
isAvailable: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Perplexity
|
|
195
|
+
const perplexityKey = getApiKey('PERPLEXITY_API_KEY', 'perplexity');
|
|
196
|
+
if (perplexityKey) {
|
|
197
|
+
const perplexity = createOpenAI({
|
|
198
|
+
apiKey: perplexityKey,
|
|
199
|
+
baseURL: 'https://api.perplexity.ai'
|
|
200
|
+
});
|
|
201
|
+
providers.set('perplexity', {
|
|
202
|
+
id: 'perplexity',
|
|
203
|
+
name: 'Perplexity',
|
|
204
|
+
createModel: (modelId, options) => perplexity(modelId, options),
|
|
205
|
+
isAvailable: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fireworks
|
|
210
|
+
const fireworksKey = getApiKey('FIREWORKS_API_KEY', 'fireworks');
|
|
211
|
+
if (fireworksKey) {
|
|
212
|
+
const fireworks = createOpenAI({
|
|
213
|
+
apiKey: fireworksKey,
|
|
214
|
+
baseURL: 'https://api.fireworks.ai/inference/v1'
|
|
215
|
+
});
|
|
216
|
+
providers.set('fireworks', {
|
|
217
|
+
id: 'fireworks',
|
|
218
|
+
name: 'Fireworks',
|
|
219
|
+
createModel: (modelId, options) => fireworks(modelId, options),
|
|
220
|
+
isAvailable: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// DeepSeek
|
|
225
|
+
const deepseekKey = getApiKey('DEEPSEEK_API_KEY', 'deepseek');
|
|
226
|
+
if (deepseekKey) {
|
|
227
|
+
const deepseek = createOpenAI({
|
|
228
|
+
apiKey: deepseekKey,
|
|
229
|
+
baseURL: 'https://api.deepseek.com/v1'
|
|
230
|
+
});
|
|
231
|
+
providers.set('deepseek', {
|
|
232
|
+
id: 'deepseek',
|
|
233
|
+
name: 'DeepSeek',
|
|
234
|
+
createModel: (modelId, options) => deepseek(modelId, options),
|
|
235
|
+
isAvailable: true,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// xAI (Grok)
|
|
240
|
+
const xaiKey = getApiKey('XAI_API_KEY', 'xai');
|
|
241
|
+
if (xaiKey) {
|
|
242
|
+
const xai = createOpenAI({
|
|
243
|
+
apiKey: xaiKey,
|
|
244
|
+
baseURL: 'https://api.x.ai/v1'
|
|
245
|
+
});
|
|
246
|
+
providers.set('xai', {
|
|
247
|
+
id: 'xai',
|
|
248
|
+
name: 'xAI',
|
|
249
|
+
createModel: (modelId, options) => xai(modelId, options),
|
|
250
|
+
isAvailable: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Local LLM (OpenAI Compatible - Ollama, LM Studio, etc.)
|
|
255
|
+
// Does not require API key strictly, but checks for base URL
|
|
256
|
+
const localUrl = getApiKey('LOCAL_API_URL', 'local') || process.env['LOCAL_API_URL'] || 'http://localhost:11434/v1';
|
|
257
|
+
// We register it even if no custom key, using default localhost
|
|
258
|
+
const local = createOpenAI({
|
|
259
|
+
apiKey: 'not-needed',
|
|
260
|
+
baseURL: localUrl,
|
|
261
|
+
});
|
|
262
|
+
providers.set('local', {
|
|
263
|
+
id: 'local',
|
|
264
|
+
name: 'Local LLM',
|
|
265
|
+
createModel: (modelId, options) => local(modelId, options),
|
|
266
|
+
isAvailable: true,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Initialize Antigravity provider with OAuth tokens using the plugin's loader
|
|
271
|
+
async function initializeAntigravityProvider(): Promise<void> {
|
|
272
|
+
try {
|
|
273
|
+
// Use the plugin's loader to get the custom fetch function
|
|
274
|
+
const { initializeAntigravityLoader } = await import('./antigravity-loader.js');
|
|
275
|
+
const antigravityFetch = await initializeAntigravityLoader();
|
|
276
|
+
|
|
277
|
+
// Register Antigravity-enabled provider using Google Generative AI SDK
|
|
278
|
+
const google = createGoogleGenerativeAI({
|
|
279
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
|
|
280
|
+
apiKey: '', // Empty - OAuth handled by custom fetch
|
|
281
|
+
fetch: antigravityFetch as typeof fetch,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// This provider handles google/* models
|
|
285
|
+
providers.set('google', {
|
|
286
|
+
id: 'google',
|
|
287
|
+
name: 'Google (Antigravity)',
|
|
288
|
+
createModel: (modelId, options) => {
|
|
289
|
+
// Use model name as-is - plugin handles the transformation
|
|
290
|
+
return google(modelId, options);
|
|
291
|
+
},
|
|
292
|
+
isAvailable: true,
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Failed to initialize Antigravity provider:', error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Map Antigravity model names to actual model identifiers
|
|
300
|
+
// For Antigravity Cloud Code endpoint, use model names as-is
|
|
301
|
+
function mapAntigravityModel(modelId: string): string {
|
|
302
|
+
// Don't transform - Antigravity handles these model names directly
|
|
303
|
+
return modelId;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Refresh Antigravity account (call before requests)
|
|
307
|
+
export function refreshAntigravityAccount(): AntigravityAccount | undefined {
|
|
308
|
+
clearExpiredRateLimits();
|
|
309
|
+
const account = getNextAccount();
|
|
310
|
+
if (account) {
|
|
311
|
+
currentAntigravityAccount = account;
|
|
312
|
+
}
|
|
313
|
+
return account;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Get current Antigravity account
|
|
317
|
+
export function getCurrentAntigravityAccount(): AntigravityAccount | undefined {
|
|
318
|
+
return currentAntigravityAccount;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function getProvider(providerId: ProviderType): ProviderInstance | undefined {
|
|
322
|
+
return providers.get(providerId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function getModel(modelId: string, options?: Record<string, unknown>): LanguageModel | undefined {
|
|
326
|
+
const { providerId, modelId: model } = parseModelId(modelId);
|
|
327
|
+
const provider = providers.get(providerId as ProviderType);
|
|
328
|
+
|
|
329
|
+
if (!provider) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return provider.createModel(model, options);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function listProviders(): ProviderType[] {
|
|
337
|
+
return Array.from(providers.keys());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function hasProvider(providerId: string): boolean {
|
|
341
|
+
return providers.has(providerId as ProviderType);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Get all available providers with status
|
|
345
|
+
export function getProvidersWithStatus(): Array<{
|
|
346
|
+
id: ProviderType;
|
|
347
|
+
name: string;
|
|
348
|
+
isAvailable: boolean;
|
|
349
|
+
envVar: string;
|
|
350
|
+
maskedKey?: string;
|
|
351
|
+
}> {
|
|
352
|
+
// Helper to mask key
|
|
353
|
+
const mask = (key?: string) => {
|
|
354
|
+
if (!key) return undefined;
|
|
355
|
+
if (key.length < 8) return '********';
|
|
356
|
+
return `${key.substring(0, 3)}...${key.substring(key.length - 4)}`;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const allProviders: Array<{
|
|
360
|
+
id: ProviderType;
|
|
361
|
+
name: string;
|
|
362
|
+
isAvailable: boolean;
|
|
363
|
+
envVar: string;
|
|
364
|
+
maskedKey?: string;
|
|
365
|
+
}> = [
|
|
366
|
+
{
|
|
367
|
+
id: 'anthropic',
|
|
368
|
+
name: 'Anthropic (Claude)',
|
|
369
|
+
isAvailable: providers.has('anthropic'),
|
|
370
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
371
|
+
maskedKey: mask(getApiKey('ANTHROPIC_API_KEY', 'anthropic'))
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'openai',
|
|
375
|
+
name: 'OpenAI (GPT)',
|
|
376
|
+
isAvailable: providers.has('openai'),
|
|
377
|
+
envVar: 'OPENAI_API_KEY',
|
|
378
|
+
maskedKey: mask(getApiKey('OPENAI_API_KEY', 'openai'))
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'google',
|
|
382
|
+
name: 'Google (Gemini)',
|
|
383
|
+
isAvailable: providers.has('google'),
|
|
384
|
+
envVar: 'GOOGLE_API_KEY or OAuth',
|
|
385
|
+
maskedKey: mask(getApiKey('GOOGLE_API_KEY', 'google'))
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: 'groq',
|
|
389
|
+
name: 'Groq',
|
|
390
|
+
isAvailable: providers.has('groq'),
|
|
391
|
+
envVar: 'GROQ_API_KEY',
|
|
392
|
+
maskedKey: mask(getApiKey('GROQ_API_KEY', 'groq'))
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'mistral',
|
|
396
|
+
name: 'Mistral',
|
|
397
|
+
isAvailable: providers.has('mistral'),
|
|
398
|
+
envVar: 'MISTRAL_API_KEY',
|
|
399
|
+
maskedKey: mask(getApiKey('MISTRAL_API_KEY', 'mistral'))
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: 'together',
|
|
403
|
+
name: 'Together AI',
|
|
404
|
+
isAvailable: providers.has('together'),
|
|
405
|
+
envVar: 'TOGETHER_API_KEY',
|
|
406
|
+
maskedKey: mask(getApiKey('TOGETHER_API_KEY', 'together'))
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'perplexity',
|
|
410
|
+
name: 'Perplexity',
|
|
411
|
+
isAvailable: providers.has('perplexity'),
|
|
412
|
+
envVar: 'PERPLEXITY_API_KEY',
|
|
413
|
+
maskedKey: mask(getApiKey('PERPLEXITY_API_KEY', 'perplexity'))
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: 'fireworks',
|
|
417
|
+
name: 'Fireworks',
|
|
418
|
+
isAvailable: providers.has('fireworks'),
|
|
419
|
+
envVar: 'FIREWORKS_API_KEY',
|
|
420
|
+
maskedKey: mask(getApiKey('FIREWORKS_API_KEY', 'fireworks'))
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: 'deepseek',
|
|
424
|
+
name: 'DeepSeek',
|
|
425
|
+
isAvailable: providers.has('deepseek'),
|
|
426
|
+
envVar: 'DEEPSEEK_API_KEY',
|
|
427
|
+
maskedKey: mask(getApiKey('DEEPSEEK_API_KEY', 'deepseek'))
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'xai',
|
|
431
|
+
name: 'xAI (Grok)',
|
|
432
|
+
isAvailable: providers.has('xai'),
|
|
433
|
+
envVar: 'XAI_API_KEY',
|
|
434
|
+
maskedKey: mask(getApiKey('XAI_API_KEY', 'xai'))
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: 'local',
|
|
438
|
+
name: 'Local (LM Studio / Ollama)',
|
|
439
|
+
isAvailable: providers.has('local'),
|
|
440
|
+
envVar: 'LOCAL_API_URL',
|
|
441
|
+
maskedKey: getApiKey('LOCAL_API_URL', 'local') || 'http://localhost:11434/v1'
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
return allProviders;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Fetch dynamic models from Local LLM provider (Ollama/LM Studio)
|
|
449
|
+
export async function fetchLocalModels(): Promise<Array<{ id: string; name: string; provider: string; isAvailable: boolean }>> {
|
|
450
|
+
const localUrl = getApiKey('LOCAL_API_URL', 'local') || process.env['LOCAL_API_URL'] || 'http://localhost:11434/v1';
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const baseUrl = localUrl.replace(/\/+$/, '');
|
|
454
|
+
// Handle both /v1/models (standard) and just /models if user omitted v1 but server needs it?
|
|
455
|
+
// Usually user provides ".../v1".
|
|
456
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
457
|
+
|
|
458
|
+
// Add timeout to prevent hanging
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
|
|
461
|
+
|
|
462
|
+
const response = await fetch(modelsUrl, {
|
|
463
|
+
signal: controller.signal,
|
|
464
|
+
// Add some headers to make it more compatible
|
|
465
|
+
headers: {
|
|
466
|
+
'Accept': 'application/json',
|
|
467
|
+
'User-Agent': 'JARVIS-LocalLLM-Fetcher/1.0'
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
clearTimeout(timeoutId);
|
|
472
|
+
|
|
473
|
+
if (!response.ok) return [];
|
|
474
|
+
|
|
475
|
+
const data = await response.json();
|
|
476
|
+
const list = Array.isArray(data) ? data : (data.data && Array.isArray(data.data) ? data.data : []);
|
|
477
|
+
|
|
478
|
+
return list.map((m: any) => ({
|
|
479
|
+
id: `local/${m.id}`,
|
|
480
|
+
name: m.id, // Ollama/LM Studio models often use the ID as the name
|
|
481
|
+
provider: 'local',
|
|
482
|
+
isAvailable: true
|
|
483
|
+
}));
|
|
484
|
+
} catch (e) {
|
|
485
|
+
// Quiet fail if local is not running or times out
|
|
486
|
+
console.debug('[Local Models] Could not fetch:', (e as Error).message);
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Get available models from config
|
|
492
|
+
export function getAvailableModels(config: JarvisConfig): Array<{ id: string; name: string; provider: string; isAvailable: boolean }> {
|
|
493
|
+
const models: Array<{ id: string; name: string; provider: string; isAvailable: boolean }> = [];
|
|
494
|
+
|
|
495
|
+
// Always use the full provider set from config
|
|
496
|
+
const providersToUse = config.provider || {};
|
|
497
|
+
|
|
498
|
+
for (const [providerId, providerConfig] of Object.entries(providersToUse)) {
|
|
499
|
+
// Check if the provider is actually initialized in the map
|
|
500
|
+
const isAvailable = providers.has(providerId as ProviderType);
|
|
501
|
+
|
|
502
|
+
if (providerConfig?.models) {
|
|
503
|
+
for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
|
|
504
|
+
models.push({
|
|
505
|
+
id: `${providerId}/${modelId}`,
|
|
506
|
+
name: modelConfig.name,
|
|
507
|
+
provider: providerId,
|
|
508
|
+
isAvailable,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Absolute fallback if everything else fails
|
|
515
|
+
if (models.length === 0) {
|
|
516
|
+
return [
|
|
517
|
+
{ id: 'google/antigravity-claude-sonnet-4-5', name: 'Claude Sonnet 4.5 (Antigravity)', provider: 'google', isAvailable: true },
|
|
518
|
+
{ id: 'google/antigravity-gemini-3-pro', name: 'Gemini 3 Pro (Antigravity)', provider: 'google', isAvailable: true },
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return models;
|
|
523
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Session management for Jarvis
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { getConfigDir } from '../config';
|
|
6
|
+
import type { Session, Message, MessagePart } from '../core/types';
|
|
7
|
+
|
|
8
|
+
const SESSIONS_DIR = join(getConfigDir(), 'sessions');
|
|
9
|
+
|
|
10
|
+
interface TodoItem {
|
|
11
|
+
id: string;
|
|
12
|
+
content: string;
|
|
13
|
+
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
|
14
|
+
priority: 'high' | 'medium' | 'low';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SessionState {
|
|
18
|
+
todos: TodoItem[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// In-memory session store
|
|
22
|
+
const sessions = new Map<string, Session>();
|
|
23
|
+
const sessionStates = new Map<string, SessionState>();
|
|
24
|
+
|
|
25
|
+
export function ensureSessionsDir(): void {
|
|
26
|
+
if (!existsSync(SESSIONS_DIR)) {
|
|
27
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSession(agentId: string, title?: string, parentId?: string, workdir?: string): Session {
|
|
32
|
+
const session: Session = {
|
|
33
|
+
id: randomUUID(),
|
|
34
|
+
title: title || `Session ${new Date().toLocaleString()}`,
|
|
35
|
+
agentId,
|
|
36
|
+
workdir: workdir || process.cwd(),
|
|
37
|
+
messages: [],
|
|
38
|
+
createdAt: new Date(),
|
|
39
|
+
updatedAt: new Date(),
|
|
40
|
+
parentId,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
sessions.set(session.id, session);
|
|
44
|
+
sessionStates.set(session.id, { todos: [] });
|
|
45
|
+
saveSession(session);
|
|
46
|
+
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getSession(id: string): Session | undefined {
|
|
51
|
+
// Try memory first
|
|
52
|
+
let session = sessions.get(id);
|
|
53
|
+
|
|
54
|
+
if (!session) {
|
|
55
|
+
// Try loading from disk
|
|
56
|
+
session = loadSessionFromDisk(id);
|
|
57
|
+
if (session) {
|
|
58
|
+
sessions.set(id, session);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updateSession(session: Session): void {
|
|
66
|
+
session.updatedAt = new Date();
|
|
67
|
+
sessions.set(session.id, session);
|
|
68
|
+
saveSession(session);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteSession(id: string): boolean {
|
|
72
|
+
const session = sessions.get(id);
|
|
73
|
+
if (!session) return false;
|
|
74
|
+
|
|
75
|
+
sessions.delete(id);
|
|
76
|
+
sessionStates.delete(id);
|
|
77
|
+
|
|
78
|
+
// Delete from disk
|
|
79
|
+
const filePath = join(SESSIONS_DIR, `${id}.json`);
|
|
80
|
+
if (existsSync(filePath)) {
|
|
81
|
+
unlinkSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function listSessions(): Session[] {
|
|
88
|
+
ensureSessionsDir();
|
|
89
|
+
|
|
90
|
+
// Load all sessions from disk
|
|
91
|
+
const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
|
|
92
|
+
|
|
93
|
+
const allSessions: Session[] = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const id = file.replace('.json', '');
|
|
96
|
+
const session = getSession(id);
|
|
97
|
+
if (session) {
|
|
98
|
+
allSessions.push(session);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Sort by updated date, newest first
|
|
103
|
+
return allSessions.sort((a, b) =>
|
|
104
|
+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function addMessage(sessionId: string, message: Omit<Message, 'id' | 'timestamp'>): Message {
|
|
109
|
+
const session = getSession(sessionId);
|
|
110
|
+
if (!session) {
|
|
111
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fullMessage: Message = {
|
|
115
|
+
...message,
|
|
116
|
+
id: randomUUID(),
|
|
117
|
+
timestamp: new Date(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
session.messages.push(fullMessage);
|
|
121
|
+
updateSession(session);
|
|
122
|
+
|
|
123
|
+
return fullMessage;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getMessages(sessionId: string): Message[] {
|
|
127
|
+
const session = getSession(sessionId);
|
|
128
|
+
return session?.messages || [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Session state management (todos, etc.)
|
|
132
|
+
export function getSessionState(sessionId: string): SessionState {
|
|
133
|
+
return sessionStates.get(sessionId) || { todos: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function setTodos(sessionId: string, todos: TodoItem[]): void {
|
|
137
|
+
const state = getSessionState(sessionId);
|
|
138
|
+
state.todos = todos;
|
|
139
|
+
sessionStates.set(sessionId, state);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getTodos(sessionId: string): TodoItem[] {
|
|
143
|
+
return getSessionState(sessionId).todos;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Persistence
|
|
147
|
+
function saveSession(session: Session): void {
|
|
148
|
+
ensureSessionsDir();
|
|
149
|
+
const filePath = join(SESSIONS_DIR, `${session.id}.json`);
|
|
150
|
+
writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadSessionFromDisk(id: string): Session | undefined {
|
|
154
|
+
ensureSessionsDir();
|
|
155
|
+
const filePath = join(SESSIONS_DIR, `${id}.json`);
|
|
156
|
+
|
|
157
|
+
if (!existsSync(filePath)) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const data = readFileSync(filePath, 'utf-8');
|
|
163
|
+
const session = JSON.parse(data) as Session;
|
|
164
|
+
|
|
165
|
+
// Convert date strings to Date objects
|
|
166
|
+
session.createdAt = new Date(session.createdAt);
|
|
167
|
+
session.updatedAt = new Date(session.updatedAt);
|
|
168
|
+
session.messages.forEach(m => {
|
|
169
|
+
m.timestamp = new Date(m.timestamp);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return session;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(`Error loading session ${id}:`, error);
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get child sessions
|
|
180
|
+
export function getChildSessions(parentId: string): Session[] {
|
|
181
|
+
return listSessions().filter(s => s.parentId === parentId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clear all sessions (for testing/reset)
|
|
185
|
+
export function clearAllSessions(): void {
|
|
186
|
+
sessions.clear();
|
|
187
|
+
sessionStates.clear();
|
|
188
|
+
|
|
189
|
+
ensureSessionsDir();
|
|
190
|
+
const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
unlinkSync(join(SESSIONS_DIR, file));
|
|
193
|
+
}
|
|
194
|
+
}
|