Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/agent/src/core/toolAgent/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { execSync } from 'child_process';

import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { getPlatformNewline } from '../../utils/github.js';

/**

Check failure on line 6 in packages/agent/src/core/toolAgent/config.ts

View workflow job for this annotation

GitHub Actions / ci

There should be no empty line within import group

Check failure on line 6 in packages/agent/src/core/toolAgent/config.ts

View workflow job for this annotation

GitHub Actions / ci

There should be no empty line within import group
* Available model providers
*/
export type ModelProvider = 'anthropic' | 'openai';
Expand Down Expand Up @@ -64,6 +64,8 @@
githubMode: toolContext.githubMode,
};

// Use the platform-specific newline handling for GitHub CLI commands

const githubModeInstructions = context.githubMode
? [
'',
Expand All @@ -76,6 +78,8 @@
'- 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')
: '';

Expand Down
45 changes: 44 additions & 1 deletion packages/agent/src/tools/system/shellStart.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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();
});
});
18 changes: 15 additions & 3 deletions packages/agent/src/tools/system/shellStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,24 +80,35 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {

execute: async (
{ command, timeout = DEFAULT_TIMEOUT },
{ logger, workingDirectory },
{ logger, workingDirectory, githubMode },
): Promise<ReturnType> => {
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();
let hasResolved = false;

// 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: [],
Expand Down
45 changes: 45 additions & 0 deletions packages/agent/src/utils/github.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
31 changes: 31 additions & 0 deletions packages/agent/src/utils/github.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading