@studio-foundation/runner 0.3.0-beta.1 → 0.3.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +6 -3
  2. package/ARCHITECTURE.md +0 -53
  3. package/configs/agents/analyst.agent.yaml +0 -31
  4. package/configs/agents/code-generator.agent.yaml +0 -31
  5. package/configs/agents/generic.agent.yaml +0 -23
  6. package/src/__tests__/script-executor.test.ts +0 -180
  7. package/src/index.ts +0 -68
  8. package/src/integrations/integration-loader.test.ts +0 -88
  9. package/src/integrations/integration-loader.ts +0 -68
  10. package/src/middleware/anonymization.ts +0 -38
  11. package/src/plugins/index.ts +0 -4
  12. package/src/plugins/mcp-client.test.ts +0 -148
  13. package/src/plugins/mcp-client.ts +0 -128
  14. package/src/plugins/oauth-provider.test.ts +0 -167
  15. package/src/plugins/oauth-provider.ts +0 -175
  16. package/src/plugins/plugin-loader.test.ts +0 -114
  17. package/src/plugins/plugin-loader.ts +0 -90
  18. package/src/prompt-builder.test.ts +0 -167
  19. package/src/prompt-builder.ts +0 -332
  20. package/src/providers/anthropic.test.ts +0 -101
  21. package/src/providers/anthropic.ts +0 -135
  22. package/src/providers/mock.ts +0 -57
  23. package/src/providers/ollama.test.ts +0 -166
  24. package/src/providers/ollama.ts +0 -152
  25. package/src/providers/openai-responses.ts +0 -212
  26. package/src/providers/openai.test.ts +0 -67
  27. package/src/providers/openai.ts +0 -139
  28. package/src/providers/provider.ts +0 -54
  29. package/src/providers/registry.ts +0 -77
  30. package/src/runner.test.ts +0 -343
  31. package/src/runner.ts +0 -396
  32. package/src/script-executor.ts +0 -107
  33. package/src/tools/builtin/git.ts +0 -311
  34. package/src/tools/builtin/patch.ts +0 -257
  35. package/src/tools/builtin/repo-manager.ts +0 -142
  36. package/src/tools/builtin/search.ts +0 -108
  37. package/src/tools/builtin/shell.ts +0 -82
  38. package/src/tools/builtin/studio-run.ts +0 -73
  39. package/src/tools/builtin/web-search.test.ts +0 -122
  40. package/src/tools/builtin/web-search.ts +0 -101
  41. package/src/tools/errors.test.ts +0 -12
  42. package/src/tools/errors.ts +0 -6
  43. package/src/tools/plugin-loader.test.ts +0 -130
  44. package/src/tools/plugin-loader.ts +0 -203
  45. package/src/tools/skills/README.md +0 -49
  46. package/src/tools/skills/skill-loader.test.ts +0 -106
  47. package/src/tools/skills/skill-loader.ts +0 -62
  48. package/src/tools/tool-executor.test.ts +0 -88
  49. package/src/tools/tool-executor.ts +0 -84
  50. package/src/tools/tool-registry.ts +0 -130
  51. package/src/tools/yaml-executor.ts +0 -120
  52. package/src/utils/race-signal.test.ts +0 -50
  53. package/src/utils/race-signal.ts +0 -17
  54. package/templates/integrations/linear.integration.yaml +0 -35
  55. package/templates/integrations/slack.integration.yaml +0 -22
  56. package/templates/integrations/webhook.integration.yaml +0 -17
  57. package/templates/tools/git.tool.yaml +0 -80
  58. package/templates/tools/repo-manager.tool.yaml +0 -64
  59. package/templates/tools/search.tool.yaml +0 -22
  60. package/templates/tools/shell.tool.yaml +0 -19
  61. package/templates/tools/web-search.tool.yaml +0 -24
  62. package/tests/anonymization-middleware.test.ts +0 -61
  63. package/tests/anthropic.test.ts +0 -87
  64. package/tests/apply-patch.test.ts +0 -355
  65. package/tests/fixtures/tools/test-builtin.tool.yaml +0 -14
  66. package/tests/fixtures/tools/test-shell.tool.yaml +0 -19
  67. package/tests/mock-provider.test.ts +0 -104
  68. package/tests/openai.test.ts +0 -72
  69. package/tests/plugin-loader.test.ts +0 -54
  70. package/tests/prompt-builder.test.ts +0 -468
  71. package/tests/runner-anonymization.test.ts +0 -89
  72. package/tests/runner.test.ts +0 -885
  73. package/tests/studio-run.test.ts +0 -94
  74. package/tests/tool-executor.test.ts +0 -115
  75. package/tests/tool-registry.test.ts +0 -84
  76. package/tests/yaml-executor.test.ts +0 -76
  77. package/tsconfig.json +0 -20
  78. package/vitest.config.ts +0 -7
@@ -1,311 +0,0 @@
1
- /**
2
- * Git tools - branch, commit, push, pull, status, diff
3
- */
4
-
5
- import { exec } from 'child_process';
6
- import { promisify } from 'util';
7
- import type { Tool } from '../tool-registry.js';
8
-
9
- const execAsync = promisify(exec);
10
-
11
- const PROTECTED_BRANCHES = ['main', 'master', 'develop', 'production'];
12
-
13
- async function isGitRepo(workingDir: string): Promise<boolean> {
14
- try {
15
- await execAsync('git rev-parse --git-dir', { cwd: workingDir });
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- async function runGit(args: string, workingDir: string): Promise<{ stdout: string; stderr: string }> {
23
- return execAsync(`git ${args}`, { cwd: workingDir, timeout: 30000 });
24
- }
25
-
26
- export function createGitTools(workingDir: string): Tool[] {
27
- return [
28
- {
29
- name: 'git-checkout',
30
- description: 'Checkout or create a branch',
31
- parameters: {
32
- type: 'object',
33
- properties: {
34
- branch: {
35
- type: 'string',
36
- description: 'Branch name to checkout or create'
37
- },
38
- create: {
39
- type: 'boolean',
40
- description: 'Create the branch if it does not exist (-b flag). Default: false'
41
- }
42
- },
43
- required: ['branch']
44
- },
45
- execute: async ({ branch, create }) => {
46
- const branchName = branch as string;
47
- const shouldCreate = create as boolean | undefined ?? false;
48
-
49
- if (!(await isGitRepo(workingDir))) {
50
- return { success: false, output: null, error: 'Not a git repository' };
51
- }
52
-
53
- if (shouldCreate && PROTECTED_BRANCHES.includes(branchName)) {
54
- return {
55
- success: false,
56
- output: null,
57
- error: `Cannot create protected branch: ${branchName}`
58
- };
59
- }
60
-
61
- try {
62
- const flag = shouldCreate ? '-b ' : '';
63
- const { stdout, stderr } = await runGit(`checkout ${flag}${branchName}`, workingDir);
64
- return {
65
- success: true,
66
- output: { branch: branchName, created: shouldCreate, stdout: stdout.trim(), stderr: stderr.trim() }
67
- };
68
- } catch (error: unknown) {
69
- const e = error as { stderr?: string; message?: string };
70
- return {
71
- success: false,
72
- output: null,
73
- error: e.stderr?.trim() || e.message || 'git checkout failed'
74
- };
75
- }
76
- }
77
- },
78
-
79
- {
80
- name: 'git-commit',
81
- description: 'Stage files and commit changes',
82
- parameters: {
83
- type: 'object',
84
- properties: {
85
- message: {
86
- type: 'string',
87
- description: 'Commit message'
88
- },
89
- files: {
90
- type: 'array',
91
- items: { type: 'string' },
92
- description: 'Files to stage. If empty or omitted, stages all changes (git add -A)'
93
- }
94
- },
95
- required: ['message']
96
- },
97
- execute: async ({ message, files }) => {
98
- const commitMessage = message as string;
99
- const filesToStage = files as string[] | undefined;
100
-
101
- if (!(await isGitRepo(workingDir))) {
102
- return { success: false, output: null, error: 'Not a git repository' };
103
- }
104
-
105
- try {
106
- // Check for merge conflicts
107
- const { stdout: statusOut } = await runGit('status --porcelain', workingDir);
108
- const hasConflicts = statusOut.split('\n').some(line => line.startsWith('UU') || line.startsWith('AA') || line.startsWith('DD'));
109
- if (hasConflicts) {
110
- return { success: false, output: null, error: 'Cannot commit: merge conflicts detected' };
111
- }
112
-
113
- // Stage files
114
- if (filesToStage && filesToStage.length > 0) {
115
- await runGit(`add -- ${filesToStage.map(f => `"${f}"`).join(' ')}`, workingDir);
116
- } else {
117
- await runGit('add -A', workingDir);
118
- }
119
-
120
- // Commit
121
- const { stdout } = await runGit(`commit -m ${JSON.stringify(commitMessage)}`, workingDir);
122
- return {
123
- success: true,
124
- output: { message: commitMessage, stdout: stdout.trim() }
125
- };
126
- } catch (error: unknown) {
127
- const e = error as { stderr?: string; stdout?: string; message?: string };
128
- return {
129
- success: false,
130
- output: null,
131
- error: e.stderr?.trim() || e.stdout?.trim() || e.message || 'git commit failed'
132
- };
133
- }
134
- }
135
- },
136
-
137
- {
138
- name: 'git-push',
139
- description: 'Push current branch to remote',
140
- parameters: {
141
- type: 'object',
142
- properties: {
143
- remote: {
144
- type: 'string',
145
- description: 'Remote name. Default: origin'
146
- },
147
- set_upstream: {
148
- type: 'boolean',
149
- description: 'Set upstream tracking (-u flag). Default: true'
150
- }
151
- },
152
- required: []
153
- },
154
- execute: async ({ remote, set_upstream }) => {
155
- const remoteName = (remote as string | undefined) ?? 'origin';
156
- const setUpstream = (set_upstream as boolean | undefined) ?? true;
157
-
158
- if (!(await isGitRepo(workingDir))) {
159
- return { success: false, output: null, error: 'Not a git repository' };
160
- }
161
-
162
- try {
163
- const { stdout: branchOut } = await runGit('rev-parse --abbrev-ref HEAD', workingDir);
164
- const currentBranch = branchOut.trim();
165
-
166
- const upstreamFlag = setUpstream ? `-u ` : '';
167
- const { stdout, stderr } = await runGit(`push ${upstreamFlag}${remoteName} ${currentBranch}`, workingDir);
168
- return {
169
- success: true,
170
- output: { remote: remoteName, branch: currentBranch, stdout: stdout.trim(), stderr: stderr.trim() }
171
- };
172
- } catch (error: unknown) {
173
- const e = error as { stderr?: string; message?: string };
174
- return {
175
- success: false,
176
- output: null,
177
- error: e.stderr?.trim() || e.message || 'git push failed'
178
- };
179
- }
180
- }
181
- },
182
-
183
- {
184
- name: 'git-pull',
185
- description: 'Pull latest changes from remote',
186
- parameters: {
187
- type: 'object',
188
- properties: {
189
- remote: {
190
- type: 'string',
191
- description: 'Remote name. Default: origin'
192
- },
193
- branch: {
194
- type: 'string',
195
- description: 'Branch to pull. If omitted, pulls current branch'
196
- }
197
- },
198
- required: []
199
- },
200
- execute: async ({ remote, branch }) => {
201
- const remoteName = (remote as string | undefined) ?? 'origin';
202
- const branchName = branch as string | undefined;
203
-
204
- if (!(await isGitRepo(workingDir))) {
205
- return { success: false, output: null, error: 'Not a git repository' };
206
- }
207
-
208
- try {
209
- const branchArg = branchName ? ` ${branchName}` : '';
210
- const { stdout, stderr } = await runGit(`pull ${remoteName}${branchArg}`, workingDir);
211
- return {
212
- success: true,
213
- output: { remote: remoteName, branch: branchName ?? 'current', stdout: stdout.trim(), stderr: stderr.trim() }
214
- };
215
- } catch (error: unknown) {
216
- const e = error as { stderr?: string; stdout?: string; message?: string };
217
- return {
218
- success: false,
219
- output: null,
220
- error: e.stderr?.trim() || e.stdout?.trim() || e.message || 'git pull failed'
221
- };
222
- }
223
- }
224
- },
225
-
226
- {
227
- name: 'git-status',
228
- description: 'Show working tree status (modified, added, deleted files)',
229
- parameters: {
230
- type: 'object',
231
- properties: {},
232
- required: []
233
- },
234
- execute: async () => {
235
- if (!(await isGitRepo(workingDir))) {
236
- return { success: false, output: null, error: 'Not a git repository' };
237
- }
238
-
239
- try {
240
- const { stdout: porcelain } = await runGit('status --porcelain', workingDir);
241
- const { stdout: branch } = await runGit('rev-parse --abbrev-ref HEAD', workingDir);
242
-
243
- const files = porcelain
244
- .split('\n')
245
- .filter(line => line.trim())
246
- .map(line => ({ status: line.slice(0, 2).trim(), file: line.slice(3) }));
247
-
248
- return {
249
- success: true,
250
- output: {
251
- branch: branch.trim(),
252
- clean: files.length === 0,
253
- files
254
- }
255
- };
256
- } catch (error: unknown) {
257
- const e = error as { stderr?: string; message?: string };
258
- return {
259
- success: false,
260
- output: null,
261
- error: e.stderr?.trim() || e.message || 'git status failed'
262
- };
263
- }
264
- }
265
- },
266
-
267
- {
268
- name: 'git-diff',
269
- description: 'Show changes in working tree or staged files',
270
- parameters: {
271
- type: 'object',
272
- properties: {
273
- staged: {
274
- type: 'boolean',
275
- description: 'Show staged changes (--cached). Default: false'
276
- },
277
- file: {
278
- type: 'string',
279
- description: 'Diff a specific file. If omitted, shows all changes'
280
- }
281
- },
282
- required: []
283
- },
284
- execute: async ({ staged, file }) => {
285
- const showStaged = (staged as boolean | undefined) ?? false;
286
- const filePath = file as string | undefined;
287
-
288
- if (!(await isGitRepo(workingDir))) {
289
- return { success: false, output: null, error: 'Not a git repository' };
290
- }
291
-
292
- try {
293
- const stagedFlag = showStaged ? '--cached ' : '';
294
- const fileArg = filePath ? `-- "${filePath}"` : '';
295
- const { stdout } = await runGit(`diff ${stagedFlag}${fileArg}`.trim(), workingDir);
296
- return {
297
- success: true,
298
- output: { diff: stdout, staged: showStaged, file: filePath ?? null }
299
- };
300
- } catch (error: unknown) {
301
- const e = error as { stderr?: string; message?: string };
302
- return {
303
- success: false,
304
- output: null,
305
- error: e.stderr?.trim() || e.message || 'git diff failed'
306
- };
307
- }
308
- }
309
- }
310
- ];
311
- }
@@ -1,257 +0,0 @@
1
- /**
2
- * Patch tool - apply unified diffs to files
3
- */
4
-
5
- import * as fs from 'fs/promises';
6
- import * as path from 'path';
7
- import type { Tool, ToolResult } from '../tool-registry.js';
8
-
9
- interface Hunk {
10
- oldStart: number;
11
- oldCount: number;
12
- lines: HunkLine[];
13
- }
14
-
15
- interface HunkLine {
16
- type: 'context' | 'add' | 'remove';
17
- content: string;
18
- }
19
-
20
- interface PatchResult {
21
- success: boolean;
22
- path: string;
23
- hunks_applied: number;
24
- hunks_total: number;
25
- lines_added: number;
26
- lines_removed: number;
27
- }
28
-
29
- /**
30
- * Parse a unified diff string into hunks.
31
- */
32
- function parseHunks(patch: string): Hunk[] {
33
- const rawLines = patch.split('\n');
34
- // Remove the trailing empty string that split() produces when the patch ends
35
- // with '\n'. Without this, the '' is treated as a blank context line and gets
36
- // appended to getOldBlock(), causing spurious "Ambiguous match" errors when
37
- // the file contains blank lines.
38
- if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
39
- rawLines.pop();
40
- }
41
- // Filter out --- / +++ headers
42
- const lines = rawLines.filter(
43
- (l) => !l.startsWith('---') && !l.startsWith('+++')
44
- );
45
-
46
- const hunks: Hunk[] = [];
47
- let current: Hunk | null = null;
48
-
49
- for (const line of lines) {
50
- const hunkHeader = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+\d+(?:,\d+)?\s*@@/);
51
- if (hunkHeader) {
52
- current = {
53
- oldStart: parseInt(hunkHeader[1], 10),
54
- oldCount: parseInt(hunkHeader[2] ?? '1', 10),
55
- lines: [],
56
- };
57
- hunks.push(current);
58
- continue;
59
- }
60
-
61
- if (!current) continue;
62
-
63
- if (line.startsWith('+')) {
64
- current.lines.push({ type: 'add', content: line.slice(1) });
65
- } else if (line.startsWith('-')) {
66
- current.lines.push({ type: 'remove', content: line.slice(1) });
67
- } else if (line.startsWith(' ') || line === '') {
68
- // Context line — space prefix or empty line
69
- const content = line.startsWith(' ') ? line.slice(1) : line;
70
- current.lines.push({ type: 'context', content });
71
- }
72
- // Ignore lines like ""
73
- }
74
-
75
- return hunks;
76
- }
77
-
78
- /**
79
- * Build the "old block" — context + removed lines that must match the file.
80
- */
81
- function getOldBlock(hunk: Hunk): string[] {
82
- return hunk.lines
83
- .filter((l) => l.type === 'context' || l.type === 'remove')
84
- .map((l) => l.content);
85
- }
86
-
87
- /**
88
- * Build the "new block" — context + added lines that replace the old block.
89
- */
90
- function getNewBlock(hunk: Hunk): string[] {
91
- return hunk.lines
92
- .filter((l) => l.type === 'context' || l.type === 'add')
93
- .map((l) => l.content);
94
- }
95
-
96
- /**
97
- * Compare two strings with trailing whitespace tolerance.
98
- */
99
- function fuzzyMatch(a: string, b: string): boolean {
100
- return a.trimEnd() === b.trimEnd();
101
- }
102
-
103
- /**
104
- * Check if oldBlock matches fileLines starting at position `start`.
105
- */
106
- function blockMatchesAt(fileLines: string[], oldBlock: string[], start: number): boolean {
107
- if (start + oldBlock.length > fileLines.length) return false;
108
- return oldBlock.every((line, i) => fuzzyMatch(fileLines[start + i], line));
109
- }
110
-
111
- /**
112
- * Find where the old block matches in the file.
113
- * Tries the hinted line first, then scans the full file.
114
- * Returns the 0-based start index.
115
- */
116
- function findMatch(
117
- fileLines: string[],
118
- oldBlock: string[],
119
- hintLine: number,
120
- hunkIndex: number
121
- ): number {
122
- // Convert 1-based hint to 0-based
123
- const hint = hintLine - 1;
124
-
125
- // Pure-insertion hunk: @@ -N,0 ... has no context or removed lines.
126
- // Scanning for an empty block would vacuously match every position, so we
127
- // insert directly at the hinted position instead.
128
- if (oldBlock.length === 0) {
129
- return Math.max(0, hint);
130
- }
131
-
132
- // Fast path: try at the hinted position
133
- if (hint >= 0 && blockMatchesAt(fileLines, oldBlock, hint)) {
134
- return hint;
135
- }
136
-
137
- // Slow path: scan the whole file
138
- const matches: number[] = [];
139
- for (let i = 0; i < fileLines.length; i++) {
140
- if (blockMatchesAt(fileLines, oldBlock, i)) {
141
- matches.push(i);
142
- }
143
- }
144
-
145
- if (matches.length === 1) return matches[0];
146
-
147
- if (matches.length === 0) {
148
- const expected = oldBlock[0] ?? '(empty)';
149
- throw new Error(
150
- `Context mismatch at hunk ${hunkIndex + 1}: could not find context "${expected}" in file`
151
- );
152
- }
153
-
154
- throw new Error(
155
- `Ambiguous match at hunk ${hunkIndex + 1}: context found at lines ${matches.map((m) => m + 1).join(', ')}. Add more context lines.`
156
- );
157
- }
158
-
159
- export function createPatchTools(repoPath: string): Tool[] {
160
- return [
161
- {
162
- name: 'repo_manager-apply_patch',
163
- description:
164
- 'Apply a unified diff patch to a file. The patch must include enough context lines for unambiguous matching. Fails loudly if context doesn\'t match the file content.',
165
- parameters: {
166
- type: 'object',
167
- properties: {
168
- path: {
169
- type: 'string',
170
- description: 'Relative path to the file to patch (from workspace root)',
171
- },
172
- patch: {
173
- type: 'string',
174
- description:
175
- 'Unified diff format patch. Must start with @@ hunk headers. Use - for removed lines, + for added lines, space for context lines. Include at least 3 context lines before and after changes.',
176
- },
177
- },
178
- required: ['path', 'patch'],
179
- },
180
- execute: async ({ path: filePath, patch: patchStr }): Promise<ToolResult> => {
181
- try {
182
- const fullPath = path.join(repoPath, filePath as string);
183
-
184
- // Read file
185
- let fileContent: string;
186
- try {
187
- fileContent = await fs.readFile(fullPath, 'utf-8');
188
- } catch {
189
- return {
190
- success: false,
191
- output: null,
192
- error: `File not found: ${filePath}`,
193
- };
194
- }
195
-
196
- // Parse hunks
197
- const hunks = parseHunks(patchStr as string);
198
- if (hunks.length === 0) {
199
- return {
200
- success: false,
201
- output: null,
202
- error: 'Invalid patch format: no hunks found (expected @@ headers)',
203
- };
204
- }
205
-
206
- const fileLines = fileContent.split('\n');
207
- let totalAdded = 0;
208
- let totalRemoved = 0;
209
-
210
- // Find all match positions first (before any modifications)
211
- const matchPositions: number[] = [];
212
- for (let i = 0; i < hunks.length; i++) {
213
- const oldBlock = getOldBlock(hunks[i]);
214
- const pos = findMatch(fileLines, oldBlock, hunks[i].oldStart, i);
215
- matchPositions.push(pos);
216
- }
217
-
218
- // Apply hunks in reverse order to preserve line numbers
219
- const indices = hunks.map((_, i) => i);
220
- indices.sort((a, b) => matchPositions[b] - matchPositions[a]);
221
-
222
- for (const i of indices) {
223
- const hunk = hunks[i];
224
- const pos = matchPositions[i];
225
- const oldBlock = getOldBlock(hunk);
226
- const newBlock = getNewBlock(hunk);
227
-
228
- fileLines.splice(pos, oldBlock.length, ...newBlock);
229
- totalAdded += hunk.lines.filter((l) => l.type === 'add').length;
230
- totalRemoved += hunk.lines.filter((l) => l.type === 'remove').length;
231
- }
232
-
233
- // Write file back
234
- await fs.writeFile(fullPath, fileLines.join('\n'), 'utf-8');
235
-
236
- const result: PatchResult = {
237
- success: true,
238
- path: filePath as string,
239
- hunks_applied: hunks.length,
240
- hunks_total: hunks.length,
241
- lines_added: totalAdded,
242
- lines_removed: totalRemoved,
243
- };
244
-
245
- return { success: true, output: result };
246
- } catch (error: unknown) {
247
- const errorMessage = error instanceof Error ? error.message : String(error);
248
- return {
249
- success: false,
250
- output: null,
251
- error: errorMessage,
252
- };
253
- }
254
- },
255
- },
256
- ];
257
- }
@@ -1,142 +0,0 @@
1
- /**
2
- * Repository manager tools - file operations
3
- */
4
-
5
- import * as fs from 'fs/promises';
6
- import * as path from 'path';
7
- import type { Tool } from '../tool-registry.js';
8
-
9
- export function createRepoManagerTools(repoPath: string): Tool[] {
10
- return [
11
- {
12
- name: 'repo_manager-read_file',
13
- description: 'Read the contents of a file in the repository',
14
- parameters: {
15
- type: 'object',
16
- properties: {
17
- path: {
18
- type: 'string',
19
- description: 'Relative path to the file from repository root'
20
- }
21
- },
22
- required: ['path']
23
- },
24
- execute: async ({ path: filePath }) => {
25
- try {
26
- const fullPath = path.join(repoPath, filePath as string);
27
- const content = await fs.readFile(fullPath, 'utf-8');
28
- return {
29
- success: true,
30
- output: { path: filePath, content }
31
- };
32
- } catch (error: unknown) {
33
- const errorMessage = error instanceof Error ? error.message : String(error);
34
- return {
35
- success: false,
36
- output: null,
37
- error: `Failed to read file: ${errorMessage}`
38
- };
39
- }
40
- }
41
- },
42
- {
43
- name: 'repo_manager-write_file',
44
- description: 'Write content to a file in the repository (creates or overwrites)',
45
- parameters: {
46
- type: 'object',
47
- properties: {
48
- path: {
49
- type: 'string',
50
- description: 'Relative path to the file from repository root'
51
- },
52
- content: {
53
- type: 'string',
54
- description: 'Complete file content to write'
55
- }
56
- },
57
- required: ['path', 'content']
58
- },
59
- execute: async ({ path: filePath, content }) => {
60
- try {
61
- const fullPath = path.join(repoPath, filePath as string);
62
- const dir = path.dirname(fullPath);
63
-
64
- // Create parent directories if needed
65
- await fs.mkdir(dir, { recursive: true });
66
-
67
- // Write file
68
- await fs.writeFile(fullPath, content as string, 'utf-8');
69
-
70
- return {
71
- success: true,
72
- output: { path: filePath, written: true }
73
- };
74
- } catch (error: unknown) {
75
- const errorMessage = error instanceof Error ? error.message : String(error);
76
- return {
77
- success: false,
78
- output: null,
79
- error: `Failed to write file: ${errorMessage}`
80
- };
81
- }
82
- }
83
- },
84
- {
85
- name: 'repo_manager-list_files',
86
- description: 'List files in a directory of the repository',
87
- parameters: {
88
- type: 'object',
89
- properties: {
90
- path: {
91
- type: 'string',
92
- description: 'Relative path to the directory (default: root)',
93
- default: '.'
94
- },
95
- recursive: {
96
- type: 'boolean',
97
- description: 'List files recursively (default: false)',
98
- default: false
99
- }
100
- }
101
- },
102
- execute: async ({ path: dirPath = '.', recursive = false }) => {
103
- try {
104
- const fullPath = path.join(repoPath, dirPath as string);
105
-
106
- const files: string[] = [];
107
- const ignoredDirs = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
108
-
109
- const listDir = async (currentPath: string, relativeBase: string) => {
110
- const entries = await fs.readdir(currentPath, { withFileTypes: true });
111
-
112
- for (const entry of entries) {
113
- const relativePath = path.join(relativeBase, entry.name);
114
-
115
- if (entry.isDirectory()) {
116
- if (recursive && !ignoredDirs.includes(entry.name)) {
117
- await listDir(path.join(currentPath, entry.name), relativePath);
118
- }
119
- } else if (entry.isFile()) {
120
- files.push(relativePath);
121
- }
122
- }
123
- };
124
-
125
- await listDir(fullPath, '');
126
-
127
- return {
128
- success: true,
129
- output: { path: dirPath, files, count: files.length }
130
- };
131
- } catch (error: unknown) {
132
- const errorMessage = error instanceof Error ? error.message : String(error);
133
- return {
134
- success: false,
135
- output: null,
136
- error: `Failed to list files: ${errorMessage}`
137
- };
138
- }
139
- }
140
- }
141
- ];
142
- }