generated from ztimson/template
Compare commits
21 Commits
f2936ae4dc
...
release-bo
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cfcb3f95c | |||
| e625782eec | |||
| 72ffe3dcc7 | |||
| 1ab97c2676 | |||
| 7447204351 | |||
| 3b01e1bfc1 | |||
| 1460c3a0ae | |||
| ebc3da8605 | |||
| 677f84c97a | |||
| f543e08e36 | |||
| 5b9f8e0e13 | |||
| 019b05105a | |||
| decd533e4e | |||
| 7becf99be2 | |||
| 99a1e55471 | |||
| 23cb66544e | |||
| 9e5372f37b | |||
| eb4486f196 | |||
| f3df34ec47 | |||
| e62e11fb75 | |||
| 8add830d2b |
2
.github/issue_template/ai-refinement.md
vendored
2
.github/issue_template/ai-refinement.md
vendored
@@ -16,4 +16,4 @@ How can it be fixed or improved?
|
||||
|
||||
Steps to reproduce?
|
||||
|
||||
Anything other useful information, logs or screenshots?
|
||||
Any other useful information? Logs, screenshots, steps to reproduce?
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -9,7 +9,5 @@
|
||||
|
||||
## Checklist
|
||||
<!-- Complete after creating PR -->
|
||||
- [ ] Linked issues
|
||||
- [ ] Reviewed changes
|
||||
- [ ] Reviewed changes (or use `Review/AI` label)
|
||||
- [ ] Updated comments/documentation
|
||||
|
||||
4
.github/workflows/code-review.yml
vendored
4
.github/workflows/code-review.yml
vendored
@@ -2,7 +2,7 @@ name: Code review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
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
|
||||
env:
|
||||
AI_HOST: anthropic
|
||||
|
||||
27
.github/workflows/release-creator.yml
vendored
Normal file
27
.github/workflows/release-creator.yml
vendored
Normal 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 }}
|
||||
3
.github/workflows/ticket-refinement.yml
vendored
3
.github/workflows/ticket-refinement.yml
vendored
@@ -3,6 +3,7 @@ name: Ticket refinement
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -13,7 +14,7 @@ jobs:
|
||||
git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
||||
git checkout ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Run AI Formatter
|
||||
- name: Refine ticket
|
||||
run: npx -y -p @ztimson/ai-agents@latest refine
|
||||
env:
|
||||
AI_HOST: anthropic
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ztimson/ai-agents",
|
||||
"version": "0.0.8",
|
||||
"version": "0.1.3",
|
||||
"description": "AI agents",
|
||||
"keywords": ["ai", "review"],
|
||||
"author": "ztimson",
|
||||
@@ -8,6 +8,7 @@
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"refine": "./src/refine.mjs",
|
||||
"release": "./src/release.mjs",
|
||||
"review": "./src/review.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
130
src/refine.mjs
130
src/refine.mjs
@@ -6,8 +6,8 @@ import * as dotenv from 'dotenv';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
dotenv.config({quiet: true});
|
||||
dotenv.config({path: '.env.local', override: true, quiet: true});
|
||||
dotenv.config({quiet: true, debug: false});
|
||||
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
||||
|
||||
(async () => {
|
||||
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'],
|
||||
host = process.env['AI_HOST'],
|
||||
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'];
|
||||
|
||||
console.log(`Processing issue #${ticket}`);
|
||||
|
||||
// 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}`}
|
||||
}).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()}`);
|
||||
const issueData = await issueRes.json();
|
||||
if(!issueData.labels?.some(l => l.name === 'Review/AI')) {
|
||||
if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
|
||||
console.log('Skipping');
|
||||
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');
|
||||
const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
|
||||
|
||||
@@ -80,6 +86,7 @@ Implementation details, constraints, dependencies, design decisions
|
||||
| **Total** | **0-15** |
|
||||
`;
|
||||
|
||||
// Create AI
|
||||
let options = {ollama: {model, host}};
|
||||
if(host === 'anthropic') options = {anthropic: {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}},
|
||||
fn: (args) => title = args.title
|
||||
}, {
|
||||
name: 'type',
|
||||
description: 'Set the ticket type, must be called EXACTLY ONCE',
|
||||
args: {type: {type: 'string', description: 'Ticket type', enum: ['Bug', 'DevOps', 'Document', 'Enhancement', 'Refactor', 'Security'], required: true}},
|
||||
fn: (args) => type = args.type
|
||||
name: 'add_label',
|
||||
description: 'Add a label to the ticket',
|
||||
args: {label: {type: 'string', description: 'Label name', required: true}},
|
||||
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.
|
||||
|
||||
**MANDATORY STEPS:**
|
||||
1. Identify ticket type: Bug, DevOps, Document, Enhancement, Refactor, or Security
|
||||
2. Call \`type\` tool EXACTLY ONCE with the type from step 1
|
||||
3. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
|
||||
4. Output formatted markdown matching template structure below
|
||||
1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
|
||||
2. Identify one label from each group which best applies to the ticket: ${labelsReq.replaceAll(',', ', ')}
|
||||
3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
|
||||
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:**
|
||||
- Use ## headers (match template exactly)
|
||||
@@ -125,11 +141,11 @@ Implementation details, constraints, dependencies, design decisions
|
||||
| **Total** | **0-15** |
|
||||
|
||||
**SCORING:**
|
||||
- Size: # of modules/layers/files changed
|
||||
- Size: # of modules/layers/files affected
|
||||
- Complexity: Technical difficulty
|
||||
- Unknowns: Research/uncertainty needed
|
||||
|
||||
**README:**
|
||||
**PROJECT README:**
|
||||
\`\`\`markdown
|
||||
${readme.trim() || 'No README available'}
|
||||
\`\`\`
|
||||
@@ -141,35 +157,61 @@ ${template.trim()}
|
||||
|
||||
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 body = messages?.pop()?.content;
|
||||
if(!body) {
|
||||
console.log('Invalid response from AI');
|
||||
return process.exit(1);
|
||||
}
|
||||
const updateRes = 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,
|
||||
})
|
||||
});
|
||||
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`, {
|
||||
if(!body) throw new Error('Invalid response from AI');
|
||||
|
||||
// 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 search = await fetch(`${git}/api/v1/repos/issues/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `token ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({labels: [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`]})
|
||||
});
|
||||
if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`);
|
||||
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
owner,
|
||||
priority_repo_id: repoInfo.id,
|
||||
type: 'issues',
|
||||
limit: 3,
|
||||
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
90
src/release.mjs
Normal 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);
|
||||
});
|
||||
@@ -19,12 +19,23 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
||||
owner = process.env['GIT_OWNER'],
|
||||
repo = process.env['GIT_REPO'],
|
||||
auth = process.env['GIT_TOKEN'],
|
||||
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
|
||||
pr = process.env['PULL_REQUEST'],
|
||||
host = process.env['AI_HOST'],
|
||||
model = process.env['AI_MODEL'],
|
||||
token = process.env['AI_TOKEN'];
|
||||
|
||||
console.log(`Reviewing: ${root}\n`);
|
||||
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 comments = [];
|
||||
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,
|
||||
model: [host, model],
|
||||
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: [{
|
||||
name: 'read_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;
|
||||
if(git) {
|
||||
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()}`);
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user