@ariso-ai/ivan 1.0.15 → 1.0.17
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/dist/services/address-task-executor.d.ts +4 -0
- package/dist/services/address-task-executor.d.ts.map +1 -1
- package/dist/services/address-task-executor.js +253 -198
- package/dist/services/address-task-executor.js.map +1 -1
- package/dist/services/git-manager-cli.d.ts.map +1 -1
- package/dist/services/git-manager-cli.js +44 -14
- package/dist/services/git-manager-cli.js.map +1 -1
- package/dist/services/git-manager-pat.d.ts.map +1 -1
- package/dist/services/git-manager-pat.js +48 -16
- package/dist/services/git-manager-pat.js.map +1 -1
- package/dist/services/github-api-client.d.ts +4 -0
- package/dist/services/github-api-client.d.ts.map +1 -1
- package/dist/services/github-api-client.js +52 -0
- package/dist/services/github-api-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,11 +8,15 @@ export declare class AddressTaskExecutor {
|
|
|
8
8
|
private prService;
|
|
9
9
|
private workingDir;
|
|
10
10
|
private repoInstructions;
|
|
11
|
+
private githubClient;
|
|
12
|
+
private repoOwner;
|
|
13
|
+
private repoName;
|
|
11
14
|
constructor();
|
|
12
15
|
private getClaudeExecutor;
|
|
13
16
|
private getOpenAIService;
|
|
14
17
|
executeAddressTasks(tasks: Task[], quiet?: boolean): Promise<void>;
|
|
15
18
|
private getUnaddressedComments;
|
|
19
|
+
private findCommentId;
|
|
16
20
|
private generateReviewInstructions;
|
|
17
21
|
private tryCommitWithFixes;
|
|
18
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address-task-executor.d.ts","sourceRoot":"","sources":["../../src/services/address-task-executor.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"address-task-executor.d.ts","sourceRoot":"","sources":["../../src/services/address-task-executor.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAKtC,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAc;;IAQ9B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IAOlB,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YA0tBjE,sBAAsB;YAoBtB,aAAa;YAyEb,0BAA0B;YAyD1B,kBAAkB;CAuEjC"}
|
|
@@ -6,6 +6,7 @@ import { ExecutorFactory } from './executor-factory.js';
|
|
|
6
6
|
import { OpenAIService } from './openai-service.js';
|
|
7
7
|
import { ConfigManager } from '../config.js';
|
|
8
8
|
import { createGitManager, createPRService } from './service-factory.js';
|
|
9
|
+
import { GitHubAPIClient } from './github-api-client.js';
|
|
9
10
|
export class AddressTaskExecutor {
|
|
10
11
|
jobManager;
|
|
11
12
|
gitManager = null;
|
|
@@ -15,6 +16,9 @@ export class AddressTaskExecutor {
|
|
|
15
16
|
prService = null;
|
|
16
17
|
workingDir;
|
|
17
18
|
repoInstructions;
|
|
19
|
+
githubClient = null;
|
|
20
|
+
repoOwner = '';
|
|
21
|
+
repoName = '';
|
|
18
22
|
constructor() {
|
|
19
23
|
this.jobManager = new JobManager();
|
|
20
24
|
this.configManager = new ConfigManager();
|
|
@@ -61,6 +65,33 @@ export class AddressTaskExecutor {
|
|
|
61
65
|
this.gitManager.validateGitHubCliAuthentication();
|
|
62
66
|
if (!quiet)
|
|
63
67
|
console.log(chalk.green('✅ GitHub CLI is authenticated'));
|
|
68
|
+
// Initialize GitHub API client if using PAT
|
|
69
|
+
const authType = this.configManager.getGithubAuthType();
|
|
70
|
+
if (authType === 'pat') {
|
|
71
|
+
const pat = this.configManager.getGithubPat();
|
|
72
|
+
if (pat) {
|
|
73
|
+
this.githubClient = new GitHubAPIClient(pat);
|
|
74
|
+
const repoInfo = GitHubAPIClient.getRepoInfoFromRemote(this.workingDir);
|
|
75
|
+
this.repoOwner = repoInfo.owner;
|
|
76
|
+
this.repoName = repoInfo.repo;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// If not using PAT, get repo info from gh command
|
|
80
|
+
if (!this.githubClient) {
|
|
81
|
+
try {
|
|
82
|
+
const repoInfo = execSync('gh repo view --json owner,name', {
|
|
83
|
+
cwd: this.workingDir,
|
|
84
|
+
encoding: 'utf-8'
|
|
85
|
+
});
|
|
86
|
+
const parsed = JSON.parse(repoInfo);
|
|
87
|
+
this.repoOwner = parsed.owner.login;
|
|
88
|
+
this.repoName = parsed.name;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
if (!quiet)
|
|
92
|
+
console.log(chalk.yellow('⚠️ Could not get repository info'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
64
95
|
// Load repository instructions
|
|
65
96
|
this.repoInstructions = await this.configManager.getRepoInstructions(this.workingDir);
|
|
66
97
|
// Group tasks by branch
|
|
@@ -221,10 +252,16 @@ export class AddressTaskExecutor {
|
|
|
221
252
|
try {
|
|
222
253
|
const reviewAgent = this.configManager.getReviewAgent();
|
|
223
254
|
const reviewComment = `${reviewAgent} please review the test and lint fixes that were applied to address the failing CI checks`;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
255
|
+
// Use GitHub API client if available, otherwise fall back to gh command
|
|
256
|
+
if (this.githubClient && this.repoOwner && this.repoName) {
|
|
257
|
+
await this.githubClient.addPRComment(this.repoOwner, this.repoName, taskPrNumber, reviewComment);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
execSync(`gh pr comment ${taskPrNumber} --body "${reviewComment}"`, {
|
|
261
|
+
cwd: worktreePath || this.workingDir,
|
|
262
|
+
stdio: 'pipe'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
228
265
|
if (spinner)
|
|
229
266
|
spinner.succeed('Review request comment added');
|
|
230
267
|
}
|
|
@@ -258,47 +295,49 @@ export class AddressTaskExecutor {
|
|
|
258
295
|
if (addressTasks.length === 0) {
|
|
259
296
|
continue;
|
|
260
297
|
}
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
// Process each comment
|
|
276
|
-
for (const comment of comments) {
|
|
277
|
-
// In quiet mode, output the comment being addressed
|
|
278
|
-
console.log('');
|
|
279
|
-
console.log(chalk.blue(`📝 Addressing comment from @${comment.author}:`));
|
|
280
|
-
console.log(chalk.gray(` "${comment.body.substring(0, 100)}${comment.body.length > 100 ? '...' : ''}"`));
|
|
281
|
-
// Find the corresponding task
|
|
282
|
-
const task = addressTasks.find(t => t.description.includes(comment.author) &&
|
|
283
|
-
t.description.includes(comment.body.substring(0, 50)));
|
|
284
|
-
if (!task) {
|
|
298
|
+
// Process each address task directly (no need to re-fetch comments from GitHub)
|
|
299
|
+
for (const task of addressTasks) {
|
|
300
|
+
// Skip tasks that aren't comment-related
|
|
301
|
+
if (!task.description.includes('comment from @')) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// Parse comment information from task description
|
|
305
|
+
// Format: "Address PR #123 comment from @author: "body" (in path:line)"
|
|
306
|
+
const authorMatch = task.description.match(/comment from @(\w+)/);
|
|
307
|
+
const bodyMatch = task.description.match(/: "(.+?)"/);
|
|
308
|
+
const pathMatch = task.description.match(/\(in (.+?)(:\d+)?\)$/);
|
|
309
|
+
const lineMatch = task.description.match(/:(\d+)\)$/);
|
|
310
|
+
if (!authorMatch || !bodyMatch) {
|
|
285
311
|
if (!quiet)
|
|
286
|
-
console.log(chalk.yellow(
|
|
312
|
+
console.log(chalk.yellow(`⚠️ Could not parse comment from task: ${task.description}`));
|
|
287
313
|
continue;
|
|
288
314
|
}
|
|
315
|
+
const comment = {
|
|
316
|
+
author: authorMatch[1],
|
|
317
|
+
body: bodyMatch[1],
|
|
318
|
+
path: pathMatch ? pathMatch[1] : undefined,
|
|
319
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
320
|
+
id: '' // We'll fetch this if needed for replying
|
|
321
|
+
};
|
|
322
|
+
// Output what we're addressing
|
|
323
|
+
console.log('');
|
|
324
|
+
console.log(chalk.blue(`📝 Addressing comment from @${comment.author}:`));
|
|
325
|
+
console.log(chalk.gray(` "${comment.body}"`));
|
|
326
|
+
if (comment.path) {
|
|
327
|
+
console.log(chalk.gray(` File: ${comment.path}${comment.line ? `:${comment.line}` : ''}`));
|
|
328
|
+
}
|
|
289
329
|
await this.jobManager.updateTaskStatus(task.uuid, 'active');
|
|
290
|
-
//
|
|
291
|
-
if (
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
330
|
+
// Fetch the comment ID from GitHub for replying later
|
|
331
|
+
if (prNumber) {
|
|
332
|
+
const commentId = await this.findCommentId(parseInt(prNumber), comment.author, comment.body.substring(0, 50), comment.path, comment.line);
|
|
333
|
+
if (commentId) {
|
|
334
|
+
comment.id = commentId;
|
|
335
|
+
// Save comment URL
|
|
336
|
+
if (this.repoOwner && this.repoName) {
|
|
337
|
+
const commentUrl = `https://github.com/${this.repoOwner}/${this.repoName}/pull/${prNumber}#discussion_r${commentId}`;
|
|
338
|
+
await this.jobManager.updateTaskCommentUrl(task.uuid, commentUrl);
|
|
339
|
+
}
|
|
299
340
|
}
|
|
300
|
-
const commentUrl = `https://github.com/${owner.login}/${repoName}/pull/${prNumber}#discussion_r${comment.id}`;
|
|
301
|
-
await this.jobManager.updateTaskCommentUrl(task.uuid, commentUrl);
|
|
302
341
|
}
|
|
303
342
|
if (!quiet)
|
|
304
343
|
spinner = ora('Running Claude Code to address comment...').start();
|
|
@@ -343,78 +382,82 @@ export class AddressTaskExecutor {
|
|
|
343
382
|
if (replyBody.length > maxLength) {
|
|
344
383
|
replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)';
|
|
345
384
|
}
|
|
346
|
-
// Use
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
385
|
+
// Use GitHub API client if available, otherwise fall back to gh command
|
|
386
|
+
if (this.githubClient && this.repoOwner && this.repoName && prNumber) {
|
|
387
|
+
await this.githubClient.addReviewThreadReply(this.repoOwner, this.repoName, parseInt(prNumber), comment.id, replyBody);
|
|
388
|
+
if (spinner)
|
|
389
|
+
spinner.succeed('Reply added to comment');
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Fallback to gh command
|
|
393
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
394
|
+
const { join } = await import('path');
|
|
395
|
+
const { tmpdir } = await import('os');
|
|
396
|
+
const tempFile = join(tmpdir(), `ivan-comment-${Date.now()}.txt`);
|
|
397
|
+
writeFileSync(tempFile, replyBody);
|
|
398
|
+
try {
|
|
399
|
+
const repoInfo = execSync('gh repo view --json owner,name', {
|
|
400
|
+
cwd: worktreePath || this.workingDir,
|
|
401
|
+
encoding: 'utf-8'
|
|
402
|
+
});
|
|
403
|
+
const { owner, name: repoName } = JSON.parse(repoInfo);
|
|
404
|
+
const threadQuery = `
|
|
405
|
+
query {
|
|
406
|
+
repository(owner: "${owner.login}", name: "${repoName}") {
|
|
407
|
+
pullRequest(number: ${prNumber}) {
|
|
408
|
+
reviewThreads(first: 100) {
|
|
409
|
+
nodes {
|
|
410
|
+
id
|
|
411
|
+
comments(first: 100) {
|
|
412
|
+
nodes {
|
|
413
|
+
databaseId
|
|
414
|
+
}
|
|
370
415
|
}
|
|
371
416
|
}
|
|
372
417
|
}
|
|
373
418
|
}
|
|
374
419
|
}
|
|
375
420
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
break;
|
|
421
|
+
`;
|
|
422
|
+
const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
|
|
423
|
+
cwd: worktreePath || this.workingDir,
|
|
424
|
+
encoding: 'utf-8'
|
|
425
|
+
});
|
|
426
|
+
const threadData = JSON.parse(threadResult);
|
|
427
|
+
const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
|
|
428
|
+
let threadId = null;
|
|
429
|
+
for (const thread of threads) {
|
|
430
|
+
const comments = thread.comments?.nodes || [];
|
|
431
|
+
if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
|
|
432
|
+
threadId = thread.id;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
391
435
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
id
|
|
436
|
+
if (!threadId) {
|
|
437
|
+
throw new Error('Could not find review thread for comment');
|
|
438
|
+
}
|
|
439
|
+
const mutation = `
|
|
440
|
+
mutation {
|
|
441
|
+
addPullRequestReviewThreadReply(input: {
|
|
442
|
+
pullRequestReviewThreadId: "${threadId}"
|
|
443
|
+
body: ${JSON.stringify(replyBody)}
|
|
444
|
+
}) {
|
|
445
|
+
comment {
|
|
446
|
+
id
|
|
447
|
+
}
|
|
405
448
|
}
|
|
406
449
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
450
|
+
`;
|
|
451
|
+
execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
|
|
452
|
+
cwd: worktreePath || this.workingDir,
|
|
453
|
+
stdio: 'pipe'
|
|
454
|
+
});
|
|
455
|
+
if (spinner)
|
|
456
|
+
spinner.succeed('Reply added to comment');
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
unlinkSync(tempFile);
|
|
460
|
+
}
|
|
418
461
|
}
|
|
419
462
|
}
|
|
420
463
|
catch (error) {
|
|
@@ -485,68 +528,73 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
|
|
|
485
528
|
replyBody = replyBody.substring(0, maxLength) + '\n\n... (message truncated)\n\n' +
|
|
486
529
|
`This has been addressed in commit ${commitHash.substring(0, 7)}`;
|
|
487
530
|
}
|
|
488
|
-
// Use
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
531
|
+
// Use GitHub API client if available, otherwise fall back to gh command
|
|
532
|
+
if (this.githubClient && this.repoOwner && this.repoName && prNumber) {
|
|
533
|
+
await this.githubClient.addReviewThreadReply(this.repoOwner, this.repoName, parseInt(prNumber), comment.id, replyBody);
|
|
534
|
+
if (spinner)
|
|
535
|
+
spinner.succeed('Reply added to comment');
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
// Fallback to gh command
|
|
539
|
+
const repoInfo = execSync('gh repo view --json owner,name', {
|
|
540
|
+
cwd: worktreePath || this.workingDir,
|
|
541
|
+
encoding: 'utf-8'
|
|
542
|
+
});
|
|
543
|
+
const { owner, name: repoName } = JSON.parse(repoInfo);
|
|
544
|
+
const threadQuery = `
|
|
545
|
+
query {
|
|
546
|
+
repository(owner: "${owner.login}", name: "${repoName}") {
|
|
547
|
+
pullRequest(number: ${prNumber}) {
|
|
548
|
+
reviewThreads(first: 100) {
|
|
549
|
+
nodes {
|
|
550
|
+
id
|
|
551
|
+
comments(first: 100) {
|
|
552
|
+
nodes {
|
|
553
|
+
databaseId
|
|
554
|
+
}
|
|
505
555
|
}
|
|
506
556
|
}
|
|
507
557
|
}
|
|
508
558
|
}
|
|
509
559
|
}
|
|
510
560
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
break;
|
|
561
|
+
`;
|
|
562
|
+
const threadResult = execSync(`gh api graphql -f query='${threadQuery.replace(/'/g, "'\\''")}'`, {
|
|
563
|
+
cwd: worktreePath || this.workingDir,
|
|
564
|
+
encoding: 'utf-8'
|
|
565
|
+
});
|
|
566
|
+
const threadData = JSON.parse(threadResult);
|
|
567
|
+
const threads = threadData.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
|
|
568
|
+
let threadId = null;
|
|
569
|
+
for (const thread of threads) {
|
|
570
|
+
const comments = thread.comments?.nodes || [];
|
|
571
|
+
if (comments.some((c) => c.databaseId?.toString() === comment.id)) {
|
|
572
|
+
threadId = thread.id;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
526
575
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
id
|
|
576
|
+
if (!threadId) {
|
|
577
|
+
throw new Error('Could not find review thread for comment');
|
|
578
|
+
}
|
|
579
|
+
const mutation = `
|
|
580
|
+
mutation {
|
|
581
|
+
addPullRequestReviewThreadReply(input: {
|
|
582
|
+
pullRequestReviewThreadId: "${threadId}"
|
|
583
|
+
body: ${JSON.stringify(replyBody)}
|
|
584
|
+
}) {
|
|
585
|
+
comment {
|
|
586
|
+
id
|
|
587
|
+
}
|
|
540
588
|
}
|
|
541
589
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
590
|
+
`;
|
|
591
|
+
execSync(`gh api graphql -f query='${mutation.replace(/'/g, "'\\''")}'`, {
|
|
592
|
+
cwd: worktreePath || this.workingDir,
|
|
593
|
+
stdio: 'pipe'
|
|
594
|
+
});
|
|
595
|
+
if (spinner)
|
|
596
|
+
spinner.succeed('Reply added to comment');
|
|
597
|
+
}
|
|
550
598
|
}
|
|
551
599
|
catch (error) {
|
|
552
600
|
if (spinner)
|
|
@@ -593,10 +641,16 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
|
|
|
593
641
|
spinner = ora('Adding review request comment...').start();
|
|
594
642
|
const reviewAgent = this.configManager.getReviewAgent();
|
|
595
643
|
const reviewComment = `${reviewAgent} ${reviewInstructions}`;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
644
|
+
// Use GitHub API client if available, otherwise fall back to gh command
|
|
645
|
+
if (this.githubClient && this.repoOwner && this.repoName) {
|
|
646
|
+
await this.githubClient.addPRComment(this.repoOwner, this.repoName, parseInt(prNumber), reviewComment);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
execSync(`gh pr comment ${prNumber} --body "${reviewComment.replace(/"/g, '\\"')}"`, {
|
|
650
|
+
cwd: worktreePath || this.workingDir,
|
|
651
|
+
stdio: 'pipe'
|
|
652
|
+
});
|
|
653
|
+
}
|
|
600
654
|
if (spinner)
|
|
601
655
|
spinner.succeed('Review request comment added');
|
|
602
656
|
}
|
|
@@ -631,30 +685,46 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
|
|
|
631
685
|
}
|
|
632
686
|
}
|
|
633
687
|
async getUnaddressedComments(prNumber) {
|
|
688
|
+
if (!this.prService) {
|
|
689
|
+
throw new Error('PR service not initialized');
|
|
690
|
+
}
|
|
634
691
|
try {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
692
|
+
return await this.prService.getUnaddressedComments(prNumber);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
console.error('Error fetching comments:', error);
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async findCommentId(prNumber, author, bodySnippet, path, line) {
|
|
700
|
+
// Fetch ALL comments (including resolved ones) to find the matching comment
|
|
701
|
+
try {
|
|
702
|
+
// Get repo info if not already set
|
|
703
|
+
let owner = this.repoOwner;
|
|
704
|
+
let repoName = this.repoName;
|
|
705
|
+
if (!owner || !repoName) {
|
|
706
|
+
const repoInfo = execSync('gh repo view --json owner,name', {
|
|
707
|
+
cwd: this.workingDir,
|
|
708
|
+
encoding: 'utf-8'
|
|
709
|
+
});
|
|
710
|
+
const parsed = JSON.parse(repoInfo);
|
|
711
|
+
owner = parsed.owner.login;
|
|
712
|
+
repoName = parsed.name;
|
|
713
|
+
}
|
|
714
|
+
// Use GraphQL to get ALL review threads, not just unresolved ones
|
|
642
715
|
const graphqlQuery = `
|
|
643
716
|
query {
|
|
644
|
-
repository(owner: "${owner
|
|
717
|
+
repository(owner: "${owner}", name: "${repoName}") {
|
|
645
718
|
pullRequest(number: ${prNumber}) {
|
|
646
719
|
reviewThreads(first: 100) {
|
|
647
720
|
nodes {
|
|
648
|
-
isResolved
|
|
649
721
|
comments(first: 100) {
|
|
650
722
|
nodes {
|
|
651
|
-
id
|
|
652
723
|
databaseId
|
|
653
724
|
body
|
|
654
725
|
author {
|
|
655
726
|
login
|
|
656
727
|
}
|
|
657
|
-
createdAt
|
|
658
728
|
path
|
|
659
729
|
line
|
|
660
730
|
}
|
|
@@ -665,44 +735,29 @@ Co-authored-by: ivan-agent <ivan-agent@users.noreply.github.com}`;
|
|
|
665
735
|
}
|
|
666
736
|
}
|
|
667
737
|
`;
|
|
668
|
-
const graphqlResult = execSync(`gh api graphql -f query='${graphqlQuery}'`, {
|
|
738
|
+
const graphqlResult = execSync(`gh api graphql -f query='${graphqlQuery.replace(/'/g, "'\\''")}'`, {
|
|
669
739
|
cwd: this.workingDir,
|
|
670
740
|
encoding: 'utf-8'
|
|
671
741
|
});
|
|
672
742
|
const result = JSON.parse(graphqlResult);
|
|
673
743
|
const threads = result.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
|
|
674
|
-
|
|
675
|
-
// Process each thread
|
|
744
|
+
// Search through all comments in all threads
|
|
676
745
|
for (const thread of threads) {
|
|
677
|
-
// Skip resolved threads
|
|
678
|
-
if (thread.isResolved) {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
746
|
const comments = thread.comments?.nodes || [];
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (!hasReplies && firstComment.path) {
|
|
690
|
-
// Only include if it's an inline code comment (has a path) and has no replies
|
|
691
|
-
unaddressedComments.push({
|
|
692
|
-
id: firstComment.databaseId ? firstComment.databaseId.toString() : firstComment.id,
|
|
693
|
-
author: firstComment.author.login,
|
|
694
|
-
body: firstComment.body,
|
|
695
|
-
createdAt: firstComment.createdAt,
|
|
696
|
-
path: firstComment.path,
|
|
697
|
-
line: firstComment.line
|
|
698
|
-
});
|
|
747
|
+
for (const comment of comments) {
|
|
748
|
+
if (comment.author?.login === author &&
|
|
749
|
+
comment.body.includes(bodySnippet) &&
|
|
750
|
+
(!path || comment.path === path) &&
|
|
751
|
+
(!line || comment.line === line)) {
|
|
752
|
+
return comment.databaseId ? comment.databaseId.toString() : null;
|
|
753
|
+
}
|
|
699
754
|
}
|
|
700
755
|
}
|
|
701
|
-
return
|
|
756
|
+
return null;
|
|
702
757
|
}
|
|
703
758
|
catch (error) {
|
|
704
|
-
console.error('Error
|
|
705
|
-
return
|
|
759
|
+
console.error('Error finding comment ID:', error);
|
|
760
|
+
return null;
|
|
706
761
|
}
|
|
707
762
|
}
|
|
708
763
|
async generateReviewInstructions(diff, changedFiles, _prNumber) {
|