Skip to content

Commit 9f2ce3f

Browse files
committedMar 24, 2025
Merge remote-tracking branch 'origin/main'
2 parents 74eb7dc + f165a25 commit 9f2ce3f

File tree

6 files changed

+183
-4
lines changed

6 files changed

+183
-4
lines changed
 

‎docs/github-cli-usage.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# GitHub CLI Usage in MyCoder
2+
3+
This document explains how to properly use the GitHub CLI (`gh`) with MyCoder, especially when creating issues, PRs, or comments with multiline content.
4+
5+
## Using `stdinContent` for Multiline Content
6+
7+
When creating GitHub issues, PRs, or comments via the `gh` CLI tool, always use the `stdinContent` parameter for multiline content:
8+
9+
```javascript
10+
shellStart({
11+
command: 'gh issue create --body-stdin',
12+
stdinContent:
13+
'Issue description here with **markdown** support\nThis is a new line',
14+
description: 'Creating a new issue',
15+
});
16+
```
17+
18+
## Handling Newlines
19+
20+
MyCoder automatically handles newlines in two ways:
21+
22+
1. **Actual newlines** in template literals:
23+
24+
```javascript
25+
stdinContent: `Line 1
26+
Line 2
27+
Line 3`;
28+
```
29+
30+
2. **Escaped newlines** in regular strings:
31+
```javascript
32+
stdinContent: 'Line 1\\nLine 2\\nLine 3';
33+
```
34+
35+
Both approaches will result in properly formatted multiline content in GitHub. MyCoder automatically converts literal `\n` sequences to actual newlines before sending the content to the GitHub CLI.
36+
37+
## Best Practices
38+
39+
- Use template literals (backticks) for multiline content whenever possible, as they're more readable
40+
- When working with dynamic strings that might contain `\n`, don't worry - MyCoder will handle the conversion automatically
41+
- Always use `--body-stdin` (or equivalent) flags with the GitHub CLI to ensure proper formatting
42+
- For very large content, consider using `--body-file` with a temporary file instead
43+
44+
## Common Issues
45+
46+
If you notice that your GitHub comments or PR descriptions still contain literal `\n` sequences:
47+
48+
1. Make sure you're using the `stdinContent` parameter with `shellStart` or `shellExecute`
49+
2. Verify that you're using the correct GitHub CLI flags (e.g., `--body-stdin`)
50+
3. Check if your content is being processed by another function before reaching `stdinContent` that might be escaping the newlines
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,85 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it, vi } from 'vitest';
22

3-
// Skip testing for now
4-
describe.skip('shellExecuteTool', () => {
5-
it('should execute a shell command', async () => {
3+
import { shellExecuteTool } from './shellExecute';
4+
5+
// Mock child_process.exec
6+
vi.mock('child_process', () => ({
7+
exec: vi.fn(),
8+
}));
9+
10+
// Mock util.promisify to return our mocked exec function
11+
vi.mock('util', () => ({
12+
promisify: vi.fn((fn) => fn),
13+
}));
14+
15+
describe('shellExecuteTool', () => {
16+
// Original test - skipped
17+
it.skip('should execute a shell command', async () => {
618
// This is a dummy test that will be skipped
719
expect(true).toBe(true);
820
});
21+
22+
// New test for newline conversion
23+
it('should properly convert literal newlines in stdinContent', async () => {
24+
// Setup
25+
const { exec } = await import('child_process');
26+
const stdinWithLiteralNewlines = 'Line 1\\nLine 2\\nLine 3';
27+
const expectedProcessedContent = 'Line 1\nLine 2\nLine 3';
28+
29+
// Create a minimal mock context
30+
const mockContext = {
31+
logger: {
32+
debug: vi.fn(),
33+
error: vi.fn(),
34+
log: vi.fn(),
35+
warn: vi.fn(),
36+
info: vi.fn(),
37+
},
38+
workingDirectory: '/test',
39+
headless: false,
40+
userSession: false,
41+
tokenTracker: { trackTokens: vi.fn() },
42+
githubMode: false,
43+
provider: 'anthropic',
44+
maxTokens: 4000,
45+
temperature: 0,
46+
agentTracker: { registerAgent: vi.fn() },
47+
shellTracker: { registerShell: vi.fn(), processStates: new Map() },
48+
browserTracker: { registerSession: vi.fn() },
49+
};
50+
51+
// Create a real Buffer but spy on the toString method
52+
const realBuffer = Buffer.from('test');
53+
const bufferSpy = vi
54+
.spyOn(Buffer, 'from')
55+
.mockImplementationOnce((content) => {
56+
// Store the actual content for verification
57+
if (typeof content === 'string') {
58+
// This is where we verify the content has been transformed
59+
expect(content).toEqual(expectedProcessedContent);
60+
}
61+
return realBuffer;
62+
});
63+
64+
// Mock exec to resolve with empty stdout/stderr
65+
(exec as any).mockImplementationOnce((cmd, opts, callback) => {
66+
callback(null, { stdout: '', stderr: '' });
67+
});
68+
69+
// Execute the tool with literal newlines in stdinContent
70+
await shellExecuteTool.execute(
71+
{
72+
command: 'cat',
73+
description: 'Testing literal newline conversion',
74+
stdinContent: stdinWithLiteralNewlines,
75+
},
76+
mockContext as any,
77+
);
78+
79+
// Verify the Buffer.from was called
80+
expect(bufferSpy).toHaveBeenCalled();
81+
82+
// Reset mocks
83+
bufferSpy.mockRestore();
84+
});
985
});

‎packages/agent/src/tools/shell/shellExecute.ts

+3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export const shellExecuteTool: Tool<Parameters, ReturnType> = {
7474

7575
// If stdinContent is provided, use platform-specific approach to pipe content
7676
if (stdinContent && stdinContent.length > 0) {
77+
// Replace literal \n with actual newlines and \t with actual tabs
78+
stdinContent = stdinContent.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
79+
7780
const isWindows = process.platform === 'win32';
7881
const encodedContent = Buffer.from(stdinContent).toString('base64');
7982

‎packages/agent/src/tools/shell/shellStart.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,46 @@ describe('shellStartTool', () => {
192192
'With stdin content of length: 12',
193193
);
194194
});
195+
196+
it('should properly convert literal newlines in stdinContent', async () => {
197+
await import('child_process');
198+
const originalPlatform = process.platform;
199+
Object.defineProperty(process, 'platform', {
200+
value: 'darwin',
201+
writable: true,
202+
});
203+
204+
const stdinWithLiteralNewlines = 'Line 1\\nLine 2\\nLine 3';
205+
const expectedProcessedContent = 'Line 1\nLine 2\nLine 3';
206+
207+
// Capture the actual content being passed to Buffer.from
208+
let capturedContent = '';
209+
vi.spyOn(Buffer, 'from').mockImplementationOnce((content) => {
210+
if (typeof content === 'string') {
211+
capturedContent = content;
212+
}
213+
// Call the real implementation for encoding
214+
return Buffer.from(content);
215+
});
216+
217+
await shellStartTool.execute(
218+
{
219+
command: 'cat',
220+
description: 'Testing literal newline conversion',
221+
timeout: 0,
222+
stdinContent: stdinWithLiteralNewlines,
223+
},
224+
mockToolContext,
225+
);
226+
227+
// Verify that the literal newlines were converted to actual newlines
228+
expect(capturedContent).toEqual(expectedProcessedContent);
229+
230+
// Reset mocks and platform
231+
vi.spyOn(Buffer, 'from').mockRestore();
232+
Object.defineProperty(process, 'platform', {
233+
value: originalPlatform,
234+
writable: true,
235+
});
236+
});
195237
});

‎packages/agent/src/tools/shell/shellStart.ts

+5
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
117117
let childProcess;
118118

119119
if (stdinContent && stdinContent.length > 0) {
120+
// Replace literal \n with actual newlines and \t with actual tabs
121+
stdinContent = stdinContent
122+
.replace(/\\n/g, '\n')
123+
.replace(/\\t/g, '\t');
124+
120125
if (isWindows) {
121126
// Windows approach using PowerShell
122127
const encodedContent = Buffer.from(stdinContent).toString('base64');

‎test_content.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This is line 1.
2+
This is line 2.
3+
This is line 3.

0 commit comments

Comments
 (0)
Failed to load comments.