AG X v2.0.3 - Antigravity fork with multi-provider support

Features:
- Welcome screen on first run (provider choice before LS starts)
- 15+ AI providers (Google Gemini, OpenAI, Anthropic, DeepSeek, Ollama, etc.)
- Provider config syncs to endpoints.json for translation proxy
- Built-in Node.js translation proxy for non-native backends
- Auto-update support, tray integration, URI scheme handler
This commit is contained in:
admin
2026-05-22 23:20:10 +04:00
Unverified
commit 43e2a2f78f
46 changed files with 7719 additions and 0 deletions

116
dist/services/apiProxy.js vendored Normal file
View File

@@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiProxy = void 0;
const http = require("http");
const https = require("https");
const child_process = require("child_process");
const path = require("path");
const os = require("os");
const fs = require("fs");
const translationProxy_1 = require("./translationProxy");
/**
* API Proxy for AG X.
*
* All backends now use the built-in Node.js translation proxy.
* No external Python or tools required.
*/
class ApiProxy {
constructor(providerService) {
this.providerService = providerService;
this.server = null;
this.proxyProcess = null;
this.port = providerService.getProxyPort();
}
start() {
if (this.server || this.proxyProcess) {
return;
}
const backendType = this.providerService.getActiveBackendType();
if (backendType === 'gemini-native' || backendType === 'native') {
console.log('[ApiProxy] Native backend, no proxy needed');
return;
}
this.startInternalProxy();
}
/**
* Start the built-in Node.js translation proxy as a child process
*/
startInternalProxy() {
const activeProvider = this.providerService.getActiveProvider();
const config = this.providerService.getProviderConfig(activeProvider);
if (!config) {
console.error('[ApiProxy] No provider config found');
return;
}
// Prepare the proxy config
const configDir = path.join(os.homedir(), '.cache', 'ag-x-proxy');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const models = (config.models || []).map((m) => {
if (typeof m === 'string') {
return { id: m, object: "model", created: 1700000000, owned_by: activeProvider };
}
return m;
});
const proxyConfig = {
port: this.port,
backend_type: config.backendType || 'openai-compat',
target_url: config.apiUrl || '',
api_key: config.apiKey || '',
cc_version: config.ccVersion || '0.26.8',
oauth_provider: config.oauthProvider || '',
reasoning_enabled: config.reasoningEnabled !== undefined ? config.reasoningEnabled : true,
reasoning_effort: config.reasoningEffort || 'medium',
models: models,
};
const configPath = path.join(configDir, `ag-x-proxy-${this.port}.json`);
fs.writeFileSync(configPath, JSON.stringify(proxyConfig, null, 2), 'utf-8');
console.log(`[ApiProxy] Starting built-in Node.js translation proxy on port ${this.port}`);
// Use Electron's Node.js binary to run our translation proxy
const proxyScript = path.join(__dirname, 'translationProxy.js');
const nodeBin = process.execPath;
this.proxyProcess = child_process.spawn(nodeBin, [proxyScript], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
env: { ...process.env },
});
this.proxyProcess.stdout?.on('data', (data) => {
const lines = data.toString().trim().split('\n');
for (const line of lines) {
console.log(`[Proxy] ${line}`);
}
});
this.proxyProcess.stderr?.on('data', (data) => {
const lines = data.toString().trim().split('\n');
for (const line of lines) {
console.log(`[Proxy] ${line}`);
}
});
this.proxyProcess.on('error', (err) => {
console.error('[ApiProxy] Proxy error:', err);
});
this.proxyProcess.on('exit', (code, signal) => {
console.log(`[ApiProxy] Proxy exited with code ${code}, signal ${signal}`);
this.proxyProcess = null;
});
this.proxyProcess.unref();
console.log(`[ApiProxy] Built-in Node.js proxy spawned for ${config.backendType} on port ${this.port}`);
}
stop() {
if (this.proxyProcess) {
try {
this.proxyProcess.kill();
}
catch (e) { /* ignore */ }
this.proxyProcess = null;
}
if (this.server) {
this.server.close();
this.server = null;
console.log('[ApiProxy] Stopped');
}
}
}
exports.ApiProxy = ApiProxy;

358
dist/services/providerService.js vendored Normal file
View File

@@ -0,0 +1,358 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProviderService = exports.ProviderType = void 0;
const fs = require("fs");
const path = require("path");
const os = require("os");
var ProviderType;
(function (ProviderType) {
ProviderType["GOOGLE_GEMINI"] = "google_gemini";
ProviderType["OPENAI"] = "openai";
ProviderType["ANTHROPIC"] = "anthropic";
ProviderType["Z_AI"] = "z_ai";
ProviderType["OPENCODE_ZEN"] = "opencode_zen";
ProviderType["OPENCODE_GO"] = "opencode_go";
ProviderType["OPENCODE_ZEN_ANTHROPIC"] = "opencode_zen_anthropic";
ProviderType["OPENCODE_GO_ANTHROPIC"] = "opencode_go_anthropic";
ProviderType["CROF_AI"] = "crof_ai";
ProviderType["NVIDIA_NIM"] = "nvidia_nim";
ProviderType["KILO_AI"] = "kilo_ai";
ProviderType["COMMAND_CODE"] = "command_code";
ProviderType["OPENROUTER"] = "openrouter";
ProviderType["OPENADAPTER"] = "openadapter";
ProviderType["DEEPSEEK"] = "deepseek";
ProviderType["OLLAMA"] = "ollama";
ProviderType["TOGETHER"] = "together";
ProviderType["GROQ"] = "groq";
ProviderType["CUSTOM"] = "custom";
})(ProviderType = exports.ProviderType || (exports.ProviderType = {}));
const PREDEFINED_PROVIDERS = {
[ProviderType.GOOGLE_GEMINI]: {
name: 'Google Gemini (OAuth)',
icon: '🔮',
description: 'Built-in Gemini via Google OAuth — zero config',
apiUrl: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
backendType: 'gemini-native',
requiresApiKey: false,
apiKeyHint: '',
models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-3-flash-preview', 'gemini-3-pro-preview'],
defaultModel: 'gemini-2.5-pro',
},
[ProviderType.OPENAI]: {
name: 'OpenAI',
icon: '🟢',
description: 'GPT models via Responses API',
apiUrl: 'https://api.openai.com/v1',
backendType: 'native',
requiresApiKey: true,
apiKeyHint: 'OpenAI API Key (sk-...)',
models: ['gpt-4o', 'gpt-4o-mini', 'o1', 'o1-mini', 'o3', 'o3-mini'],
defaultModel: 'gpt-4o',
},
[ProviderType.ANTHROPIC]: {
name: 'Anthropic',
icon: '🟣',
description: 'Claude models via Messages API',
apiUrl: 'https://api.anthropic.com',
backendType: 'anthropic',
requiresApiKey: true,
apiKeyHint: 'Anthropic API Key (sk-ant-...)',
models: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'],
defaultModel: 'claude-sonnet-4-20250514',
},
[ProviderType.Z_AI]: {
name: 'Z.AI Coding',
icon: '🅩',
description: 'GLM & Z models via Z.AI',
apiUrl: 'https://api.z.ai/api/coding/paas/v4',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'Z.AI API Key',
models: ['glm-5.1', 'glm-4.7', 'GLM-4-Plus', 'GLM-4-Long', 'GLM-4-Flash', 'GLM-4-FlashX', 'GLM-Z1-Flash'],
defaultModel: 'glm-5.1',
},
[ProviderType.OPENCODE_ZEN]: {
name: 'OpenCode Zen',
icon: '🧘',
description: 'Multi-model via OpenCode Zen (OpenAI-compat)',
apiUrl: 'https://opencode.ai/zen/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'OpenCode API Key',
models: ['glm-5.1', 'glm-5', 'kimi-k2.5', 'kimi-k2.6', 'minimax-m2.7', 'minimax-m2.5', 'minimax-m2.5-free', 'deepseek-v4-flash-free', 'nemotron-3-super-free', 'qwen3.6-plus', 'qwen3.5-plus', 'big-pickle'],
defaultModel: 'glm-5.1',
},
[ProviderType.OPENCODE_GO]: {
name: 'OpenCode Go',
icon: '🚀',
description: 'Multi-model via OpenCode Go (OpenAI-compat)',
apiUrl: 'https://opencode.ai/zen/go/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'OpenCode API Key',
models: ['glm-5.1', 'glm-5', 'kimi-k2.5', 'kimi-k2.6', 'mimo-v2.5', 'mimo-v2.5-pro', 'minimax-m2.7', 'minimax-m2.5', 'qwen3.6-plus', 'qwen3.5-plus', 'deepseek-v4-pro', 'deepseek-v4-flash'],
defaultModel: 'glm-5.1',
},
[ProviderType.OPENCODE_ZEN_ANTHROPIC]: {
name: 'OpenCode Zen (Anthropic)',
icon: '🧘',
description: 'Claude models via OpenCode Zen',
apiUrl: 'https://opencode.ai/zen/v1',
backendType: 'anthropic',
requiresApiKey: true,
apiKeyHint: 'OpenCode API Key',
models: ['claude-opus-4-7', 'claude-opus-4-6', 'claude-opus-4-5', 'claude-opus-4-1', 'claude-sonnet-4-6', 'claude-sonnet-4-5', 'claude-sonnet-4', 'claude-haiku-4-5', 'claude-3-5-haiku'],
defaultModel: 'claude-sonnet-4-6',
},
[ProviderType.OPENCODE_GO_ANTHROPIC]: {
name: 'OpenCode Go (Anthropic)',
icon: '🚀',
description: 'Claude models via OpenCode Go',
apiUrl: 'https://opencode.ai/zen/go/v1',
backendType: 'anthropic',
requiresApiKey: true,
apiKeyHint: 'OpenCode API Key',
models: ['minimax-m2.7', 'minimax-m2.5'],
defaultModel: 'minimax-m2.7',
},
[ProviderType.CROF_AI]: {
name: 'Crof.ai',
icon: '🌐',
description: 'OpenAI-compatible models via Crof.ai',
apiUrl: 'https://crof.ai/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'Crof.ai API Key',
models: [],
defaultModel: '',
},
[ProviderType.NVIDIA_NIM]: {
name: 'NVIDIA NIM',
icon: '💚',
description: 'NVIDIA accelerated inference models',
apiUrl: 'https://integrate.api.nvidia.com/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'NVIDIA API Key (nvapi-...)',
models: [],
defaultModel: '',
},
[ProviderType.KILO_AI]: {
name: 'Kilo.ai Gateway',
icon: '⚖️',
description: 'Multi-provider via Kilo.ai',
apiUrl: 'https://api.kilo.ai/api/gateway',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'Kilo.ai API Key',
models: [],
defaultModel: '',
},
[ProviderType.COMMAND_CODE]: {
name: 'Command Code',
icon: '⌘',
description: '20+ models via Command Code API',
apiUrl: 'https://api.commandcode.ai',
backendType: 'command-code',
requiresApiKey: true,
apiKeyHint: 'Command Code API Key',
ccVersion: '0.26.8',
models: [
'deepseek/deepseek-v4-flash', 'deepseek/deepseek-v4-pro',
'anthropic:claude-sonnet-4-6', 'anthropic:claude-haiku-4-5-20251001',
'anthropic:claude-opus-4-7', 'anthropic:claude-opus-4-6',
'openai:gpt-5.5', 'openai:gpt-5.4', 'openai:gpt-5.4-mini', 'openai:gpt-5.3-codex',
'moonshotai/Kimi-K2.6', 'moonshotai/Kimi-K2.5',
'zai-org/GLM-5.1', 'zai-org/GLM-5',
'MiniMaxAI/MiniMax-M2.7', 'MiniMaxAI/MiniMax-M2.5',
'Qwen/Qwen3.6-Max-Preview', 'Qwen/Qwen3.6-Plus',
'stepfun/Step-3.5-Flash', 'google/gemini-3.1-flash-lite',
],
defaultModel: 'deepseek/deepseek-v4-flash',
},
[ProviderType.OPENROUTER]: {
name: 'OpenRouter',
icon: '🔀',
description: 'Route to hundreds of models via OpenRouter',
apiUrl: 'https://openrouter.ai/api/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'OpenRouter API Key (sk-or-...)',
models: [],
defaultModel: '',
},
[ProviderType.OPENADAPTER]: {
name: 'OpenAdapter',
icon: '🔌',
description: 'Free/proxy models via OpenAdapter',
apiUrl: 'https://api.openadapter.in/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'OpenAdapter API Key',
models: ['0G-DeepSeek-V3', '0G-DeepSeek-v4-Pro', '0G-GLM-5', '0G-GLM-5.1', '0G-Qwen3.6', '0G-Qwen-VL'],
defaultModel: '0G-DeepSeek-v4-Pro',
},
[ProviderType.DEEPSEEK]: {
name: 'DeepSeek',
icon: '🔍',
description: 'DeepSeek models directly',
apiUrl: 'https://api.deepseek.com/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'DeepSeek API Key',
models: ['deepseek-chat', 'deepseek-reasoner'],
defaultModel: 'deepseek-chat',
},
[ProviderType.OLLAMA]: {
name: 'Ollama (Local)',
icon: '🦙',
description: 'Run models locally with Ollama',
apiUrl: 'http://127.0.0.1:11434',
backendType: 'openai-compat',
requiresApiKey: false,
apiKeyHint: '',
models: ['llama3.1', 'llama3', 'codellama', 'mistral', 'mixtral', 'deepseek-coder', 'qwen2.5-coder'],
defaultModel: 'llama3.1',
},
[ProviderType.TOGETHER]: {
name: 'Together AI',
icon: '🤝',
description: 'Open-source models via Together',
apiUrl: 'https://api.together.xyz/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'Together API Key',
models: [],
defaultModel: '',
},
[ProviderType.GROQ]: {
name: 'Groq',
icon: '⚡',
description: 'Ultra-fast inference via Groq',
apiUrl: 'https://api.groq.com/openai/v1',
backendType: 'openai-compat',
requiresApiKey: true,
apiKeyHint: 'Groq API Key (gsk_...)',
models: [],
defaultModel: '',
},
[ProviderType.CUSTOM]: {
name: 'Custom Provider',
icon: '⚙️',
description: 'Any OpenAI-compatible endpoint',
apiUrl: '',
backendType: 'openai-compat',
requiresApiKey: false,
apiKeyHint: 'API Key (if required)',
models: [],
defaultModel: '',
},
};
class ProviderService {
constructor(storageManager) {
this.storageManager = storageManager;
this.configPath = path.join(os.homedir(), '.ag-x', 'ag-x', 'provider_config.json');
this.config = this.loadConfig();
}
loadConfig() {
try {
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, 'utf-8');
return JSON.parse(content);
}
}
catch (e) {
console.error('Error loading provider config:', e);
}
// Build defaults from all predefined providers
const providers = {};
for (const [key, preset] of Object.entries(PREDEFINED_PROVIDERS)) {
providers[key] = {
...preset,
apiKey: '',
model: preset.defaultModel,
enabled: key === ProviderType.GOOGLE_GEMINI,
};
}
return {
activeProvider: ProviderType.GOOGLE_GEMINI,
providers,
};
}
saveConfig() {
try {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
}
catch (e) {
console.error('Error saving provider config:', e);
}
}
getActiveProvider() {
return this.config.activeProvider;
}
setActiveProvider(providerType) {
this.config.activeProvider = providerType;
this.saveConfig();
}
getProviderConfig(providerType) {
return this.config.providers[providerType] || null;
}
updateProviderConfig(providerType, updates) {
if (this.config.providers[providerType]) {
Object.assign(this.config.providers[providerType], updates);
this.saveConfig();
}
}
getAllProviders() {
return this.config.providers;
}
getPredefinedProviders() {
return PREDEFINED_PROVIDERS;
}
getActiveProviderApiUrl() {
const provider = this.config.providers[this.config.activeProvider];
return provider?.apiUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com';
}
getActiveProviderApiKey() {
const provider = this.config.providers[this.config.activeProvider];
return provider?.apiKey || '';
}
getActiveProviderModel() {
const provider = this.config.providers[this.config.activeProvider];
return provider?.model || '';
}
getActiveBackendType() {
const provider = this.config.providers[this.config.activeProvider];
return provider?.backendType || 'gemini-native';
}
getActiveCcVersion() {
const provider = this.config.providers[this.config.activeProvider];
return provider?.ccVersion || '0.26.8';
}
/**
* Does this provider need the built-in translation proxy?
* gemini-native and native (OpenAI) go direct; everything else needs proxy.
*/
needsProxy() {
const bt = this.getActiveBackendType();
return bt !== 'gemini-native' && bt !== 'native';
}
/**
* Does this provider need the full translation proxy (anthropic, command-code)?
* command-code and anthropic need the full Python proxy for full compatibility.
*/
needsTranslateProxy() {
const bt = this.getActiveBackendType();
return bt === 'command-code' || bt === 'anthropic';
}
getProxyPort() {
return 9876;
}
}
exports.ProviderService = ProviderService;

49
dist/services/settingsService.js vendored Normal file
View File

@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SettingsService = exports.DEFAULTS = exports.SettingKey = void 0;
const utils_1 = require("../utils");
// Setting keys
var SettingKey;
(function (SettingKey) {
SettingKey["RUN_IN_BACKGROUND"] = "runInBackground";
SettingKey["KEEP_COMPUTER_AWAKE"] = "keepComputerAwake";
})(SettingKey || (exports.SettingKey = SettingKey = {}));
// Default values
exports.DEFAULTS = new Map([
// The following setting is off by default for windows because the app
// icon is not as discoverable in the bottom right corner menu bar as
// it is on macOS and linux.
[SettingKey.RUN_IN_BACKGROUND, process.platform !== 'win32'],
[SettingKey.KEEP_COMPUTER_AWAKE, false],
]);
/**
* A thin wrapper around StorageManager to listen for changes
* in settings and apply their side effects.
*/
class SettingsService {
constructor(storageManager) {
this.storageManager = storageManager;
this.storageManager.onDidChange((changes) => {
this.applySideEffects(changes);
});
void this.initialize();
}
async initialize() {
const items = await this.storageManager.getItems();
this.applySideEffects(items);
}
applySideEffects(settings) {
const val = settings[SettingKey.KEEP_COMPUTER_AWAKE];
if (val !== undefined) {
const preventSleep = val === null
? exports.DEFAULTS.get(SettingKey.KEEP_COMPUTER_AWAKE)
: val === 'true';
utils_1.SleepBlocker.getInstance().shouldKeepComputerAwake(preventSleep);
}
}
async getSetting(key) {
const items = await this.storageManager.getItems();
return items[key] === 'true';
}
}
exports.SettingsService = SettingsService;

65
dist/services/settingsService.test.js vendored Normal file
View File

@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const settingsService_1 = require("./settingsService");
const utils_1 = require("../utils");
vitest_1.vi.mock('../storage');
vitest_1.vi.mock('../utils');
(0, vitest_1.describe)('SettingsService', () => {
let settingsService;
let mockStorageManager;
let mockSleepBlocker;
(0, vitest_1.beforeEach)(() => {
vitest_1.vi.clearAllMocks();
mockStorageManager = {
getItems: vitest_1.vi.fn().mockResolvedValue({
runInBackground: String(process.platform !== 'win32'),
keepComputerAwake: 'false',
}),
onDidChange: vitest_1.vi.fn().mockReturnValue({ dispose: vitest_1.vi.fn() }),
};
mockSleepBlocker = {
shouldKeepComputerAwake: vitest_1.vi.fn(),
};
vitest_1.vi.mocked(utils_1.SleepBlocker.getInstance).mockReturnValue(mockSleepBlocker);
settingsService = new settingsService_1.SettingsService(mockStorageManager);
});
(0, vitest_1.it)('should return defaults when storage is empty', async () => {
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(true);
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(false);
});
(0, vitest_1.it)('should return values from storage', async () => {
mockStorageManager.getItems.mockResolvedValue({
runInBackground: 'false',
keepComputerAwake: 'true',
});
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false);
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(true);
});
(0, vitest_1.it)('should return updated value after storage change', async () => {
mockStorageManager.getItems.mockResolvedValue({
[settingsService_1.SettingKey.RUN_IN_BACKGROUND]: 'false',
});
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false);
});
(0, vitest_1.it)('should trigger SleepBlocker on keepComputerAwake change', async () => {
let changeListener;
mockStorageManager.onDidChange.mockImplementation((listener) => {
changeListener = listener;
return { dispose: vitest_1.vi.fn() };
});
// Instantiate again to trigger constructor with the new mock
settingsService = new settingsService_1.SettingsService(mockStorageManager);
// Simulate change
changeListener({ keepComputerAwake: 'true' });
(0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true);
});
(0, vitest_1.it)('should trigger initial SleepBlocker state', async () => {
mockStorageManager.getItems.mockResolvedValue({
keepComputerAwake: 'true',
});
settingsService = new settingsService_1.SettingsService(mockStorageManager);
await new Promise(process.nextTick);
(0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true);
});
});

1191
dist/services/translationProxy.js vendored Normal file

File diff suppressed because it is too large Load Diff