Initial Release: OpenQode Public Alpha v1.3
This commit is contained in:
535
backend-integration.js
Normal file
535
backend-integration.js
Normal 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;
|
||||
Reference in New Issue
Block a user