diff --git a/packages/agent/src/core/toolAgent/config.ts b/packages/agent/src/core/toolAgent/config.ts index e66737c..37462c7 100644 --- a/packages/agent/src/core/toolAgent/config.ts +++ b/packages/agent/src/core/toolAgent/config.ts @@ -1,7 +1,7 @@ import { execSync } from 'child_process'; - import { anthropic } from '@ai-sdk/anthropic'; import { openai } from '@ai-sdk/openai'; +import { getPlatformNewline } from '../../utils/github.js'; /** * Available model providers @@ -64,6 +64,8 @@ export function getDefaultSystemPrompt(toolContext: ToolContext): string { githubMode: toolContext.githubMode, }; + // Use the platform-specific newline handling for GitHub CLI commands + const githubModeInstructions = context.githubMode ? [ '', @@ -76,6 +78,8 @@ export function getDefaultSystemPrompt(toolContext: ToolContext): string { '- Create additional GitHub issues for follow-up tasks or ideas', '', 'You can use the GitHub CLI (`gh`) for all GitHub interactions.', + '', + `When creating GitHub issues or PRs, use "${getPlatformNewline()}" for newlines in your text.`, ].join('\n') : ''; diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index 7984296..7f72b97 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -1,7 +1,8 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; +import * as githubUtils from '../../utils/github.js'; import { MockLogger } from '../../utils/mockLogger.js'; import { sleep } from '../../utils/sleep.js'; @@ -158,4 +159,46 @@ describe('shellStartTool', () => { expect(result.mode).toBe('sync'); }); + + it('should format GitHub CLI commands correctly in GitHub mode', async () => { + // Create a GitHub mode context + const githubModeContext: ToolContext = { + ...toolContext, + githubMode: true, + }; + + // Spy on formatGitHubText function + const formatSpy = vi.spyOn(githubUtils, 'formatGitHubText'); + formatSpy.mockImplementation((text) => `FORMATTED:${text}`); + + // Test with a GitHub CLI command that has a body + await shellStartTool.execute( + { + command: 'gh issue create --title "Test Issue" --body "Line 1\\nLine 2"', + description: 'GitHub CLI test', + timeout: 100, + }, + githubModeContext, + ); + + // Verify formatGitHubText was called + expect(formatSpy).toHaveBeenCalled(); + + // Test with a non-GitHub command (should not format) + formatSpy.mockClear(); + await shellStartTool.execute( + { + command: 'echo "Not a GitHub command"', + description: 'Non-GitHub command test', + timeout: 100, + }, + githubModeContext, + ); + + // Verify formatGitHubText was not called for non-GitHub commands + expect(formatSpy).not.toHaveBeenCalled(); + + // Restore the original implementation + formatSpy.mockRestore(); + }); }); diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index 60508ca..3e278c9 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { Tool } from '../../core/types.js'; +import { formatGitHubText } from '../../utils/github.js'; import { errorToString } from '../../utils/errorToString.js'; import type { ChildProcess } from 'child_process'; @@ -79,10 +80,21 @@ export const shellStartTool: Tool = { execute: async ( { command, timeout = DEFAULT_TIMEOUT }, - { logger, workingDirectory }, + { logger, workingDirectory, githubMode }, ): Promise => { logger.verbose(`Starting shell command: ${command}`); + // If GitHub mode is enabled and this is a GitHub CLI command, + // ensure newlines are properly escaped for the platform + let processedCommand = command; + if (githubMode && command.startsWith('gh ')) { + // Only process commands that might contain content with newlines + if (command.includes('--body') || command.includes('--title') || command.includes('--comment')) { + processedCommand = formatGitHubText(command); + logger.verbose(`Processed GitHub command with platform-specific newlines`); + } + } + return new Promise((resolve) => { try { const instanceId = uuidv4(); @@ -90,13 +102,13 @@ export const shellStartTool: Tool = { // Split command into command and args // Use command directly with shell: true - const process = spawn(command, [], { + const process = spawn(processedCommand, [], { shell: true, cwd: workingDirectory, }); const processState: ProcessState = { - command, + command: processedCommand, process, stdout: [], stderr: [], diff --git a/packages/agent/src/utils/github.test.ts b/packages/agent/src/utils/github.test.ts new file mode 100644 index 0000000..3035a34 --- /dev/null +++ b/packages/agent/src/utils/github.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { getPlatformNewline, formatGitHubText } from './github.js'; + +describe('GitHub utilities', () => { + describe('getPlatformNewline', () => { + it('should return \\n for non-Windows platforms', () => { + // Mock process.platform to be 'darwin' + vi.stubGlobal('process', { ...process, platform: 'darwin' }); + expect(getPlatformNewline()).toBe('\\n'); + + // Mock process.platform to be 'linux' + vi.stubGlobal('process', { ...process, platform: 'linux' }); + expect(getPlatformNewline()).toBe('\\n'); + }); + + it('should return \\r\\n for Windows platform', () => { + // Mock process.platform to be 'win32' + vi.stubGlobal('process', { ...process, platform: 'win32' }); + expect(getPlatformNewline()).toBe('\\r\\n'); + }); + }); + + describe('formatGitHubText', () => { + it('should replace newlines with platform-specific escape sequences', () => { + // Mock process.platform to be 'darwin' + vi.stubGlobal('process', { ...process, platform: 'darwin' }); + + const input = 'Hello\nWorld\nThis is a test'; + const expected = 'Hello\\nWorld\\nThis is a test'; + + expect(formatGitHubText(input)).toBe(expected); + }); + + it('should handle Windows newlines correctly', () => { + // Mock process.platform to be 'win32' + vi.stubGlobal('process', { ...process, platform: 'win32' }); + + const input = 'Hello\nWorld\nThis is a test'; + const expected = 'Hello\\r\\nWorld\\r\\nThis is a test'; + + expect(formatGitHubText(input)).toBe(expected); + }); + }); +}); \ No newline at end of file diff --git a/packages/agent/src/utils/github.ts b/packages/agent/src/utils/github.ts new file mode 100644 index 0000000..ef6b6ac --- /dev/null +++ b/packages/agent/src/utils/github.ts @@ -0,0 +1,31 @@ +/** + * Utility functions for GitHub CLI integration + */ + +/** + * Returns the appropriate newline character sequence for the current platform + * when used in GitHub CLI commands + * + * @returns The platform-specific newline escape sequence + */ +export function getPlatformNewline(): string { + // Check if we're on Windows + if (process.platform === 'win32') { + return '\\r\\n'; // Windows uses CRLF + } + return '\\n'; // Unix-based systems (Linux, macOS) use LF +} + +/** + * Formats text for use in GitHub CLI commands by ensuring + * newlines are properly escaped for the current platform + * + * @param text The text to format + * @returns Formatted text with proper newline escaping + */ +export function formatGitHubText(text: string): string { + const platformNewline = getPlatformNewline(); + + // Replace any literal newlines with platform-specific escape sequences + return text.replace(/\n/g, platformNewline); +} \ No newline at end of file