diff --git a/package.json b/package.json index de28e0d..d7c9990 100644 --- a/package.json +++ b/package.json @@ -83,5 +83,4 @@ } } } -} - +} \ No newline at end of file diff --git a/src/actions/log.ts b/src/actions/log.ts index 72a5803..90fb42d 100644 --- a/src/actions/log.ts +++ b/src/actions/log.ts @@ -98,7 +98,7 @@ export const gitLogAction: Action = { message: Memory, _state: State | undefined ): Promise => { - const text = message.content.text.toLowerCase(); + const text = (message.content.text || '').toLowerCase(); // Check for git log related keywords const hasLogKeywords = ( diff --git a/src/actions/status.ts b/src/actions/status.ts index 762f175..2f887aa 100644 --- a/src/actions/status.ts +++ b/src/actions/status.ts @@ -138,7 +138,7 @@ export const gitStatusAction: Action = { message: Memory, _state: State | undefined ): Promise => { - const text = message.content.text.toLowerCase(); + const text = (message.content.text || '').toLowerCase(); // Check for git status related keywords const hasStatusKeywords = ( diff --git a/src/index.ts b/src/index.ts index 66dc452..8028b24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ export { parseGitUrl, isSshUrl, isHttpsUrl, - + // Execution utilities redactUrl, redactArgs, @@ -90,7 +90,7 @@ export { parseLogOutput, parseBranchOutput, parseRemoteOutput, - + // State persistence utilities getGitStateCacheKey, createEmptyGitState, diff --git a/src/plugin.ts b/src/plugin.ts index fa9dec1..82058b4 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -12,6 +12,7 @@ import { GitService } from './services/git'; // Providers import { gitInstructionsProvider, + gitSettingsProvider, gitWorkingCopiesProvider, gitStatusProvider, gitBranchesProvider, @@ -96,7 +97,7 @@ export const gitPlugin: Plugin = { logger.info('[Git] Initializing plugin-git...'); // Log configuration (without sensitive values) - const configKeys = Object.keys(config).filter(k => + const configKeys = Object.keys(config).filter(k => !k.includes('TOKEN') && !k.includes('PASSWORD') ); if (configKeys.length > 0) { @@ -139,6 +140,7 @@ export const gitPlugin: Plugin = { // Providers - inject git state into agent context providers: [ gitInstructionsProvider, // Comprehensive usage instructions (highest priority) + gitSettingsProvider, // Current settings (non-sensitive) gitWorkingCopiesProvider, // Shows managed repositories gitStatusProvider, // Shows current repo status gitLogProvider, // Shows recent commit history diff --git a/src/providers/index.ts b/src/providers/index.ts index ca99f9e..dd22f74 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -3,6 +3,7 @@ */ export { gitInstructionsProvider } from './instructions'; +export { gitSettingsProvider } from './settings'; export { gitWorkingCopiesProvider } from './workingCopies'; export { gitStatusProvider } from './status'; export { gitBranchesProvider } from './branches'; diff --git a/src/providers/settings.ts b/src/providers/settings.ts new file mode 100644 index 0000000..94de703 --- /dev/null +++ b/src/providers/settings.ts @@ -0,0 +1,124 @@ +/** + * GIT_SETTINGS Provider + * + * Exposes current git plugin settings (non-sensitive only). + * Helps users understand their current configuration. + */ + +import type { + IAgentRuntime, + Memory, + Provider, + ProviderResult, + State, +} from '@elizaos/core'; +import { GitService } from '../services/git'; +import { getActiveRepository, getWorkingCopies } from '../utils/state'; + +export const gitSettingsProvider: Provider = { + name: 'GIT_SETTINGS', + description: 'Current git plugin configuration and settings (non-sensitive)', + + get: async ( + runtime: IAgentRuntime, + message: Memory, + _state?: State + ): Promise => { + const gitService = runtime.getService('git'); + + // Get non-sensitive settings + const allowedPath = runtime.getSetting('GIT_ALLOWED_PATH') || 'not restricted'; + const authorName = runtime.getSetting('GIT_AUTHOR_NAME') || 'not set'; + const authorEmail = runtime.getSetting('GIT_AUTHOR_EMAIL') || 'not set'; + const cloneTimeout = runtime.getSetting('GIT_CLONE_TIMEOUT') || '300'; + const workingCopiesDir = runtime.getSetting('GIT_WORKING_COPIES_DIR') || '~/.eliza/git'; + + // Check auth status without exposing credentials + const hasToken = !!runtime.getSetting('GIT_TOKEN'); + const hasUserPass = !!runtime.getSetting('GIT_USERNAME') && !!runtime.getSetting('GIT_PASSWORD'); + const authConfigured = hasToken || hasUserPass; + const authMethod = hasToken ? 'token' : hasUserPass ? 'username/password' : 'none'; + + // Get git CLI status + let gitVersion = 'unknown'; + let gitInstalled = false; + if (gitService) { + gitInstalled = await gitService.isGitInstalled(); + if (gitInstalled) { + gitVersion = (await gitService.getGitVersion()) || 'unknown'; + } + } + + // Get repository state + const activeRepo = await getActiveRepository(runtime, message.roomId); + const workingCopies = await getWorkingCopies(runtime, message.roomId); + + // Check for workspace integration + const workspaceService = runtime.getService('workspace'); + const usingWorkspace = !!workspaceService; + + const settings = { + gitInstalled, + gitVersion, + allowedPath, + workingCopiesDir, + cloneTimeoutSeconds: parseInt(cloneTimeout), + author: { + name: authorName, + email: authorEmail, + }, + authentication: { + configured: authConfigured, + method: authMethod, + }, + state: { + activeRepo: activeRepo + ? { + path: activeRepo.path, + branch: activeRepo.branch, + remote: activeRepo.remote, + } + : null, + workingCopiesCount: workingCopies.length, + }, + usingWorkspace, + }; + + const lines = [ + '## Git Plugin Settings', + '', + `**Git CLI:** ${settings.gitInstalled ? `Installed (${settings.gitVersion})` : 'Not installed'}`, + '', + '**Directories:**', + `- Allowed Path: \`${settings.allowedPath}\``, + `- Working Copies: \`${settings.workingCopiesDir}\``, + `- Using Workspace: ${settings.usingWorkspace ? 'Yes' : 'No'}`, + '', + '**Clone Timeout:** ' + settings.cloneTimeoutSeconds + 's', + '', + '**Author:**', + `- Name: ${settings.author.name}`, + `- Email: ${settings.author.email}`, + '', + '**Authentication:**', + `- Configured: ${settings.authentication.configured ? 'Yes' : 'No'}`, + `- Method: ${settings.authentication.method}`, + '', + '**State:**', + `- Working Copies: ${settings.state.workingCopiesCount}`, + ]; + + if (settings.state.activeRepo) { + lines.push(`- Active Repo: \`${settings.state.activeRepo.path}\``); + lines.push(`- Branch: ${settings.state.activeRepo.branch}`); + } else { + lines.push('- Active Repo: None'); + } + + return { + text: lines.join('\n'), + values: settings, + data: { settings }, + }; + }, +}; diff --git a/src/services/git.ts b/src/services/git.ts index 7c58193..1625369 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -57,7 +57,7 @@ export class GitService extends Service { constructor(runtime?: IAgentRuntime) { super(runtime); - + // Load configuration from runtime settings this.gitConfig = { allowedPath: runtime?.getSetting('GIT_ALLOWED_PATH') as string | undefined, @@ -77,19 +77,19 @@ export class GitService extends Service { */ static async start(runtime: IAgentRuntime): Promise { logger.info('[Git] Starting git service...'); - + // Check if git is available const available = await isGitAvailable(); if (!available) { throw new Error('Git CLI not found. Please install git to use this plugin.'); } - + const version = await getGitVersion(); logger.info(`[Git] Git version: ${version}`); - + const service = new GitService(runtime); service.gitVersion = version; - + logger.info('[Git] Git service started'); return service; } @@ -101,6 +101,32 @@ export class GitService extends Service { logger.info('[Git] Git service stopped'); } + /** + * Checks if git CLI is installed on the system. + * Uses cached value from service startup, with fallback to live check. + */ + async isGitInstalled(): Promise { + // If we already determined git is available at startup + if (this.gitVersion !== null) { + return true; + } + // Fallback to live check + return await isGitAvailable(); + } + + /** + * Gets the git version string. + * Returns cached version from service startup if available. + */ + async getGitVersion(): Promise { + // Return cached version if available + if (this.gitVersion !== null) { + return this.gitVersion; + } + // Fallback to live check + return await getGitVersion(); + } + /** * Gets the git configuration. */ @@ -136,6 +162,63 @@ export class GitService extends Service { return isGitRepository(path); } + /** + * Gets the active workspace path from plugin-workspace if available. + * + * PROGRESSIVE ENHANCEMENT: + * - If plugin-workspace is loaded AND has an active workspace, return that path + * - Otherwise, return null (caller should use explicit path or GIT_ALLOWED_PATH) + * + * This allows plugin-git to work standalone OR with workspace management. + * + * @param conversationId - The conversation/room ID to check for active workspace + * @returns The active workspace path, or null if none + */ + getActiveWorkspacePath(conversationId: string): string | null { + if (!this.runtime) return null; + + try { + const workspaceService = this.runtime.getService('workspace'); + if (workspaceService && typeof workspaceService.getActiveWorkspace === 'function') { + const activeWorkspace = workspaceService.getActiveWorkspace(conversationId); + if (activeWorkspace?.path) { + logger.debug(`[Git] Using active workspace: ${activeWorkspace.name} (${activeWorkspace.path})`); + return activeWorkspace.path; + } + } + } catch { + // plugin-workspace not available + } + + return null; + } + + /** + * Resolves the target path for a git operation. + * + * Priority: + * 1. Explicit path provided by user + * 2. Active workspace from plugin-workspace + * 3. GIT_ALLOWED_PATH setting + * 4. null (caller should request a path) + */ + resolveTargetPath(explicitPath: string | undefined, conversationId: string): string | null { + if (explicitPath) { + return explicitPath; + } + + const workspacePath = this.getActiveWorkspacePath(conversationId); + if (workspacePath) { + return workspacePath; + } + + if (this.gitConfig.allowedPath) { + return this.gitConfig.allowedPath; + } + + return null; + } + /** * Injects authentication into a URL if configured. */ @@ -144,15 +227,15 @@ export class GitService extends Service { if (isSshUrl(url)) { return url; } - + if (this.gitConfig.token) { return injectToken(url, this.gitConfig.token); } - + if (this.gitConfig.username && this.gitConfig.password) { return injectCredentials(url, this.gitConfig.username, this.gitConfig.password); } - + return url; } @@ -161,17 +244,17 @@ export class GitService extends Service { */ private getGitEnv(): Record { const env: Record = {}; - + if (this.gitConfig.authorName) { env.GIT_AUTHOR_NAME = this.gitConfig.authorName; env.GIT_COMMITTER_NAME = this.gitConfig.authorName; } - + if (this.gitConfig.authorEmail) { env.GIT_AUTHOR_EMAIL = this.gitConfig.authorEmail; env.GIT_COMMITTER_EMAIL = this.gitConfig.authorEmail; } - + return env; } @@ -182,7 +265,7 @@ export class GitService extends Service { */ async clone(options: CloneOptions): Promise { const { url, branch, shallow, depth, path: customPath } = options; - + // Determine destination path let destination: string; if (customPath) { @@ -197,7 +280,7 @@ export class GitService extends Service { destination = `${this.getReposDir()}/unknown/${hash}`; } } - + // Validate destination path const validation = this.validatePath(destination); if (!validation.valid) { @@ -209,12 +292,12 @@ export class GitService extends Service { error: validation.error, }; } - + // Inject authentication const authUrl = this.injectAuth(url); - + logger.info(`[Git] Cloning ${redactUrl(url)} to ${destination}`); - + const result = await safeClone(authUrl, destination, { branch, shallow, @@ -222,7 +305,7 @@ export class GitService extends Service { env: this.getGitEnv(), timeout: this.gitConfig.cloneTimeout, }); - + if (!result.success) { logger.error(`[Git] Clone failed: ${result.stderr}`); return { @@ -233,10 +316,10 @@ export class GitService extends Service { error: result.stderr || 'Clone failed', }; } - + // Get the actual branch that was checked out const branchResult = await this.getCurrentBranch(destination); - + return { success: true, path: destination, @@ -253,18 +336,18 @@ export class GitService extends Service { if (!validation.valid) { return { success: false, error: validation.error }; } - + const args = ['init']; if (options?.initialBranch) { args.push('--initial-branch', options.initialBranch); } - + const result = await safeSpawn(args, { cwd: path, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -279,24 +362,24 @@ export class GitService extends Service { logger.error(`[Git] Path validation failed: ${validation.error}`); return null; } - + if (!isGitRepository(repoPath)) { logger.error(`[Git] Not a git repository: ${repoPath}`); return null; } - + const result = await safeSpawn( ['status', '--porcelain=v2', '--branch', '--untracked-files=all'], { cwd: repoPath } ); - + if (!result.success) { logger.error(`[Git] Failed to get status: ${result.stderr}`); return null; } - + const parsed = parseStatusOutput(result.stdout); - + return { branch: parsed.branch, upstream: parsed.upstream, @@ -327,11 +410,11 @@ export class GitService extends Service { ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoPath } ); - + if (!result.success) { return null; } - + return result.stdout.trim(); } @@ -343,11 +426,11 @@ export class GitService extends Service { ['rev-parse', 'HEAD'], { cwd: repoPath } ); - + if (!result.success) { return null; } - + return result.stdout.trim(); } @@ -362,18 +445,18 @@ export class GitService extends Service { '--format=%H|%h|%s|%an|%ae|%aI', `-n${options?.count || 10}`, ]; - + if (options?.branch) { args.push(options.branch); } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { logger.error(`[Git] Failed to get log: ${result.stderr}`); return []; } - + return parseLogOutput(result.stdout).map(entry => ({ ...entry, hashShort: entry.hashShort, @@ -387,12 +470,12 @@ export class GitService extends Service { */ async getBranches(repoPath: string): Promise { const result = await safeSpawn(['branch', '-a'], { cwd: repoPath }); - + if (!result.success) { logger.error(`[Git] Failed to get branches: ${result.stderr}`); return []; } - + return parseBranchOutput(result.stdout); } @@ -405,17 +488,17 @@ export class GitService extends Service { options?: { startPoint?: string } ): Promise<{ success: boolean; error?: string }> { const args = ['branch', branchName]; - + if (options?.startPoint) { args.push(options.startPoint); } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -429,11 +512,11 @@ export class GitService extends Service { ): Promise<{ success: boolean; error?: string }> { const flag = options?.force ? '-D' : '-d'; const result = await safeSpawn(['branch', flag, branchName], { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -442,27 +525,27 @@ export class GitService extends Service { */ async checkout(repoPath: string, options: CheckoutOptions): Promise<{ success: boolean; error?: string }> { const args = ['checkout']; - + if (options.create) { args.push('-b'); } - + if (options.force) { args.push('-f'); } - + args.push(options.branch); - + if (options.startPoint && options.create) { args.push(options.startPoint); } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -473,11 +556,11 @@ export class GitService extends Service { */ async getRemotes(repoPath: string): Promise { const result = await safeSpawn(['remote', '-v'], { cwd: repoPath }); - + if (!result.success) { return []; } - + return parseRemoteOutput(result.stdout); } @@ -489,25 +572,25 @@ export class GitService extends Service { options?: { remote?: string; branch?: string; prune?: boolean } ): Promise<{ success: boolean; error?: string }> { const args = ['fetch']; - + if (options?.prune) { args.push('--prune'); } - + if (options?.remote) { args.push(options.remote); - + if (options?.branch) { args.push(options.branch); } } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -516,21 +599,21 @@ export class GitService extends Service { */ async pull(repoPath: string, options?: PullOptions): Promise { const args = ['pull']; - + if (options?.rebase) { args.push('--rebase'); } - + if (options?.remote) { args.push(options.remote); - + if (options?.branch) { args.push(options.branch); } } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + // Check for conflicts if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) { const status = await this.getStatus(repoPath); @@ -547,7 +630,7 @@ export class GitService extends Service { error: 'Merge conflicts detected', }; } - + if (!result.success) { return { success: false, @@ -556,11 +639,11 @@ export class GitService extends Service { error: result.stderr, }; } - + // Check if already up to date - const upToDate = result.stdout.includes('Already up to date') || - result.stdout.includes('Already up-to-date'); - + const upToDate = result.stdout.includes('Already up to date') || + result.stdout.includes('Already up-to-date'); + return { success: true, commitsPulled: upToDate ? 0 : 1, // Simplified @@ -573,20 +656,20 @@ export class GitService extends Service { */ async push(repoPath: string, options?: PushOptions): Promise { const args = ['push']; - + if (options?.setUpstream) { args.push('-u'); } - + const remote = options?.remote || 'origin'; args.push(remote); - + if (options?.branch) { args.push(options.branch); } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, @@ -596,10 +679,10 @@ export class GitService extends Service { error: result.stderr, }; } - - const newBranch = result.stderr.includes('new branch') || - result.stdout.includes('new branch'); - + + const newBranch = result.stderr.includes('new branch') || + result.stdout.includes('new branch'); + return { success: true, remote, @@ -619,7 +702,7 @@ export class GitService extends Service { options?: { all?: boolean } ): Promise<{ success: boolean; error?: string }> { const args = ['add']; - + if (options?.all) { args.push('-A'); } else if (files && files.length > 0) { @@ -627,13 +710,13 @@ export class GitService extends Service { } else { args.push('.'); } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -642,17 +725,17 @@ export class GitService extends Service { */ async unstage(repoPath: string, files?: string[]): Promise<{ success: boolean; error?: string }> { const args = ['reset', 'HEAD']; - + if (files && files.length > 0) { args.push('--', ...files); } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -663,24 +746,24 @@ export class GitService extends Service { */ async commit(repoPath: string, options: CommitOptions): Promise<{ success: boolean; hash?: string; error?: string }> { const args = ['commit', '-m', options.message]; - + if (options.amend) { args.push('--amend'); } - + if (options.allowEmpty) { args.push('--allow-empty'); } - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + // Get the commit hash const hash = await this.getCurrentCommit(repoPath); - + return { success: true, hash: hash || undefined }; } @@ -701,25 +784,25 @@ export class GitService extends Service { error: result.success ? undefined : result.stderr, }; } - + const args = ['merge']; - + if (options.noFf) { args.push('--no-ff'); } - + if (options.squash) { args.push('--squash'); } - + if (options.message) { args.push('-m', options.message); } - + args.push(options.branch); - + const result = await safeSpawn(args, { cwd: repoPath, env: this.getGitEnv() }); - + // Check for conflicts if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) { const status = await this.getStatus(repoPath); @@ -731,7 +814,7 @@ export class GitService extends Service { error: 'Merge conflicts detected', }; } - + if (!result.success) { return { success: false, @@ -741,10 +824,10 @@ export class GitService extends Service { error: result.stderr, }; } - + const fastForward = result.stdout.includes('Fast-forward'); const mergeCommit = await this.getCurrentCommit(repoPath); - + return { success: true, fastForward, @@ -761,7 +844,7 @@ export class GitService extends Service { */ async stash(repoPath: string, options: StashOptions): Promise<{ success: boolean; error?: string; list?: string[] }> { const args = ['stash']; - + switch (options.action) { case 'push': args.push('push'); @@ -772,44 +855,44 @@ export class GitService extends Service { args.push('-m', options.message); } break; - + case 'pop': args.push('pop'); if (options.index !== undefined) { args.push(`stash@{${options.index}}`); } break; - + case 'apply': args.push('apply'); if (options.index !== undefined) { args.push(`stash@{${options.index}}`); } break; - + case 'drop': args.push('drop'); if (options.index !== undefined) { args.push(`stash@{${options.index}}`); } break; - + case 'list': args.push('list'); break; } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + if (options.action === 'list') { const list = result.stdout.split('\n').filter(line => line.trim()); return { success: true, list }; } - + return { success: true }; } @@ -820,21 +903,21 @@ export class GitService extends Service { */ async reset(repoPath: string, options: ResetOptions): Promise<{ success: boolean; error?: string }> { const args = ['reset', `--${options.mode}`]; - + if (options.ref) { args.push(options.ref); } - + if (options.files && options.files.length > 0) { args.push('--', ...options.files); } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true }; } @@ -848,25 +931,25 @@ export class GitService extends Service { options?: { staged?: boolean; file?: string; stat?: boolean } ): Promise<{ success: boolean; diff?: string; error?: string }> { const args = ['diff']; - + if (options?.staged) { args.push('--staged'); } - + if (options?.stat) { args.push('--stat'); } - + if (options?.file) { args.push('--', options.file); } - + const result = await safeSpawn(args, { cwd: repoPath }); - + if (!result.success) { return { success: false, error: result.stderr }; } - + return { success: true, diff: result.stdout }; } } diff --git a/src/utils/state.ts b/src/utils/state.ts index 98c6dc0..52d5b18 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -237,27 +237,32 @@ export async function removeWorkingCopy( roomId?: UUID ): Promise { const existingState = await loadGitState(runtime, roomId) || createEmptyGitState(); - + const { [path]: removed, ...remainingCopies } = existingState.workingCopies; - + const newState: GitState = { ...existingState, workingCopies: remainingCopies, updatedAt: new Date().toISOString(), }; - + // Clear active repo if it was the removed one if (existingState.activeRepository?.path === path) { delete newState.activeRepository; } - + await saveGitState(runtime, newState, roomId); return newState; } /** * Gets the active repository from state. - * Falls back to most recently accessed working copy if no active repo set. + * + * PROGRESSIVE ENHANCEMENT: + * Priority: + * 1. Explicit active repo from plugin-git state + * 2. Active workspace from plugin-workspace (if loaded and is a git repo) + * 3. Most recently cloned working copy from plugin-git state * * @param runtime - Agent runtime * @param roomId - Optional room ID @@ -268,27 +273,48 @@ export async function getActiveRepository( roomId?: UUID ): Promise { const state = await loadGitState(runtime, roomId); - + + // 1. Return explicit active repo if set in plugin-git state + if (state?.activeRepository) { + return state.activeRepository; + } + + // 2. Check for plugin-workspace active workspace + try { + const workspaceService = runtime.getService('workspace'); + if (workspaceService && typeof workspaceService.getActiveWorkspace === 'function') { + const conversationId = roomId ?? runtime.agentId; + const activeWorkspace = workspaceService.getActiveWorkspace(conversationId); + + if (activeWorkspace?.path && activeWorkspace.source?.type === 'git') { + logger.debug(`[Git] Using active workspace as repository: ${activeWorkspace.name}`); + return { + path: activeWorkspace.path, + branch: activeWorkspace.source.branch || 'main', + remote: activeWorkspace.source.url, + lastAccessed: activeWorkspace.lastAccessed?.toISOString?.() || new Date().toISOString(), + }; + } + } + } catch { + // plugin-workspace not available + } + + // 3. Fall back to most recently cloned working copy from plugin-git state if (!state) { return null; } - - // Return explicit active repo if set - if (state.activeRepository) { - return state.activeRepository; - } - - // Fall back to most recently cloned working copy + const workingCopies = Object.values(state.workingCopies); if (workingCopies.length === 0) { return null; } - + // Sort by clonedAt descending - workingCopies.sort((a, b) => + workingCopies.sort((a, b) => new Date(b.clonedAt).getTime() - new Date(a.clonedAt).getTime() ); - + const mostRecent = workingCopies[0]; return { path: mostRecent.path,