-
Notifications
You must be signed in to change notification settings - Fork 0
feat/14 append file tool #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4be8181
feat: add gh_pr_review tool to fetch PR review comments #12
four-bytes-robby cbec041
fix: typecheck error in gitlab-utils.ts — cast data to any for messag…
four-bytes-robby cf2579b
feat: add append_file tool for simple file appending #14
four-bytes-robby 85f5f5f
fix: add missing lint script to package.json #14
four-bytes-robby ac09337
chore: fix all lint errors (unused vars, no-undef Bun globals, pretti…
four-bytes-robby 157f604
fix: set fetch-depth: 0 in CI to fix git-diff tests needing HEAD~1 #14
four-bytes-robby File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2025-2026 Four Bytes | ||
|
|
||
| import { tool } from '@opencode-ai/plugin'; | ||
| import { logDebugEvent } from '../lib/debug-logger'; | ||
| import { readFile, writeFile } from 'node:fs/promises'; | ||
|
|
||
| export const appendFileTool = tool({ | ||
| description: | ||
| 'Append or prepend text to a file. Use for simple file additions (changelogs, blogs, logs) where unified diff matching is unnecessary. Saves ~95% tokens vs. patch_file for append-only operations.', | ||
|
|
||
| args: { | ||
| file_path: tool.schema.string().describe('Absolute path to the file'), | ||
| content: tool.schema.string().describe('Text to insert into the file'), | ||
| mode: tool.schema.string().optional().describe('"append" (default) or "prepend"'), | ||
| after_line: tool.schema | ||
| .number() | ||
| .optional() | ||
| .describe( | ||
| 'For prepend mode: insert after this line number (0-based; 0 = before first line, -1 = before last line). Ignored in append mode.' | ||
| ), | ||
| }, | ||
|
|
||
| async execute(args, _ctx) { | ||
| logDebugEvent('append_file.start', { file_path: args.file_path, mode: args.mode }); | ||
|
|
||
| try { | ||
| const content = await readFile(args.file_path, 'utf-8'); | ||
| const mode = args.mode || 'append'; | ||
|
|
||
| let newContent: string; | ||
| if (content.length === 0) { | ||
| // Empty file — just write the content | ||
| newContent = args.content; | ||
| if (!newContent.endsWith('\n')) { | ||
| newContent += '\n'; | ||
| } | ||
| } else if (mode === 'prepend') { | ||
| const lines = content.split('\n'); | ||
| let insertAt = args.after_line ?? 0; | ||
| // -1 means before the last line | ||
| if (insertAt === -1) { | ||
| insertAt = Math.max(0, lines.length - 2); | ||
| } | ||
| // after_line=0 means position 0 (before first line). | ||
| // after_line=N (N>0) means after line at index N, so position N+1. | ||
| const insertPos = insertAt === 0 ? 0 : insertAt + 1; | ||
| const before = lines.slice(0, insertPos); | ||
| const after = lines.slice(insertPos); | ||
| newContent = [...before, args.content, ...after].join('\n'); | ||
| } else { | ||
| // Append mode | ||
| newContent = content.endsWith('\n') | ||
| ? content + args.content | ||
| : content + '\n' + args.content; | ||
| if (!newContent.endsWith('\n')) { | ||
| newContent += '\n'; | ||
| } | ||
| } | ||
|
|
||
| await writeFile(args.file_path, newContent, 'utf-8'); | ||
| const lineCount = newContent.split('\n').length; | ||
|
|
||
| logDebugEvent('append_file.done', { file_path: args.file_path, lines: lineCount }); | ||
| return `Appended to ${args.file_path} (${mode} mode, ${lineCount} lines total)`; | ||
| } catch (error) { | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
| logDebugEvent('append_file.error', { error: msg }); | ||
| return `Error: ${msg}`; | ||
| } | ||
| }, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2025-2026 Four Bytes | ||
|
|
||
| import { tool } from '@opencode-ai/plugin'; | ||
| import { runGh, resolveRepo } from '../lib/gh-utils'; | ||
| import { logDebugEvent } from '../lib/debug-logger'; | ||
|
|
||
| export const ghPrReviewTool = tool({ | ||
| description: | ||
| 'Fetch review comments and reviews on a GitHub pull request. Returns structured feedback including review state (APPROVED/CHANGES_REQUESTED/COMMENTED) and comment bodies. Saves ~90% tokens vs. bash→read→parse.', | ||
|
|
||
| args: { | ||
| pr: tool.schema.number().describe('PR number to review'), | ||
| repo: tool.schema | ||
| .string() | ||
| .optional() | ||
| .describe('GitHub repo in owner/repo format (defaults to current repo)'), | ||
| }, | ||
|
|
||
| async execute(args, ctx) { | ||
| const { pr, repo } = args; | ||
|
|
||
| logDebugEvent('gh_pr_review.start', { pr }); | ||
|
|
||
| try { | ||
| const resolvedRepo = await resolveRepo(repo, ctx.directory); | ||
| const repoArgs = ['-R', resolvedRepo]; | ||
|
|
||
| // Fetch issue-level comments + review summaries | ||
| // Note: `comments` are issue/timeline comments; `reviews` are formal review submissions. | ||
| // Inline diff comments (with path/line) require GraphQL — out of scope for v1. | ||
| const output = await runGh( | ||
| ['pr', 'view', String(pr), ...repoArgs, '--json', 'comments,reviews'], | ||
| ctx.directory | ||
| ); | ||
|
|
||
| const data = JSON.parse(output) as { | ||
| comments?: Array<{ | ||
| author?: { login?: string }; | ||
| body?: string; | ||
| createdAt?: string; | ||
| }>; | ||
| reviews?: Array<{ | ||
| author?: { login?: string }; | ||
| state?: string; | ||
| body?: string; | ||
| submittedAt?: string; | ||
| }>; | ||
| }; | ||
|
|
||
| const comments = data.comments ?? []; | ||
| const reviews = data.reviews ?? []; | ||
|
|
||
| // Filter out empty-body reviews (e.g. bare APPROVED clicks) | ||
| const substantiveReviews = reviews.filter((r) => r.body && r.body.trim().length > 0); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Review-state data is lost by filtering out bodyless reviews. Tool can report “No review comments” even when formal review decisions exist. Prompt for AI agents |
||
|
|
||
| if (comments.length === 0 && substantiveReviews.length === 0) { | ||
| return `No review comments on PR #${pr}.`; | ||
| } | ||
|
|
||
| const lines: string[] = [`Review comments for PR #${pr}:`, '']; | ||
|
|
||
| if (substantiveReviews.length > 0) { | ||
| lines.push('## Reviews'); | ||
| lines.push(''); | ||
| for (const review of substantiveReviews) { | ||
| const reviewer = review.author?.login ?? 'unknown'; | ||
| const state = review.state ?? 'COMMENTED'; | ||
| const ts = review.submittedAt ? ` (${review.submittedAt})` : ''; | ||
| const body = | ||
| review.body!.length > 500 ? `${review.body!.substring(0, 500)}…` : review.body!; | ||
| lines.push(`[${state}] ${reviewer}${ts}:`); | ||
| lines.push(body); | ||
| lines.push(''); | ||
| } | ||
| } | ||
|
|
||
| if (comments.length > 0) { | ||
| lines.push('## Comments'); | ||
| lines.push(''); | ||
| for (const comment of comments) { | ||
| const author = comment.author?.login ?? 'unknown'; | ||
| const ts = comment.createdAt ? ` (${comment.createdAt})` : ''; | ||
| const body = | ||
| (comment.body ?? '').length > 300 | ||
| ? `${comment.body!.substring(0, 300)}…` | ||
| : (comment.body ?? ''); | ||
| lines.push(`${author}${ts}:`); | ||
| lines.push(` ${body}`); | ||
| lines.push(''); | ||
| } | ||
| } | ||
|
|
||
| logDebugEvent('gh_pr_review.success', { | ||
| pr, | ||
| comments: comments.length, | ||
| reviews: substantiveReviews.length, | ||
| }); | ||
|
|
||
| return lines.join('\n').trimEnd(); | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| logDebugEvent('gh_pr_review.error', { error: msg }); | ||
| return `Error fetching PR comments: ${msg}`; | ||
| } | ||
| }, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1:
after_line: -1is placed at the wrong position in prepend mode. It can insert at the beginning (or after the last content line) instead of before the last line.Prompt for AI agents