@ztimson/ai-agents 0.1.0 → 0.1.2

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.1.0",
3
+ "version": "0.1.2",
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,6 +21,10 @@ 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}`);
@@ -32,13 +36,13 @@ dotenv.config({path: '.env.local', override: true, quiet: true});
32
36
  if(resp.ok) return resp.json();
33
37
  else throw new Error(`${resp.status} ${await resp.text()}`);
34
38
  });
35
- if(issueData.labels?.[0] !== 1 || issueData.labels?.[0]?.name !== 'Review/AI') {
39
+ if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
36
40
  console.log('Skipping');
37
41
  return process.exit();
38
42
  }
39
43
 
40
44
  // Gather readme & template
41
- let title = '', type = '', readme = '', readmeP = path.join(process.cwd(), 'README.md');
45
+ let title = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md');
42
46
  if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
43
47
  const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
44
48
 
@@ -96,18 +100,27 @@ Implementation details, constraints, dependencies, design decisions
96
100
  args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
97
101
  fn: (args) => title = args.title
98
102
  }, {
99
- name: 'type',
100
- description: 'Set the ticket type, must be called EXACTLY ONCE',
101
- args: {type: {type: 'string', description: 'Ticket type', enum: ['Bug', 'DevOps', 'Document', 'Enhancement', 'Refactor', 'Security'], required: true}},
102
- 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
+ }
103
114
  }],
104
115
  system: `Transform raw tickets into structured markdown following the template EXACTLY.
105
116
 
106
117
  **MANDATORY STEPS:**
107
- 1. Identify ticket type: Bug, DevOps, Document, Enhancement, Refactor, or Security
108
- 2. Call \`type\` tool EXACTLY ONCE with the type from step 1
109
- 3. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
110
- 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:
111
124
 
112
125
  **TEMPLATE RULES:**
113
126
  - Use ## headers (match template exactly)
@@ -128,11 +141,11 @@ Implementation details, constraints, dependencies, design decisions
128
141
  | **Total** | **0-15** |
129
142
 
130
143
  **SCORING:**
131
- - Size: # of modules/layers/files changed
144
+ - Size: # of modules/layers/files affected
132
145
  - Complexity: Technical difficulty
133
146
  - Unknowns: Research/uncertainty needed
134
147
 
135
- **README:**
148
+ **PROJECT README:**
136
149
  \`\`\`markdown
137
150
  ${readme.trim() || 'No README available'}
138
151
  \`\`\`
@@ -165,11 +178,12 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
165
178
  let dupeId = null;
166
179
  const dupeIds = search.map(t => t.id);
167
180
  const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
168
- const hasDuplicates = (await ai.language.ask(`${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
169
- 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}`
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}`
170
183
  }))?.pop()?.content;
184
+
171
185
  // Handle duplicates
172
- if(!!hasDuplicates && (dupeId = dupeIds.find(id => new RegExp(`\\b${id}\\b`, 'm').test(hasDuplicates)))) {
186
+ if(hasDuplicates && !hasDuplicates.toUpperCase().includes('NONE') && (dupeId = dupeIds.find(id => id == hasDuplicates.trim())) != null && dupeId != issueData.id) {
173
187
  await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
174
188
  method: 'POST',
175
189
  headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
@@ -178,7 +192,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
178
192
  await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
179
193
  method: 'POST',
180
194
  headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
181
- body: '{"labels":["Reviewed/Duplicate"]}'
195
+ body: `{"labels":["${labelDupe}"]}`
182
196
  }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
183
197
  await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
184
198
  method: 'PATCH',
@@ -195,15 +209,8 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
195
209
  headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
196
210
  body: JSON.stringify({title, body})
197
211
  }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
198
- if(type) { // Label
199
- await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
200
- method: 'POST',
201
- headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
202
- body: `{"labels":["Reviewed/${type[0].toUpperCase() + type.slice(1).toLowerCase()}"]}`
203
- }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
204
- }
205
212
 
206
- console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`);
213
+ console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`);
207
214
  })().catch(err => {
208
215
  console.error(`Error: ${err.message || err.toString()}`);
209
216
  process.exit(1);
package/src/review.mjs CHANGED
@@ -19,12 +19,23 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
19
19
  owner = process.env['GIT_OWNER'],
20
20
  repo = process.env['GIT_REPO'],
21
21
  auth = process.env['GIT_TOKEN'],
22
+ labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
22
23
  pr = process.env['PULL_REQUEST'],
23
24
  host = process.env['AI_HOST'],
24
25
  model = process.env['AI_MODEL'],
25
26
  token = process.env['AI_TOKEN'];
26
27
 
27
28
  console.log(`Reviewing: ${root}\n`);
29
+ const info = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}`)
30
+ .then(async resp => {
31
+ if(resp.ok) return resp.json();
32
+ throw new Error(`${resp.status} ${await resp.text()}`);
33
+ });
34
+ if(!info.labels.some(l => l.name === labelEnabled)) {
35
+ console.log('Skipping');
36
+ return process.exit();
37
+ }
38
+
28
39
  const branch = process.env['GIT_BRANCH'] || await $`cd ${root} && git symbolic-ref refs/remotes/origin/HEAD`;
29
40
  const comments = [];
30
41
  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});
53
64
  ...options,
54
65
  model: [host, model],
55
66
  path: process.env['path'] || os.tmpdir(),
56
- 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}`,
67
+ 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}`,
57
68
  tools: [{
58
69
  name: 'read_file',
59
70
  description: 'Read contents of a file',
@@ -87,7 +98,16 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
87
98
  }]
88
99
  });
89
100
 
90
- const messages = await ai.language.ask(gitDiff);
101
+ const messages = await ai.language.ask(`Title: ${info.title || 'None'}
102
+ Description:
103
+ \`\`\`md
104
+ ${info.body || 'None'}
105
+ \`\`\`
106
+
107
+ Git Diff:
108
+ \`\`\`
109
+ ${gitDiff}
110
+ \`\`\``);
91
111
  const summary = messages.pop().content;
92
112
  if(git) {
93
113
  const res = await fetch(`${git}/api/v1/repos/${owner}/${repo}/pulls/${pr}/reviews`, {