#!/usr/bin/env node import {Ai} from '@ztimson/ai-utils'; import * as os from 'node:os'; 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}); (async () => { let p = process.argv[process.argv.length - 1]; if(p === 'refine' || p.endsWith('refine.mjs')) p = null; if(!/^(\/|[A-Z]:)/m.test(p)) p = path.join(process.cwd(), p); if(!p || !fs.existsSync(p)) throw new Error('Please provide a template'); const git = process.env['GIT_HOST'], owner = process.env['GIT_OWNER'], repo = process.env['GIT_REPO'], auth = process.env['GIT_TOKEN'], ticket = process.env['TICKET'], host = process.env['AI_HOST'], model = process.env['AI_MODEL'], token = process.env['AI_TOKEN']; console.log(`Processing issue #${ticket}`); // 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()}`); const issueData = await issueRes.json(); if(!issueData.labels?.some(l => l.name === 'Review/AI')) { console.log('Skipping'); return process.exit(); } let readme = '', readmeP = path.join(process.cwd(), 'README.md'); if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8'); const template = fs.readFileSync(p, 'utf-8'); 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], path: process.env['path'] || os.tmpdir(), system: `You are a ticket formatter. Transform raw issue descriptions into structured tickets. **CRITICAL RULES:** 1. Identify the ticket type (Bug, DevOps, Enhancement, Refactor, Security) 2. Output MUST only contain the new ticket information in markdown, no extra fluff 3. Follow the template structure EXACTLY: - Title format: [Module] - [Verb] [noun] Example: Storage - Fix file uploads - Fill in the identified ticket type - Write a clear description - For bugs: fill Steps to Reproduce with numbered list - For enhancements/refactors: REMOVE the Steps to Reproduce section entirely - Acceptance Criteria: convert requirements into checkboxes (- [ ]) - Weight scoring (0-5 each): * Size: Number of modules, layers & files affected by change * Complexity: Technical difficulty to implement * Unknowns: Research/uncertainty in work estimation * Calculate Total as sum of the three - Remove sections that are not applicable based on ticket type - Use proper markdown headers (##) **README:** \`\`\`markdown ${readme.trim() || 'No README available'} \`\`\` **TEMPLATE:** \`\`\`markdown ${template.trim()} \`\`\` Output ONLY the formatted ticket, no explanation.` }) const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []); const content = messages?.pop()?.content; if(!content) { console.log('Invalid response from AI'); return process.exit(1); } const title = /^# (.+)$/m.exec(content)?.[1] || issueData.title; const typeMatch = /^## Type:\s*(.+)$/m.exec(content); const type = typeMatch?.[1]?.split('/')[0]?.trim() || 'Unassigned'; const body = content.replace(/^# .+$/m, '').replace(/^## Type:.+$/m, '').trim(); 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, labels: type?.length ? [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`] : [] }) }); if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`); console.log(body); })();