From 8add830d2b1b2229a030332ab2f3b85022df10b7 Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 23:17:45 -0500 Subject: [PATCH 1/4] Check for duplicates before adding tickets --- src/refine.mjs | 78 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/refine.mjs b/src/refine.mjs index 33759f4..5c19dc7 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -28,14 +28,14 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); // Fetch issue const issueRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { headers: {'Authorization': `token ${auth}`} - }); - if(!issueRes.ok) throw new Error(`${issueRes.status} ${await issueRes.text()}`); + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); const issueData = await issueRes.json(); - if(!issueData.labels?.some(l => l.name === 'Review/AI')) { + if(issueData.labels[0] !== 'Review/AI' || issueData.labels.length !== 1) { console.log('Skipping'); return process.exit(); } + // Gather readme & template let title = '', type = '', 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 +80,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}}; @@ -141,34 +142,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); + if(!body) throw new Error('Invalid response from AI'); + + // Check for duplicates + const search = await fetch(`${git}/api/v1/repos/issues/search`, { + method: 'POST', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: JSON.stringify({ + priority_repo_id: repo, + 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(`${title}\n\`\`\`markdown\n${body}\n\`\`\``, { + system: `Your job is to identify duplicates. Respond with the ID number of the duplicate or nothing if there are no matches \n\n${dupes}` + }))?.pop()?.content; + // Handle duplicates + if(!!hasDuplicates && (dupeId = dupeIds.find(id => hasDuplicates.includes(id.toString()))) != null) { + 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 => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { + method: 'POST', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: '{"labels":["Reviewed/Duplicate"]}' + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + 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 => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + console.log('Duplicate'); + return process.exit(); } + + // Update ticket 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`, { + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: JSON.stringify({title, body}) + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + if(type) { // Label + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { 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: `{"labels":["Reviewed/${type[0].toUpperCase() + type.slice(1).toLowerCase()}"]}` + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); } console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`); -- 2.49.1 From eb4486f196db535b4e3b6546015736fa6af23fe8 Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 23:42:17 -0500 Subject: [PATCH 2/4] Fixed bugs --- src/refine.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/refine.mjs b/src/refine.mjs index 5c19dc7..e297f4f 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -26,11 +26,13 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); 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 => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); - const issueData = await issueRes.json(); - if(issueData.labels[0] !== 'Review/AI' || issueData.labels.length !== 1) { + }).then(async resp => { + if(resp.ok) return resp.json(); + else throw new Error(`${resp.status} ${await resp.text()}`); + }); + if(issueData.labels.length !== 1 || issueData.labels?.[0] !== 'Review/AI') { console.log('Skipping'); return process.exit(); } @@ -170,17 +172,17 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); method: 'POST', headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, body: `{"body": "Duplicate of #${dupeId}"}` - }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + }).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":["Reviewed/Duplicate"]}' - }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + }).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 => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); }); console.log('Duplicate'); return process.exit(); } @@ -190,13 +192,13 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); method: 'PATCH', headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, body: JSON.stringify({title, body}) - }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); }); if(type) { // Label await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { method: 'POST', headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, body: `{"labels":["Reviewed/${type[0].toUpperCase() + type.slice(1).toLowerCase()}"]}` - }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); }); } console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`); -- 2.49.1 From 9e5372f37b10011fcfc72131dcfe4beaf115a399 Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 23:55:21 -0500 Subject: [PATCH 3/4] Fixed more bugs --- package.json | 2 +- src/refine.mjs | 15 ++++++++++----- src/review.mjs | 5 ++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c1c6f33..9826270 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/ai-agents", - "version": "0.0.8", + "version": "0.1.0", "description": "AI agents", "keywords": ["ai", "review"], "author": "ztimson", diff --git a/src/refine.mjs b/src/refine.mjs index e297f4f..6046713 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -32,7 +32,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); if(resp.ok) return resp.json(); else throw new Error(`${resp.status} ${await resp.text()}`); }); - if(issueData.labels.length !== 1 || issueData.labels?.[0] !== 'Review/AI') { + if(issueData.labels?.[0]?.name !== 1 || issueData.labels?.[0] !== 'Review/AI') { console.log('Skipping'); return process.exit(); } @@ -150,11 +150,13 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); 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({ - priority_repo_id: repo, + owner, + priority_repo_id: repoInfo.id, type: 'issues', limit: 3, q: title @@ -167,7 +169,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); system: `Your job is to identify duplicates. Respond with the ID number of the duplicate or nothing if there are no matches \n\n${dupes}` }))?.pop()?.content; // Handle duplicates - if(!!hasDuplicates && (dupeId = dupeIds.find(id => hasDuplicates.includes(id.toString()))) != null) { + if(!!hasDuplicates && (dupeId = dupeIds.find(id => new RegExp(`(^| )${id}( |$)`, 'm').test(hasDuplicates)))) { await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, { method: 'POST', headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, @@ -188,7 +190,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); } // Update ticket - const updateRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${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}) @@ -202,4 +204,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); } console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`); -})(); +})().catch(err => { + console.error(`Error: ${err.message || err.toString()}`); + process.exit(1); +}); diff --git a/src/review.mjs b/src/review.mjs index cd071e6..e1c87a3 100644 --- a/src/review.mjs +++ b/src/review.mjs @@ -106,4 +106,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); +}); -- 2.49.1 From 23cb66544e8de984080a0461c32382f26213dc5d Mon Sep 17 00:00:00 2001 From: ztimson Date: Wed, 31 Dec 2025 00:01:55 -0500 Subject: [PATCH 4/4] Fixed more bugs --- src/refine.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refine.mjs b/src/refine.mjs index 6046713..d61cc01 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -32,7 +32,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); if(resp.ok) return resp.json(); else throw new Error(`${resp.status} ${await resp.text()}`); }); - if(issueData.labels?.[0]?.name !== 1 || issueData.labels?.[0] !== 'Review/AI') { + if(issueData.labels?.[0] !== 1 || issueData.labels?.[0]?.name !== 'Review/AI') { console.log('Skipping'); return process.exit(); } @@ -169,7 +169,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`}); system: `Your job is to identify duplicates. Respond with the ID number of the duplicate or nothing if there are no matches \n\n${dupes}` }))?.pop()?.content; // Handle duplicates - if(!!hasDuplicates && (dupeId = dupeIds.find(id => new RegExp(`(^| )${id}( |$)`, 'm').test(hasDuplicates)))) { + if(!!hasDuplicates && (dupeId = dupeIds.find(id => new RegExp(`\\b${id}\\b`, 'm').test(hasDuplicates)))) { await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, { method: 'POST', headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, -- 2.49.1