Initial Release: OpenQode Public Alpha v1.3

This commit is contained in:
Gemini AI
2025-12-14 00:40:14 +04:00
Unverified
commit 8e8d80c110
119 changed files with 31174 additions and 0 deletions

535
backend-integration.js Normal file
View File

@@ -0,0 +1,535 @@
const { spawn, exec } = require('child_process');
const path = require('path');
const fs = require('fs').promises;
const os = require('os');
class OpenCodeBackend {
constructor() {
this.opencodePath = null;
this.isInitialized = false;
this.currentSession = null;
this.processes = new Map();
}
async initialize() {
try {
const isWindows = os.platform() === 'win32';
const binName = isWindows ? 'opencode.exe' : 'opencode';
// Try to find opencode binary in various locations
const possiblePaths = [
path.join(__dirname, 'bin', binName),
path.join(__dirname, binName),
binName, // Assume it's in PATH
path.join(os.homedir(), '.opencode', binName),
path.join(process.env.LOCALAPPDATA || (process.env.HOME + '/.local/share'), 'OpenCode', binName)
];
for (const opencodePath of possiblePaths) {
try {
await fs.access(opencodePath);
this.opencodePath = opencodePath;
console.log(`✅ Found OpenCode at: ${opencodePath}`);
break;
} catch (err) {
// Continue to next path
}
}
if (!this.opencodePath) {
throw new Error('OpenCode binary not found. Please ensure opencode.exe is available.');
}
// Test if OpenCode is working (direct call without initialization check)
await this.testOpenCode();
this.isInitialized = true;
console.log('✅ OpenCode backend initialized successfully');
return true;
} catch (error) {
console.error('❌ Failed to initialize OpenCode backend:', error.message);
return false;
}
}
async testOpenCode() {
return new Promise((resolve, reject) => {
const child = spawn(this.opencodePath, ['--version'], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: __dirname,
env: {
...process.env,
OPENCODE_NO_TELEMETRY: '1',
OPENCODE_LOG_LEVEL: 'ERROR',
FORCE_COLOR: '0' // Disable ANSI color codes
}
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve({ success: true, stdout: this.stripAnsiCodes(stdout).trim() });
} else {
reject(new Error(`OpenCode test failed with code ${code}: ${this.stripAnsiCodes(stderr)}`));
}
});
child.on('error', (error) => {
reject(error);
});
// Set timeout
const timeout = setTimeout(() => {
child.kill('SIGTERM');
reject(new Error('OpenCode test timed out'));
}, 10000);
child.on('close', () => {
clearTimeout(timeout);
});
});
}
async executeCommand(args, options = {}) {
if (!this.isInitialized) {
throw new Error('OpenCode backend not initialized');
}
return new Promise((resolve, reject) => {
const startTime = Date.now();
const child = spawn(this.opencodePath, args, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: options.cwd || __dirname,
env: {
...process.env,
OPENCODE_NO_TELEMETRY: '1',
OPENCODE_LOG_LEVEL: 'ERROR',
FORCE_COLOR: '0' // Disable ANSI color codes
},
...options
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
resolve({
code,
stdout: this.stripAnsiCodes(stdout).trim(),
stderr: this.stripAnsiCodes(stderr).trim(),
duration,
command: `${this.opencodePath} ${args.join(' ')}`
});
});
child.on('error', (error) => {
reject(error);
});
// Set timeout
if (options.timeout) {
setTimeout(() => {
child.kill('SIGTERM');
reject(new Error(`Command timed out after ${options.timeout}ms`));
}, options.timeout);
}
});
}
async checkAuth(provider = 'qwen') {
try {
// First check if credentials exist
const result = await this.executeCommand(['auth', 'list']);
const hasCredentials = result.stdout.includes(provider);
if (!hasCredentials) {
return {
authenticated: false,
details: 'No credentials found'
};
}
// Actually test the token by making a simple request
// Try running a minimal command to verify the token works
try {
const testResult = await this.executeCommand(['run', '-m', `${provider}/coder-model`, 'ping'], {
timeout: 15000 // 15 seconds timeout for token test
});
// Check if the response indicates token error
const output = testResult.stdout + testResult.stderr;
if (output.includes('invalid access token') || output.includes('token expired') || output.includes('unauthorized')) {
return {
authenticated: false,
tokenExpired: true,
details: 'Token expired or invalid'
};
}
return {
authenticated: true,
details: result.stdout
};
} catch (testError) {
// If test fails, might still be authenticated but network issue
return {
authenticated: true, // Assume true to not block, actual call will fail gracefully
details: result.stdout,
warning: 'Could not verify token validity'
};
}
} catch (error) {
return {
authenticated: false,
error: error.message
};
}
}
async authenticate(provider = 'qwen') {
try {
// For Qwen, we open the browser for OAuth
if (provider === 'qwen') {
// First try to logout to clear old tokens
try {
await this.executeCommand(['auth', 'logout', 'qwen'], { timeout: 5000 });
} catch (e) {
// Ignore logout errors
}
// Open qwen.ai for manual authentication
// The user needs to login at https://chat.qwen.ai and we'll use oauth
return {
success: true,
requiresBrowser: true,
browserUrl: 'https://chat.qwen.ai',
message: 'Please login at https://chat.qwen.ai in your browser, then click "Complete Auth"'
};
}
throw new Error(`Unsupported provider: ${provider}`);
} catch (error) {
return {
success: false,
error: error.message
};
}
}
buildRunArgs(message, model = 'qwen/coder-model', options = {}) {
const args = ['run'];
if (options.lakeview) {
args.push('--lakeview');
}
if (options.sequentialThinking) {
args.push('--think');
}
args.push('-m', model);
args.push(message);
return args;
}
async sendMessage(message, model = 'qwen/coder-model', options = {}) {
try {
// Use buildRunArgs to pass message directly as argument (non-interactive mode)
const args = this.buildRunArgs(message, model, options);
const sessionId = `session_${Date.now()}`;
this.currentSession = sessionId;
return new Promise((resolve, reject) => {
let child = null;
let response = '';
let errorOutput = '';
let timeoutHandle = null;
let settled = false;
const cleanup = () => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = null;
}
if (this.processes.has(sessionId)) {
const proc = this.processes.get(sessionId);
if (proc && !proc.killed) {
try {
proc.kill('SIGTERM');
} catch (e) {
// Process might already be dead
}
}
this.processes.delete(sessionId);
}
if (this.currentSession === sessionId) {
this.currentSession = null;
}
};
const finalize = (action) => {
if (settled) return;
settled = true;
cleanup();
action();
};
try {
child = spawn(this.opencodePath, args, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: __dirname,
env: {
...process.env,
OPENCODE_NO_TELEMETRY: '1',
OPENCODE_LOG_LEVEL: 'ERROR',
FORCE_COLOR: '0'
}
});
child.stdout.on('data', (data) => {
response += data.toString();
});
child.stderr.on('data', (data) => {
errorOutput += data.toString();
});
child.on('close', (code) => {
finalize(() => {
// Clean up ANSI codes from response
const cleanResponse = this.stripAnsiCodes(response.trim());
if (code === 0 || cleanResponse.length > 0) {
resolve({
success: true,
response: cleanResponse,
model,
sessionId
});
} else {
resolve({
success: false,
error: this.stripAnsiCodes(errorOutput) || `Process exited with code ${code}`,
model,
sessionId
});
}
});
});
child.on('error', (error) => {
finalize(() => reject(error));
});
this.processes.set(sessionId, child);
// Timeout - default 60 seconds for AI responses
timeoutHandle = setTimeout(() => {
if (child && !child.killed) {
child.kill('SIGTERM');
}
finalize(() => reject(new Error('Message processing timed out')));
}, options.timeout || 60000);
} catch (error) {
finalize(() => reject(error));
}
});
} catch (error) {
return {
success: false,
error: error.message,
model
};
}
}
async getAvailableModels() {
try {
const result = await this.executeCommand(['--help']);
// Parse the help output to extract available models
// This is a simplified approach - in reality, you might need to parse more carefully
const models = [
'qwen/coder-model',
'qwen/vision-model',
'gpt-4',
'gpt-3.5-turbo'
];
return {
success: true,
models
};
} catch (error) {
return {
success: false,
error: error.message,
models: []
};
}
}
async getStatus() {
try {
const authStatus = await this.checkAuth();
const versionResult = await this.executeCommand(['--version']);
return {
initialized: this.isInitialized,
opencodePath: this.opencodePath,
version: versionResult.stdout,
auth: authStatus,
currentSession: this.currentSession
};
} catch (error) {
return {
initialized: false,
error: error.message
};
}
}
async sendMessageStream(message, model = 'qwen/coder-model', options = {}) {
const args = this.buildRunArgs(message, model, options);
const sessionId = `session_${Date.now()}`;
this.currentSession = sessionId;
return new Promise((resolve, reject) => {
let child = null;
let response = '';
let errorOutput = '';
let timeoutHandle = null;
let settled = false;
const cleanup = () => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = null;
}
if (this.processes.has(sessionId)) {
const proc = this.processes.get(sessionId);
if (proc && !proc.killed) {
try {
proc.kill('SIGTERM');
} catch (e) {
// Process might already be dead
}
}
this.processes.delete(sessionId);
}
if (this.currentSession === sessionId) {
this.currentSession = null;
}
};
const finalize = (action) => {
if (settled) return;
settled = true;
cleanup();
action();
};
try {
child = spawn(this.opencodePath, args, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: __dirname,
env: {
...process.env,
OPENCODE_NO_TELEMETRY: '1',
OPENCODE_LOG_LEVEL: 'ERROR',
FORCE_COLOR: '0'
}
});
child.stdout.on('data', (data) => {
let chunk = data.toString();
chunk = this.stripAnsiCodes(chunk);
response += chunk;
if (options.onChunk) {
options.onChunk(chunk);
}
});
child.stderr.on('data', (data) => {
const errorData = data.toString();
errorOutput += errorData;
if (options.onError) {
options.onError(this.stripAnsiCodes(errorData));
}
});
child.on('close', (code) => {
finalize(() => {
if (code === 0) {
resolve({
success: true,
response: response.trim(),
sessionId
});
} else {
reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
}
});
});
child.on('error', (error) => {
finalize(() => reject(error));
});
this.processes.set(sessionId, child);
const timeoutMs = options.timeout || 300000; // Default to 5 minutes for AI responses
timeoutHandle = setTimeout(() => {
if (child && !child.killed) {
child.kill('SIGTERM');
}
finalize(() => reject(new Error(`Stream timed out after ${timeoutMs}ms`)));
}, timeoutMs);
} catch (error) {
finalize(() => reject(error));
}
});
}
async cleanup() {
// Kill any running processes
for (const [sessionId, process] of this.processes) {
try {
process.kill('SIGTERM');
} catch (error) {
// Process might already be dead
}
}
this.processes.clear();
this.currentSession = null;
}
stripAnsiCodes(str) {
// Comprehensive regular expression to match ANSI escape codes and terminal control sequences
return str.replace(/[\u001b\u009b][\[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|[\u001b\u009b][c-u w-y]|\u001b\][^\u0007]*\u0007/g, '');
}
}
module.exports = OpenCodeBackend;