@ztimson/ai-agents 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztimson/ai-agents",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "description": "AI agents",
5
5
  "keywords": ["ai", "review"],
6
6
  "author": "ztimson",
package/src/refine.mjs CHANGED
@@ -6,8 +6,8 @@ import * as dotenv from 'dotenv';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
 
9
- dotenv.config({quiet: true});
10
- dotenv.config({path: '.env.local', override: true, quiet: true});
9
+ dotenv.config({quiet: true, debug: false});
10
+ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
11
11
 
12
12
  (async () => {
13
13
  let p = process.argv[process.argv.length - 1];
@@ -21,22 +21,28 @@ dotenv.config({path: '.env.local', override: true, quiet: true});
21
21
  ticket = process.env['TICKET'],
22
22
  host = process.env['AI_HOST'],
23
23
  model = process.env['AI_MODEL'],
24
+ labelDupe = process.env['LABELS_DUPE'] || 'Review/Duplicate',
25
+ labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
26
+ labelsReq = process.env['LABELS_REQ'] || 'Kind/Aesthetic,Kind/Bug,Kind/DevOps,Kind/Document,Kind/Enhancement,Kind/Refactor,Kind/Security',
27
+ labelsOpt = process.env['LABELS_OPT'] || 'Breaking,Priority,QA',
24
28
  token = process.env['AI_TOKEN'];
25
29
 
26
30
  console.log(`Processing issue #${ticket}`);
27
31
 
28
32
  // Fetch issue
29
- const issueRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
33
+ const issueData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
30
34
  headers: {'Authorization': `token ${auth}`}
35
+ }).then(async resp => {
36
+ if(resp.ok) return resp.json();
37
+ else throw new Error(`${resp.status} ${await resp.text()}`);
31
38
  });
32
- if(!issueRes.ok) throw new Error(`${issueRes.status} ${await issueRes.text()}`);
33
- const issueData = await issueRes.json();
34
- if(!issueData.labels?.some(l => l.name === 'Review/AI')) {
39
+ if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
35
40
  console.log('Skipping');
36
41
  return process.exit();
37
42
  }
38
43
 
39
- let title = '', type = '', readme = '', readmeP = path.join(process.cwd(), 'README.md');
44
+ // Gather readme & template
45
+ let title = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md');
40
46
  if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
41
47
  const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
42
48
 
@@ -80,6 +86,7 @@ Implementation details, constraints, dependencies, design decisions
80
86
  | **Total** | **0-15** |
81
87
  `;
82
88
 
89
+ // Create AI
83
90
  let options = {ollama: {model, host}};
84
91
  if(host === 'anthropic') options = {anthropic: {model, token}};
85
92
  else if(host === 'openai') options = {openAi: {model, token}};
@@ -93,18 +100,27 @@ Implementation details, constraints, dependencies, design decisions
93
100
  args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
94
101
  fn: (args) => title = args.title
95
102
  }, {
96
- name: 'type',
97
- description: 'Set the ticket type, must be called EXACTLY ONCE',
98
- args: {type: {type: 'string', description: 'Ticket type', enum: ['Bug', 'DevOps', 'Document', 'Enhancement', 'Refactor', 'Security'], required: true}},
99
- fn: (args) => type = args.type
103
+ name: 'add_label',
104
+ description: 'Add a label to the ticket',
105
+ args: {label: {type: 'string', description: 'Label name', required: true}},
106
+ fn: async (args) => {
107
+ labels.push(args.label);
108
+ return await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
109
+ method: 'POST',
110
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
111
+ body: `{"labels":["${args.label}"]}`
112
+ }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
113
+ }
100
114
  }],
101
115
  system: `Transform raw tickets into structured markdown following the template EXACTLY.
102
116
 
103
117
  **MANDATORY STEPS:**
104
- 1. Identify ticket type: Bug, DevOps, Document, Enhancement, Refactor, or Security
105
- 2. Call \`type\` tool EXACTLY ONCE with the type from step 1
106
- 3. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
107
- 4. Output formatted markdown matching template structure below
118
+ 1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
119
+ 2. Identify one label from each group which best applies to the ticket: ${labelsReq.replaceAll(',', ', ')}
120
+ 3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
121
+ 4. Filter the following labels to any that apply to this ticket: ${labelsOpt.replaceAll(',', ', ')}
122
+ 5. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
123
+ 6. Output the new ticket description in formatted markdown matching the following rules:
108
124
 
109
125
  **TEMPLATE RULES:**
110
126
  - Use ## headers (match template exactly)
@@ -125,11 +141,11 @@ Implementation details, constraints, dependencies, design decisions
125
141
  | **Total** | **0-15** |
126
142
 
127
143
  **SCORING:**
128
- - Size: # of modules/layers/files changed
144
+ - Size: # of modules/layers/files affected
129
145
  - Complexity: Technical difficulty
130
146
  - Unknowns: Research/uncertainty needed
131
147
 
132
- **README:**
148
+ **PROJECT README:**
133
149
  \`\`\`markdown
134
150
  ${readme.trim() || 'No README available'}
135
151
  \`\`\`
@@ -141,35 +157,60 @@ ${template.trim()}
141
157
 
142
158
  Output ONLY markdown. No explanations, labels, or extra formatting.`});
143
159
 
160
+ // Format ticket with AI
144
161
  const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);
145
162
  const body = messages?.pop()?.content;
146
- if(!body) {
147
- console.log('Invalid response from AI');
148
- return process.exit(1);
149
- }
150
- const updateRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
151
- method: 'PATCH',
152
- headers: {
153
- 'Authorization': `token ${auth}`,
154
- 'Content-Type': 'application/json'
155
- },
163
+ if(!body) throw new Error('Invalid response from AI');
164
+
165
+ // Check for duplicates
166
+ const repoInfo = await fetch(`${git}/api/v1/repos/${owner}/${repo}`, {headers: {'Authorization': `token ${auth}`},}).then(resp => resp.ok ? resp.json() : null);
167
+ const search = await fetch(`${git}/api/v1/repos/issues/search`, {
168
+ method: 'POST',
169
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
156
170
  body: JSON.stringify({
157
- title,
158
- body,
171
+ owner,
172
+ priority_repo_id: repoInfo.id,
173
+ type: 'issues',
174
+ limit: 3,
175
+ q: title
159
176
  })
160
- });
161
- if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`);
162
- if(type) {
163
- const resp = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
177
+ }).then(resp => resp.ok ? resp.json() : []);
178
+ let dupeId = null;
179
+ const dupeIds = search.map(t => t.id);
180
+ const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
181
+ const hasDuplicates = (await ai.language.ask(`ID: ${issueData.id}\nTitle: ${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
182
+ system: `Your job is to identify duplicates. Respond ONLY with the duplicate's ID number or "NONE" if no match exists\n\n${dupes}`
183
+ }))?.pop()?.content;
184
+ // Handle duplicates
185
+ if(hasDuplicates && hasDuplicates !== 'NONE' && (dupeId = dupeIds.find(id => id == hasDuplicates.trim()))) {
186
+ await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
164
187
  method: 'POST',
165
- headers: {
166
- 'Authorization': `token ${auth}`,
167
- 'Content-Type': 'application/json'
168
- },
169
- body: JSON.stringify({labels: [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`]})
170
- });
171
- if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`);
188
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
189
+ body: `{"body": "Duplicate of #${dupeId}"}`
190
+ }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
191
+ await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
192
+ method: 'POST',
193
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
194
+ body: `{"labels":["${labelDupe}"]}`
195
+ }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
196
+ await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
197
+ method: 'PATCH',
198
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
199
+ body: '{"state": "closed"}'
200
+ }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
201
+ console.log('Duplicate');
202
+ return process.exit();
172
203
  }
173
204
 
174
- console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`);
175
- })();
205
+ // Update ticket
206
+ await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
207
+ method: 'PATCH',
208
+ headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
209
+ body: JSON.stringify({title, body})
210
+ }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
211
+
212
+ console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`);
213
+ })().catch(err => {
214
+ console.error(`Error: ${err.message || err.toString()}`);
215
+ process.exit(1);
216
+ });
package/src/review.mjs CHANGED
@@ -106,4 +106,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
106
106
  if(!res.ok) throw new Error(`${res.status} ${await res.text()}`);
107
107
  }
108
108
  console.log(comments.map(c => `${c.path}${c.new_position ? `:${c.new_position}` : ''}\n${c.body}`).join('\n\n') + '\n\n' + summary);
109
- })();
109
+ })().catch(err => {
110
+ console.error(`Error: ${err.message || err.toString()}`);
111
+ process.exit(1);
112
+ });