recoder-shared
Version:
Shared types, utilities, and configurations for Recoder
677 lines (586 loc) • 21.3 kB
text/typescript
/**
* User Migration System for Recoder.xyz Ecosystem
*
* Handles automated migration from LlamaCoder, KiloCode, and other AI coding tools
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { UnifiedConfigManager } from '../config/unified-config';
import { SessionManager } from '../session/session-manager';
export interface MigrationSource {
name: string;
displayName: string;
version?: string;
configPath: string;
dataPath: string;
supported: boolean;
}
export interface MigrationData {
source: MigrationSource;
config: any;
sessions: any[];
projects: any[];
settings: any;
apiKeys: Record<string, string>;
customPrompts: any[];
extensions: any[];
metadata: {
exportDate: string;
sourceVersion: string;
migrationVersion: string;
};
}
export interface MigrationResult {
success: boolean;
source: string;
migratedItems: {
config: boolean;
sessions: number;
projects: number;
apiKeys: number;
customPrompts: number;
};
warnings: string[];
errors: string[];
postMigrationSteps: string[];
}
export class UserMigrationSystem {
private static instance: UserMigrationSystem;
private configManager: UnifiedConfigManager;
private sessionManager: SessionManager;
private supportedSources: Map<string, MigrationSource> = new Map();
private constructor(platform: 'cli' | 'web' | 'extension') {
this.configManager = UnifiedConfigManager.getInstance(platform);
this.sessionManager = SessionManager.getInstance(platform);
this.initializeSupportedSources();
}
static getInstance(platform: 'cli' | 'web' | 'extension'): UserMigrationSystem {
if (!UserMigrationSystem.instance) {
UserMigrationSystem.instance = new UserMigrationSystem(platform);
}
return UserMigrationSystem.instance;
}
/**
* Initialize supported migration sources
*/
private initializeSupportedSources(): void {
const homeDir = os.homedir();
this.supportedSources.set('llamacoder', {
name: 'llamacoder',
displayName: 'LlamaCoder',
configPath: path.join(homeDir, '.llamacoder', 'config.json'),
dataPath: path.join(homeDir, '.llamacoder', 'projects'),
supported: true
});
this.supportedSources.set('kilocode', {
name: 'kilocode',
displayName: 'KiloCode (Cline/Roo)',
configPath: path.join(homeDir, '.vscode', 'extensions'),
dataPath: path.join(homeDir, '.kilocode'),
supported: true
});
this.supportedSources.set('cursor', {
name: 'cursor',
displayName: 'Cursor IDE',
configPath: path.join(homeDir, '.cursor', 'config.json'),
dataPath: path.join(homeDir, '.cursor', 'conversations'),
supported: true
});
this.supportedSources.set('github-copilot', {
name: 'github-copilot',
displayName: 'GitHub Copilot',
configPath: path.join(homeDir, '.vscode', 'extensions', 'github.copilot*'),
dataPath: path.join(homeDir, '.vscode', 'User', 'settings.json'),
supported: false // Read-only compatibility
});
this.supportedSources.set('claude-dev', {
name: 'claude-dev',
displayName: 'Claude Dev Extension',
configPath: path.join(homeDir, '.vscode', 'extensions', 'saoudrizwan.claude-dev*'),
dataPath: path.join(homeDir, '.claude-dev'),
supported: true
});
}
/**
* Detect available migration sources
*/
async detectSources(): Promise<MigrationSource[]> {
const detectedSources: MigrationSource[] = [];
for (const [name, source] of this.supportedSources) {
try {
const exists = await this.checkSourceExists(source);
if (exists) {
const version = await this.detectSourceVersion(source);
detectedSources.push({
...source,
version
});
}
} catch (error) {
console.warn(`Failed to detect ${name}:`, error);
}
}
return detectedSources;
}
/**
* Check if migration source exists
*/
private async checkSourceExists(source: MigrationSource): Promise<boolean> {
try {
// Check config path
if (fs.existsSync(source.configPath)) {
return true;
}
// Check data path
if (fs.existsSync(source.dataPath)) {
return true;
}
// Special handling for VS Code extensions
if (source.configPath.includes('extensions')) {
const extensionsDir = path.dirname(source.configPath);
if (fs.existsSync(extensionsDir)) {
const extensions = fs.readdirSync(extensionsDir);
const extensionPattern = path.basename(source.configPath);
return extensions.some(ext => ext.includes(extensionPattern.replace('*', '')));
}
}
return false;
} catch (error) {
return false;
}
}
/**
* Detect source version
*/
private async detectSourceVersion(source: MigrationSource): Promise<string | undefined> {
try {
switch (source.name) {
case 'llamacoder':
return this.detectLlamaCoderVersion(source);
case 'kilocode':
return this.detectKiloCodeVersion(source);
case 'cursor':
return this.detectCursorVersion(source);
default:
return undefined;
}
} catch (error) {
return undefined;
}
}
/**
* Export data from source
*/
async exportFromSource(sourceName: string): Promise<MigrationData> {
const source = this.supportedSources.get(sourceName);
if (!source) {
throw new Error(`Unsupported migration source: ${sourceName}`);
}
if (!source.supported) {
throw new Error(`Migration from ${source.displayName} is not supported`);
}
switch (sourceName) {
case 'llamacoder':
return this.exportLlamaCoderData(source);
case 'kilocode':
return this.exportKiloCodeData(source);
case 'cursor':
return this.exportCursorData(source);
case 'claude-dev':
return this.exportClaudeDevData(source);
default:
throw new Error(`Export not implemented for ${sourceName}`);
}
}
/**
* Import migration data to Recoder
*/
async importMigrationData(migrationData: MigrationData): Promise<MigrationResult> {
const result: MigrationResult = {
success: false,
source: migrationData.source.displayName,
migratedItems: {
config: false,
sessions: 0,
projects: 0,
apiKeys: 0,
customPrompts: 0
},
warnings: [],
errors: [],
postMigrationSteps: []
};
try {
// Migrate configuration
await this.migrateConfiguration(migrationData, result);
// Migrate API keys
await this.migrateApiKeys(migrationData, result);
// Migrate sessions
await this.migrateSessions(migrationData, result);
// Migrate projects
await this.migrateProjects(migrationData, result);
// Migrate custom prompts
await this.migrateCustomPrompts(migrationData, result);
result.success = true;
result.postMigrationSteps = this.generatePostMigrationSteps(migrationData);
} catch (error) {
result.errors.push(`Migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
result.success = false;
}
return result;
}
/**
* Migrate configuration settings
*/
private async migrateConfiguration(migrationData: MigrationData, result: MigrationResult): Promise<void> {
try {
const currentConfig = this.configManager.getConfig();
const sourceConfig = migrationData.config;
// Map common settings
const mappedConfig: any = {};
switch (migrationData.source.name) {
case 'llamacoder':
mappedConfig.defaultProvider = this.mapProvider(sourceConfig.defaultModel);
mappedConfig.projectDefaults = {
language: sourceConfig.defaultLanguage || 'typescript',
includeTests: sourceConfig.generateTests || true,
includeDocs: sourceConfig.generateDocs || false
};
break;
case 'kilocode':
mappedConfig.defaultProvider = this.mapProvider(sourceConfig.selectedProvider);
mappedConfig.qualityValidation = sourceConfig.enableValidation !== false;
mappedConfig.extension = {
autoActivate: sourceConfig.autoActivate !== false,
showInlineHints: sourceConfig.showHints !== false
};
break;
case 'cursor':
mappedConfig.defaultProvider = 'claude'; // Cursor primarily uses Claude
mappedConfig.features = {
sessionSharing: true,
crossPlatformSync: true
};
break;
}
// Merge with current configuration
const mergedConfig = {
...currentConfig,
...mappedConfig
};
// Update configuration (this will save automatically)
Object.entries(mappedConfig).forEach(([key, value]) => {
if (key === 'extension') {
this.configManager.updatePlatformConfig(value as any);
} else if (key === 'projectDefaults') {
this.configManager.updateProjectDefaults(value as any);
}
});
result.migratedItems.config = true;
} catch (error) {
result.warnings.push(`Configuration migration partially failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Migrate API keys
*/
private async migrateApiKeys(migrationData: MigrationData, result: MigrationResult): Promise<void> {
try {
let keyCount = 0;
for (const [provider, apiKey] of Object.entries(migrationData.apiKeys)) {
if (apiKey && typeof apiKey === 'string' && apiKey.length > 0) {
const mappedProvider = this.mapProvider(provider);
if (mappedProvider) {
this.configManager.setAPIKey(mappedProvider, apiKey);
keyCount++;
}
}
}
result.migratedItems.apiKeys = keyCount;
if (keyCount === 0) {
result.warnings.push('No API keys were found to migrate. You will need to configure them manually.');
result.postMigrationSteps.push('Configure API keys: recoder providers --set-key <provider>');
}
} catch (error) {
result.warnings.push(`API key migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Migrate sessions
*/
private async migrateSessions(migrationData: MigrationData, result: MigrationResult): Promise<void> {
try {
let sessionCount = 0;
for (const sessionData of migrationData.sessions) {
try {
// Convert session format
const convertedSession = this.convertSessionFormat(sessionData, migrationData.source.name);
// Import session
this.sessionManager.importSession(JSON.stringify(convertedSession));
sessionCount++;
} catch (error) {
result.warnings.push(`Failed to migrate session "${sessionData.title || 'Untitled'}": ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
result.migratedItems.sessions = sessionCount;
} catch (error) {
result.warnings.push(`Session migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Migrate projects
*/
private async migrateProjects(migrationData: MigrationData, result: MigrationResult): Promise<void> {
try {
let projectCount = 0;
for (const projectData of migrationData.projects) {
try {
// Convert project format
const convertedProject = this.convertProjectFormat(projectData, migrationData.source.name);
// Create session for project
const session = this.sessionManager.createSession(
convertedProject.title || 'Migrated Project',
{
projectPath: convertedProject.path,
framework: convertedProject.framework
}
);
// Add project content as messages
if (convertedProject.files && convertedProject.files.length > 0) {
for (const file of convertedProject.files) {
this.sessionManager.addMessage(
`File: ${file.name}\n\`\`\`${file.language || 'text'}\n${file.content}\n\`\`\``,
'system'
);
}
}
projectCount++;
} catch (error) {
result.warnings.push(`Failed to migrate project "${projectData.name || 'Untitled'}": ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
result.migratedItems.projects = projectCount;
} catch (error) {
result.warnings.push(`Project migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Migrate custom prompts
*/
private async migrateCustomPrompts(migrationData: MigrationData, result: MigrationResult): Promise<void> {
try {
let promptCount = 0;
// This would integrate with a custom prompts system
// For now, we'll save them as part of configuration
if (migrationData.customPrompts && migrationData.customPrompts.length > 0) {
// Save custom prompts in user configuration
// Implementation would depend on how custom prompts are stored in Recoder
promptCount = migrationData.customPrompts.length;
result.postMigrationSteps.push(`${promptCount} custom prompts saved. Access them via 'recoder prompts --list'`);
}
result.migratedItems.customPrompts = promptCount;
} catch (error) {
result.warnings.push(`Custom prompt migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Generate post-migration steps
*/
private generatePostMigrationSteps(migrationData: MigrationData): string[] {
const steps: string[] = [];
steps.push('Run "recoder config --validate" to verify your configuration');
steps.push('Test AI providers with "recoder providers --test"');
steps.push('Explore new features with "recoder --help"');
switch (migrationData.source.name) {
case 'llamacoder':
steps.push('Try the new multi-AI routing: "recoder generate --auto-provider"');
steps.push('Explore real code generation: "recoder generate --real-code"');
break;
case 'kilocode':
steps.push('Enable cross-platform sync: "recoder session --sync"');
steps.push('Try the new ghost mode in VS Code');
break;
case 'cursor':
steps.push('Import your conversation history via the web platform');
steps.push('Set up the VS Code extension for enhanced IDE integration');
break;
}
steps.push('Join our community: https://discord.gg/recoder');
steps.push('Read the full documentation: https://docs.recoder.xyz');
return steps;
}
// Helper methods for specific source migrations
private async detectLlamaCoderVersion(source: MigrationSource): Promise<string | undefined> {
try {
const packagePath = path.join(path.dirname(source.configPath), 'package.json');
if (fs.existsSync(packagePath)) {
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
return packageData.version;
}
} catch (error) {
// Ignore
}
return undefined;
}
private async detectKiloCodeVersion(source: MigrationSource): Promise<string | undefined> {
try {
const extensionsDir = path.dirname(source.configPath);
const extensions = fs.readdirSync(extensionsDir);
const kiloExtension = extensions.find(ext => ext.includes('kilo-code') || ext.includes('kilocode'));
if (kiloExtension) {
const packagePath = path.join(extensionsDir, kiloExtension, 'package.json');
if (fs.existsSync(packagePath)) {
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
return packageData.version;
}
}
} catch (error) {
// Ignore
}
return undefined;
}
private async detectCursorVersion(source: MigrationSource): Promise<string | undefined> {
// Cursor version detection would be implemented here
return undefined;
}
private async exportLlamaCoderData(source: MigrationSource): Promise<MigrationData> {
// LlamaCoder-specific export implementation
const migrationData: MigrationData = {
source,
config: {},
sessions: [],
projects: [],
settings: {},
apiKeys: {},
customPrompts: [],
extensions: [],
metadata: {
exportDate: new Date().toISOString(),
sourceVersion: source.version || 'unknown',
migrationVersion: '1.0.0'
}
};
// Implementation would load actual LlamaCoder data
return migrationData;
}
private async exportKiloCodeData(source: MigrationSource): Promise<MigrationData> {
// KiloCode-specific export implementation
const migrationData: MigrationData = {
source,
config: {},
sessions: [],
projects: [],
settings: {},
apiKeys: {},
customPrompts: [],
extensions: [],
metadata: {
exportDate: new Date().toISOString(),
sourceVersion: source.version || 'unknown',
migrationVersion: '1.0.0'
}
};
// Implementation would load actual KiloCode data
return migrationData;
}
private async exportCursorData(source: MigrationSource): Promise<MigrationData> {
// Cursor-specific export implementation
const migrationData: MigrationData = {
source,
config: {},
sessions: [],
projects: [],
settings: {},
apiKeys: {},
customPrompts: [],
extensions: [],
metadata: {
exportDate: new Date().toISOString(),
sourceVersion: source.version || 'unknown',
migrationVersion: '1.0.0'
}
};
return migrationData;
}
private async exportClaudeDevData(source: MigrationSource): Promise<MigrationData> {
// Claude Dev extension export implementation
const migrationData: MigrationData = {
source,
config: {},
sessions: [],
projects: [],
settings: {},
apiKeys: {},
customPrompts: [],
extensions: [],
metadata: {
exportDate: new Date().toISOString(),
sourceVersion: source.version || 'unknown',
migrationVersion: '1.0.0'
}
};
return migrationData;
}
private mapProvider(sourceProvider: string): string {
const providerMap: Record<string, string> = {
'claude': 'claude',
'claude-3': 'claude',
'gpt-4': 'openai',
'gpt-3.5': 'openai',
'llama': 'groq',
'llama2': 'groq',
'llama3': 'groq',
'gemini': 'gemini',
'gemini-pro': 'gemini',
'ollama': 'ollama'
};
return providerMap[sourceProvider] || 'claude';
}
private convertSessionFormat(sessionData: any, sourceName: string): any {
// Convert session data to Recoder format
// Implementation would vary based on source format
return {
id: sessionData.id || `migrated-${Date.now()}`,
title: sessionData.title || sessionData.name || 'Migrated Session',
createdAt: new Date(sessionData.createdAt || sessionData.created || Date.now()).getTime(),
updatedAt: new Date(sessionData.updatedAt || sessionData.modified || Date.now()).getTime(),
platform: 'cli',
messages: this.convertMessages(sessionData.messages || sessionData.history || []),
context: sessionData.context || {},
metadata: {
totalTokens: sessionData.totalTokens || 0,
aiProviders: sessionData.providers || ['claude'],
codeGenerated: sessionData.codeBlocks || 0,
filesModified: sessionData.files || []
}
};
}
private convertProjectFormat(projectData: any, sourceName: string): any {
// Convert project data to Recoder format
return {
id: projectData.id || `migrated-${Date.now()}`,
title: projectData.name || projectData.title || 'Migrated Project',
path: projectData.path || projectData.directory,
language: projectData.language || 'typescript',
framework: projectData.framework,
files: projectData.files || [],
createdAt: new Date(projectData.createdAt || Date.now()).getTime()
};
}
private convertMessages(messages: any[]): any[] {
return messages.map(msg => ({
id: msg.id || `msg-${Date.now()}-${Math.random()}`,
timestamp: new Date(msg.timestamp || msg.created || Date.now()).getTime(),
platform: 'cli',
type: msg.type || (msg.role === 'user' ? 'user' : 'assistant'),
content: msg.content || msg.text || msg.message || '',
metadata: msg.metadata || {}
}));
}
}
// Export factory functions for each platform
export const createCLIMigrationSystem = () => UserMigrationSystem.getInstance('cli');
export const createWebMigrationSystem = () => UserMigrationSystem.getInstance('web');
export const createExtensionMigrationSystem = () => UserMigrationSystem.getInstance('extension');
export default UserMigrationSystem;