23 Commits

Author SHA1 Message Date
e3d38d2df8 Updated readme
All checks were successful
Publish Library / Build NPM Project (push) Successful in 5s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 16:00:07 -05:00
9ff7beee1e Fixed output
All checks were successful
Publish Library / Build NPM Project (push) Successful in 6s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 15:56:44 -05:00
8cfcb3f95c Added checks
All checks were successful
Publish Library / Build NPM Project (push) Successful in 4s
Publish Library / Tag Version (push) Successful in 16s
Code review / review (pull_request) Successful in 38s
2026-01-14 15:56:07 -05:00
e625782eec Removed --no-cache, we are already using latest tag
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 5s
Code review / review (pull_request) Successful in 40s
2026-01-14 15:47:54 -05:00
72ffe3dcc7 Added a release bot to create release notes from closed milestones
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 15:43:53 -05:00
1ab97c2676 Merge pull request 'review-labels' (#14) from review-labels into master
All checks were successful
Publish Library / Build NPM Project (push) Successful in 8s
Publish Library / Tag Version (push) Successful in 7s
Reviewed-on: #14
2026-01-14 14:06:21 -05:00
7447204351 Fixed more logic checks
Some checks failed
Publish Library / Build NPM Project (push) Successful in 4s
Code review / review (pull_request) Has been cancelled
Publish Library / Tag Version (push) Successful in 6s
2026-01-14 14:05:41 -05:00
3b01e1bfc1 Fixed label condition check 💀
All checks were successful
Publish Library / Build NPM Project (push) Successful in 9s
Publish Library / Tag Version (push) Successful in 23s
Code review / review (pull_request) Successful in 49s
2026-01-14 13:51:41 -05:00
1460c3a0ae Added PR info as context to AI
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 5s
Code review / review (pull_request) Successful in 46s
2026-01-14 13:44:31 -05:00
ebc3da8605 Merge branch 'master' of git.zakscode.com:ztimson/ai-agents into review-labels
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 13:39:50 -05:00
677f84c97a Added label to enable review bot on PRs
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 13:38:20 -05:00
f543e08e36 Merge pull request 'Added configurable labels to ticket refiner' (#13) from ticket-duplicates into master
All checks were successful
Publish Library / Build NPM Project (push) Successful in 8s
Publish Library / Tag Version (push) Successful in 5s
Reviewed-on: #13
2026-01-14 13:22:33 -05:00
5b9f8e0e13 Fixed minor bugs and typos
All checks were successful
Code review / review (pull_request) Successful in 51s
Publish Library / Build NPM Project (push) Successful in 4s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 13:14:03 -05:00
019b05105a Merge branch 'master' of git.zakscode.com:ztimson/ai-agents into ticket-duplicates
All checks were successful
Publish Library / Build NPM Project (push) Successful in 4s
Publish Library / Tag Version (push) Successful in 16s
Code review / review (pull_request) Successful in 51s
2026-01-14 13:11:07 -05:00
decd533e4e Added configurable labels to ticket refiner
All checks were successful
Publish Library / Build NPM Project (push) Successful in 5s
Publish Library / Tag Version (push) Successful in 7s
Code review / review (pull_request) Successful in 48s
2026-01-14 13:05:16 -05:00
7becf99be2 Update .github/issue_template/ai-refinement.md
All checks were successful
Publish Library / Build NPM Project (push) Successful in 8s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 12:11:07 -05:00
99a1e55471 Merge pull request 'Check for duplicates before adding tickets' (#12) from ticket-duplicates into master
All checks were successful
Publish Library / Build NPM Project (push) Successful in 6s
Publish Library / Tag Version (push) Successful in 7s
Reviewed-on: #12
2025-12-31 00:02:18 -05:00
23cb66544e Fixed more bugs
Some checks failed
Publish Library / Build NPM Project (push) Successful in 3s
Code review / review (pull_request) Has been cancelled
Publish Library / Tag Version (push) Successful in 7s
2025-12-31 00:01:55 -05:00
9e5372f37b Fixed more bugs
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 13s
Code review / review (pull_request) Successful in 58s
2025-12-30 23:55:21 -05:00
eb4486f196 Fixed bugs
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 15s
Code review / review (pull_request) Successful in 1m30s
2025-12-30 23:42:17 -05:00
f3df34ec47 Merge branch 'master' of git.zakscode.com:ztimson/ai-agents into ticket-duplicates
All checks were successful
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 16s
Code review / review (pull_request) Successful in 48s
2025-12-30 23:34:56 -05:00
e62e11fb75 Merge branch 'master' of git.zakscode.com:ztimson/ai-agents into ticket-duplicates
Some checks failed
Code review / review (pull_request) Failing after 15s
Publish Library / Build NPM Project (push) Successful in 3s
Publish Library / Tag Version (push) Successful in 11s
2025-12-30 23:25:30 -05:00
8add830d2b Check for duplicates before adding tickets
Some checks failed
Publish Library / Build NPM Project (push) Successful in 14s
Code review / review (pull_request) Failing after 28s
Publish Library / Tag Version (push) Successful in 27s
2025-12-30 23:17:45 -05:00
10 changed files with 241 additions and 56 deletions

View File

@@ -16,4 +16,4 @@ How can it be fixed or improved?
Steps to reproduce? Steps to reproduce?
Anything other useful information, logs or screenshots? Any other useful information? Logs, screenshots, steps to reproduce?

View File

@@ -9,7 +9,5 @@
## Checklist ## Checklist
<!-- Complete after creating PR --> <!-- Complete after creating PR -->
- [ ] Linked issues - [ ] Reviewed changes (or use `Review/AI` label)
- [ ] Reviewed changes
- [ ] Updated comments/documentation - [ ] Updated comments/documentation

View File

@@ -2,7 +2,7 @@ name: Code review
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened, labeled]
jobs: jobs:
review: review:
@@ -15,7 +15,7 @@ jobs:
git checkout ${{ github.event.pull_request.head.sha }} git checkout ${{ github.event.pull_request.head.sha }}
git fetch origin ${{ github.event.pull_request.base.ref }} git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Run AI Review - name: Create review
run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE run: npx -y -p @ztimson/ai-agents@latest review $GITHUB_WORKSPACE
env: env:
AI_HOST: anthropic AI_HOST: anthropic

27
.github/workflows/release-creator.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Release Bot
on:
milestone:
types: [closed]
jobs:
release:
runs-on: ubuntu-latest
container: node:22
steps:
- name: Checkout
run: |
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
git checkout ${{ github.event.repository.default_branch }}
- name: Create release
run: npx -y @ztimson/ai-agents@latest release
env:
AI_HOST: anthropic
AI_MODEL: claude-sonnet-4-5
AI_TOKEN: ${{ secrets.ANTHROPIC_TOKEN }}
GIT_HOST: ${{ github.server_url }}
GIT_OWNER: ${{ github.repository_owner }}
GIT_REPO: ${{ github.event.repository.name }}
GIT_TOKEN: ${{ secrets.ASSISTANT_TOKEN }}
MILESTONE: ${{ github.event.milestone.number }}

View File

@@ -3,6 +3,7 @@ name: Ticket refinement
on: on:
issues: issues:
types: [labeled] types: [labeled]
jobs: jobs:
format: format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -13,7 +14,7 @@ jobs:
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" . git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
git checkout ${{ github.event.repository.default_branch }} git checkout ${{ github.event.repository.default_branch }}
- name: Run AI Formatter - name: Refine ticket
run: npx -y -p @ztimson/ai-agents@latest refine run: npx -y -p @ztimson/ai-agents@latest refine
env: env:
AI_HOST: anthropic AI_HOST: anthropic

View File

@@ -37,9 +37,12 @@ AI-powered Gitea agents for automating reviews and administration
## About ## About
Only supports Gitea AI-powered Gitea agents for automating administration of code repos:
- Code Review
- Release Notes
- Ticket Refinement
Use LLM models from Anthropic, OpenAI, or Ollama to automate ticket refinement, code reviews, and releases. Only supports Gitea, copy the relevant `.github/workflows/______.yml` action to start using it
### Built With ### Built With
[![Docker](https://img.shields.io/badge/Docker-384d54?style=for-the-badge&logo=docker)](https://docker.com/) [![Docker](https://img.shields.io/badge/Docker-384d54?style=for-the-badge&logo=docker)](https://docker.com/)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ztimson/ai-agents", "name": "@ztimson/ai-agents",
"version": "0.0.8", "version": "0.1.3",
"description": "AI agents", "description": "AI agents",
"keywords": ["ai", "review"], "keywords": ["ai", "review"],
"author": "ztimson", "author": "ztimson",
@@ -8,6 +8,7 @@
"type": "module", "type": "module",
"bin": { "bin": {
"refine": "./src/refine.mjs", "refine": "./src/refine.mjs",
"release": "./src/release.mjs",
"review": "./src/review.mjs" "review": "./src/review.mjs"
}, },
"dependencies": { "dependencies": {

View File

@@ -6,8 +6,8 @@ import * as dotenv from 'dotenv';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
dotenv.config({quiet: true}); dotenv.config({quiet: true, debug: false});
dotenv.config({path: '.env.local', override: true, quiet: true}); dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
(async () => { (async () => {
let p = process.argv[process.argv.length - 1]; let p = process.argv[process.argv.length - 1];
@@ -21,22 +21,28 @@ dotenv.config({path: '.env.local', override: true, quiet: true});
ticket = process.env['TICKET'], ticket = process.env['TICKET'],
host = process.env['AI_HOST'], host = process.env['AI_HOST'],
model = process.env['AI_MODEL'], model = process.env['AI_MODEL'],
labelDupe = process.env['LABELS_DUPE'] || 'Review/Duplicate',
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
labelsReq = process.env['LABELS_REQ'] || 'Kind/Aesthetic,Kind/Bug,Kind/DevOps,Kind/Document,Kind/Enhancement,Kind/Refactor,Kind/Security',
labelsOpt = process.env['LABELS_OPT'] || 'Breaking,Priority,QA',
token = process.env['AI_TOKEN']; token = process.env['AI_TOKEN'];
console.log(`Processing issue #${ticket}`); console.log(`Processing issue #${ticket}`);
// Fetch issue // Fetch issue
const issueRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { const issueData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
headers: {'Authorization': `token ${auth}`} headers: {'Authorization': `token ${auth}`}
}).then(async resp => {
if(resp.ok) return resp.json();
else throw new Error(`${resp.status} ${await resp.text()}`);
}); });
if(!issueRes.ok) throw new Error(`${issueRes.status} ${await issueRes.text()}`); if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
const issueData = await issueRes.json();
if(!issueData.labels?.some(l => l.name === 'Review/AI')) {
console.log('Skipping'); console.log('Skipping');
return process.exit(); return process.exit();
} }
let title = '', type = '', readme = '', readmeP = path.join(process.cwd(), 'README.md'); // Gather readme & template
let title = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md');
if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8'); if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
const template = p ? fs.readFileSync(p, 'utf-8') : `## Description const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
@@ -80,6 +86,7 @@ Implementation details, constraints, dependencies, design decisions
| **Total** | **0-15** | | **Total** | **0-15** |
`; `;
// Create AI
let options = {ollama: {model, host}}; let options = {ollama: {model, host}};
if(host === 'anthropic') options = {anthropic: {model, token}}; if(host === 'anthropic') options = {anthropic: {model, token}};
else if(host === 'openai') options = {openAi: {model, token}}; else if(host === 'openai') options = {openAi: {model, token}};
@@ -93,18 +100,27 @@ Implementation details, constraints, dependencies, design decisions
args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}}, args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
fn: (args) => title = args.title fn: (args) => title = args.title
}, { }, {
name: 'type', name: 'add_label',
description: 'Set the ticket type, must be called EXACTLY ONCE', description: 'Add a label to the ticket',
args: {type: {type: 'string', description: 'Ticket type', enum: ['Bug', 'DevOps', 'Document', 'Enhancement', 'Refactor', 'Security'], required: true}}, args: {label: {type: 'string', description: 'Label name', required: true}},
fn: (args) => type = args.type fn: async (args) => {
labels.push(args.label);
return await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"labels":["${args.label}"]}`
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
}
}], }],
system: `Transform raw tickets into structured markdown following the template EXACTLY. system: `Transform raw tickets into structured markdown following the template EXACTLY.
**MANDATORY STEPS:** **MANDATORY STEPS:**
1. Identify ticket type: Bug, DevOps, Document, Enhancement, Refactor, or Security 1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
2. Call \`type\` tool EXACTLY ONCE with the type from step 1 2. Identify one label from each group which best applies to the ticket: ${labelsReq.replaceAll(',', ', ')}
3. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads) 3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
4. Output formatted markdown matching template structure below 4. Filter the following labels to any that apply to this ticket: ${labelsOpt.replaceAll(',', ', ')}
5. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
6. Output the new ticket description in formatted markdown matching the following rules:
**TEMPLATE RULES:** **TEMPLATE RULES:**
- Use ## headers (match template exactly) - Use ## headers (match template exactly)
@@ -125,11 +141,11 @@ Implementation details, constraints, dependencies, design decisions
| **Total** | **0-15** | | **Total** | **0-15** |
**SCORING:** **SCORING:**
- Size: # of modules/layers/files changed - Size: # of modules/layers/files affected
- Complexity: Technical difficulty - Complexity: Technical difficulty
- Unknowns: Research/uncertainty needed - Unknowns: Research/uncertainty needed
**README:** **PROJECT README:**
\`\`\`markdown \`\`\`markdown
${readme.trim() || 'No README available'} ${readme.trim() || 'No README available'}
\`\`\` \`\`\`
@@ -141,35 +157,61 @@ ${template.trim()}
Output ONLY markdown. No explanations, labels, or extra formatting.`}); Output ONLY markdown. No explanations, labels, or extra formatting.`});
// Format ticket with AI
const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []); const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);
const body = messages?.pop()?.content; const body = messages?.pop()?.content;
if(!body) { if(!body) throw new Error('Invalid response from AI');
console.log('Invalid response from AI');
return process.exit(1); // Check for duplicates
} const repoInfo = await fetch(`${git}/api/v1/repos/${owner}/${repo}`, {headers: {'Authorization': `token ${auth}`},}).then(resp => resp.ok ? resp.json() : null);
const updateRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { const search = await fetch(`${git}/api/v1/repos/issues/search`, {
method: 'PATCH',
headers: {
'Authorization': `token ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
body,
})
});
if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`);
if(type) {
const resp = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST', method: 'POST',
headers: { headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
'Authorization': `token ${auth}`, body: JSON.stringify({
'Content-Type': 'application/json' owner,
}, priority_repo_id: repoInfo.id,
body: JSON.stringify({labels: [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`]}) type: 'issues',
}); limit: 3,
if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); q: title
})
}).then(resp => resp.ok ? resp.json() : []);
let dupeId = null;
const dupeIds = search.map(t => t.id);
const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
const hasDuplicates = (await ai.language.ask(`ID: ${issueData.id}\nTitle: ${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
system: `Your job is to identify duplicates. Respond ONLY with the duplicate's ID number or "NONE" if no match exists\n\n${dupes}`
}))?.pop()?.content;
// Handle duplicates
if(hasDuplicates && !hasDuplicates.toUpperCase().includes('NONE') && (dupeId = dupeIds.find(id => id == hasDuplicates.trim())) != null && dupeId != issueData.id) {
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"body": "Duplicate of #${dupeId}"}`
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"labels":["${labelDupe}"]}`
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
method: 'PATCH',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: '{"state": "closed"}'
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
console.log('Duplicate');
return process.exit();
} }
console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`); // Update ticket
})(); await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
method: 'PATCH',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: JSON.stringify({title, body})
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`);
})().catch(err => {
console.error(`Error: ${err.message || err.toString()}`);
process.exit(1);
});

90
src/release.mjs Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
import {Ai} from '@ztimson/ai';
import * as dotenv from 'dotenv';
import {$} from '@ztimson/node-utils';
dotenv.config({quiet: true, debug: false});
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
(async () => {
const git = process.env['GIT_HOST'],
owner = process.env['GIT_OWNER'],
repo = process.env['GIT_REPO'],
auth = process.env['GIT_TOKEN'],
milestone = process.env['MILESTONE'],
host = process.env['AI_HOST'] || 'ollama',
model = process.env['AI_MODEL'] || 'llama3',
token = process.env['AI_TOKEN'];
// Get milestone info
const milestoneData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/milestones/${milestone}`, {
headers: {'Authorization': `token ${auth}`}
}).then(resp => resp.ok ? resp.json() : {});
// Get closed issues
const issues = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues?state=closed&milestone=${milestone}`, {
headers: {'Authorization': `token ${auth}`}
}).then(resp => resp.ok ? resp.json() : []);
// Get closed PRs
const prs = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls?state=closed&milestone=${milestone}`, {
headers: {'Authorization': `token ${auth}`}
}).then(resp => resp.ok ? resp.json() : []);
// Get latest tag
const latestTag = await $`git describe --tags --abbrev=0`.text();
if(!latestTag) {
console.error('At least one Git tag is required');
process.exit(1);
}
// Build context
let context = `Milestone: ${milestoneData.title}
Description:
\`\`\`md
${milestoneData.description || ''}
\`\`\`
PRs:
${prs.map(pr => `- ${pr.title}\n${pr.body || ''}`).join('\n\n')}
Issues:
${issues.filter(i => !i.pull_request).map(i => `- ${i.title}\n${i.body || ''}`).join('\n\n')}`;
// Generate release notes
let options = {ollama: {model, host}};
if(host === 'anthropic') options = {anthropic: {model, token}};
else if(host === 'openai') options = {openAi: {model, token}};
const ai = new Ai({
...options,
model: [host, model],
system: `You are a release notes writer. Format the provided milestone info, PRs, and issues into clean, organized release notes. Use markdown with sections like "Features", "Bug Fixes", "Breaking Changes", etc. Be concise but informative. Include issue/PR numbers in format #123.`
});
const body = (await ai.chat(context)).pop()?.content;
if(!body) {
console.error('No release notes were generated');
process.exit(1);
}
// Create release
const name = latestTag.trim();
await fetch(`${git}/api/v1/repos/${owner}/${repo}/releases`, {
method: 'POST',
headers: {
'Authorization': `token ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
tag_name: name,
body
})
}).then(resp => { if(!resp.ok) throw new Error(resp.status + ' ' + resp.statusText) });
console.log(`Title: ${name}\nDescription:\n${body}`);
})().catch(err => {
console.error(`Error: ${err.message || err.toString()}`);
process.exit(1);
});

View File

@@ -19,12 +19,23 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
owner = process.env['GIT_OWNER'], owner = process.env['GIT_OWNER'],
repo = process.env['GIT_REPO'], repo = process.env['GIT_REPO'],
auth = process.env['GIT_TOKEN'], auth = process.env['GIT_TOKEN'],
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
pr = process.env['PULL_REQUEST'], pr = process.env['PULL_REQUEST'],
host = process.env['AI_HOST'], host = process.env['AI_HOST'],
model = process.env['AI_MODEL'], model = process.env['AI_MODEL'],
token = process.env['AI_TOKEN']; token = process.env['AI_TOKEN'];
console.log(`Reviewing: ${root}\n`); console.log(`Reviewing: ${root}`);
const info = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}`)
.then(async resp => {
if(resp.ok) return resp.json();
throw new Error(`${resp.status} ${await resp.text()}`);
});
if(!info.labels.some(l => l.name === labelEnabled)) {
console.log('Skipping');
return process.exit();
}
const branch = process.env['GIT_BRANCH'] || await $`cd ${root} && git symbolic-ref refs/remotes/origin/HEAD`; const branch = process.env['GIT_BRANCH'] || await $`cd ${root} && git symbolic-ref refs/remotes/origin/HEAD`;
const comments = []; const comments = [];
const commit = await $`cd ${root} && git log -1 --pretty=format:%H`; const commit = await $`cd ${root} && git log -1 --pretty=format:%H`;
@@ -53,7 +64,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
...options, ...options,
model: [host, model], model: [host, model],
path: process.env['path'] || os.tmpdir(), path: process.env['path'] || os.tmpdir(),
system: `You are a code reviewer. Analyze the git diff and use the \`recommend\` tool for EACH issue you find. You must call \`recommend\` exactly once for every bug or improvement opportunity directly related to changes. Ignore formatting recommendations. After making all recommendations, provide some concluding remarks about the overall state of the changes.${existingComments}`, system: `You are a code reviewer. Analyze the git diff and use the \`recommend\` tool for EACH issue you find. You must call \`recommend\` exactly once for every bug or improvement opportunity directly related to changes. Ignore formatting recommendations. After making all recommendations, provide a quick 75 words or less sitrep.${existingComments}`,
tools: [{ tools: [{
name: 'read_file', name: 'read_file',
description: 'Read contents of a file', description: 'Read contents of a file',
@@ -87,7 +98,16 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
}] }]
}); });
const messages = await ai.language.ask(gitDiff); const messages = await ai.language.ask(`Title: ${info.title || 'None'}
Description:
\`\`\`md
${info.body || 'None'}
\`\`\`
Git Diff:
\`\`\`
${gitDiff}
\`\`\``);
const summary = messages.pop().content; const summary = messages.pop().content;
if(git) { if(git) {
const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, { const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {
@@ -106,4 +126,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
if(!res.ok) throw new Error(`${res.status} ${await res.text()}`); if(!res.ok) throw new Error(`${res.status} ${await res.text()}`);
} }
console.log(comments.map(c => `${c.path}${c.new_position ? `:${c.new_position}` : ''}\n${c.body}`).join('\n\n') + '\n\n' + summary); console.log(comments.map(c => `${c.path}${c.new_position ? `:${c.new_position}` : ''}\n${c.body}`).join('\n\n') + '\n\n' + summary);
})(); })().catch(err => {
console.error(`Error: ${err.message || err.toString()}`);
process.exit(1);
});