diff --git a/packages/mcp/src/functions/create-session.ts b/packages/mcp/src/functions/create-session.ts index 4d899af..cf03d81 100644 --- a/packages/mcp/src/functions/create-session.ts +++ b/packages/mcp/src/functions/create-session.ts @@ -1,6 +1,32 @@ +import { execSync } from 'node:child_process'; import type { JulesClient, SessionConfig } from '@google/jules-sdk'; import type { CreateSessionResult, CreateSessionOptions } from './types.js'; +function detectGitRepo(): string | undefined { + try { + const url = execSync('git remote get-url origin', { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + const match = url.match(/github\.com[:/](.+?)(?:\.git)?$/); + return match?.[1] ?? undefined; + } catch { + return undefined; + } +} + +function detectGitBranch(): string | undefined { + try { + const branch = execSync('git branch --show-current', { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + return branch || undefined; + } catch { + return undefined; + } +} + /** * Create a new Jules session or automated run. * @@ -12,6 +38,9 @@ export async function createSession( client: JulesClient, options: CreateSessionOptions, ): Promise { + const repo = options.repo ?? detectGitRepo(); + const branch = options.branch ?? detectGitBranch(); + // Build config - source is optional for repoless sessions const config: SessionConfig = { prompt: options.prompt, @@ -20,9 +49,8 @@ export async function createSession( autoPr: options.autoPr !== undefined ? options.autoPr : true, }; - // Only add source if both repo and branch are provided - if (options.repo && options.branch) { - config.source = { github: options.repo, baseBranch: options.branch }; + if (repo && branch) { + config.source = { github: repo, baseBranch: branch }; } const result = options.interactive diff --git a/packages/mcp/src/tools/create-session.tool.ts b/packages/mcp/src/tools/create-session.tool.ts index 3dec642..2f48d09 100644 --- a/packages/mcp/src/tools/create-session.tool.ts +++ b/packages/mcp/src/tools/create-session.tool.ts @@ -5,7 +5,7 @@ import { defineTool, toMcpResponse } from './utils.js'; export default defineTool({ name: 'create_session', description: - 'Creates a new Jules session or automated run to perform code tasks. If repo and branch are omitted, creates a "repoless" session where the user provides their own context in the prompt and Jules will perform code tasks based on that context instead of a GitHub repo.', + 'Creates a new Jules session or automated run to perform code tasks. If repo and branch are omitted, they are auto-detected from the current git remote and branch. If auto-detection fails, creates a "repoless" session where the user provides their own context in the prompt and Jules will perform code tasks based on that context instead of a GitHub repo.', inputSchema: { type: 'object', properties: { @@ -16,11 +16,12 @@ export default defineTool({ repo: { type: 'string', description: - 'GitHub repository (owner/repo). Optional for repoless sessions.', + 'GitHub repository (owner/repo). If omitted, auto-detected from the current git remote.', }, branch: { type: 'string', - description: 'Target branch. Optional for repoless sessions.', + description: + 'Target branch. If omitted, auto-detected from the current git branch.', }, interactive: { type: 'boolean', diff --git a/packages/mcp/tests/functions/create-session.test.ts b/packages/mcp/tests/functions/create-session.test.ts index 30846ee..c07e7f5 100644 --- a/packages/mcp/tests/functions/create-session.test.ts +++ b/packages/mcp/tests/functions/create-session.test.ts @@ -1,13 +1,23 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { execSync } from 'node:child_process'; import { createSession } from '../../src/functions/create-session.js'; import { createMockClient } from './helpers.js'; import type { SessionConfig } from '@google/jules-sdk'; +vi.mock('node:child_process', () => ({ + execSync: vi.fn(), +})); + describe('createSession', () => { let mockClient: ReturnType; let capturedConfig: SessionConfig; beforeEach(() => { + // Default: git detection fails → repoless behaviour preserved + vi.mocked(execSync).mockImplementation(() => { + throw new Error('not a git repository'); + }); + mockClient = createMockClient(); // Mock both client.run and client.session to capture the config vi.spyOn(mockClient, 'run').mockImplementation(async (config) => { @@ -106,7 +116,7 @@ describe('createSession', () => { }); }); - it('omits source when only repo is provided', async () => { + it('omits source when only repo is provided and branch cannot be detected', async () => { await createSession(mockClient, { prompt: 'Fix the bug', repo: 'owner/repo', @@ -114,4 +124,119 @@ describe('createSession', () => { expect(capturedConfig.source).toBeUndefined(); }); + + describe('auto-detection', () => { + it('detects repo and branch from git when both are omitted', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git remote get-url origin') + return 'https://github.com/owner/detected-repo.git'; + if (cmd === 'git branch --show-current') return 'feature-branch'; + throw new Error(`unexpected command: ${cmd}`); + }); + + await createSession(mockClient, { prompt: 'Fix the bug' }); + + expect(capturedConfig.source).toEqual({ + github: 'owner/detected-repo', + baseBranch: 'feature-branch', + }); + }); + + it('explicit repo and branch are used without calling git detection', async () => { + await createSession(mockClient, { + prompt: 'Fix the bug', + repo: 'explicit/repo', + branch: 'explicit-branch', + }); + + expect(execSync).not.toHaveBeenCalled(); + expect(capturedConfig.source).toEqual({ + github: 'explicit/repo', + baseBranch: 'explicit-branch', + }); + }); + + it('falls back to repoless when remote URL is not a GitHub URL', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git remote get-url origin') + return 'git@gitlab.com:owner/repo.git'; + if (cmd === 'git branch --show-current') return 'main'; + throw new Error(`unexpected command: ${cmd}`); + }); + + await createSession(mockClient, { prompt: 'Fix the bug' }); + + expect(capturedConfig.source).toBeUndefined(); + }); + + it('falls back to repoless when git remote throws', async () => { + vi.mocked(execSync).mockImplementation(() => { + throw new Error('not a git repository'); + }); + + await createSession(mockClient, { prompt: 'Fix the bug' }); + + expect(capturedConfig.source).toBeUndefined(); + }); + + it('falls back to repoless when git branch returns empty string', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git remote get-url origin') + return 'https://github.com/owner/repo.git'; + if (cmd === 'git branch --show-current') return ''; + throw new Error(`unexpected command: ${cmd}`); + }); + + await createSession(mockClient, { prompt: 'Fix the bug' }); + + expect(capturedConfig.source).toBeUndefined(); + }); + + it('falls back to repoless when git branch throws', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git remote get-url origin') + return 'https://github.com/owner/repo.git'; + throw new Error('git branch failed'); + }); + + await createSession(mockClient, { prompt: 'Fix the bug' }); + + expect(capturedConfig.source).toBeUndefined(); + }); + + it('uses detected branch when repo is explicit but branch is absent', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git branch --show-current') return 'detected-branch'; + throw new Error(`unexpected command: ${cmd}`); + }); + + await createSession(mockClient, { + prompt: 'Fix the bug', + repo: 'explicit/repo', + }); + + expect(capturedConfig.source).toEqual({ + github: 'explicit/repo', + baseBranch: 'detected-branch', + }); + }); + + it('uses detected repo when branch is explicit but repo is absent', async () => { + vi.mocked(execSync).mockImplementation((cmd: any) => { + if (cmd === 'git remote get-url origin') + return 'git@github.com:detected/repo.git'; + throw new Error(`unexpected command: ${cmd}`); + }); + + await createSession(mockClient, { + prompt: 'Fix the bug', + branch: 'explicit-branch', + }); + + expect(capturedConfig.source).toEqual({ + github: 'detected/repo', + baseBranch: 'explicit-branch', + }); + }); + }); });