@rotorsoft/gent 1.7.1 → 1.9.0

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/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  getCurrentUser,
20
20
  getIssue,
21
21
  getPrForBranch,
22
+ getPrReviewData,
22
23
  getWorkflowLabels,
23
24
  isValidIssueNumber,
24
25
  listIssues,
@@ -30,7 +31,7 @@ import {
30
31
  sortByPriority,
31
32
  updateIssueLabels,
32
33
  withSpinner
33
- } from "./chunk-2LGYNV6S.js";
34
+ } from "./chunk-CMDTYS6L.js";
34
35
 
35
36
  // src/index.ts
36
37
  import { Command } from "commander";
@@ -154,8 +155,17 @@ async function initCommand(options) {
154
155
  return;
155
156
  }
156
157
  }
158
+ const { provider } = await inquirer.prompt([
159
+ {
160
+ type: "list",
161
+ name: "provider",
162
+ message: "Which AI provider would you like to use by default?",
163
+ choices: ["claude", "gemini", "codex"],
164
+ default: "claude"
165
+ }
166
+ ]);
157
167
  const configPath = getConfigPath(cwd);
158
- writeFileSync2(configPath, generateDefaultConfig(), "utf-8");
168
+ writeFileSync2(configPath, generateDefaultConfig(provider), "utf-8");
159
169
  logger.success(`Created ${colors.file(".gent.yml")}`);
160
170
  const agentPath = join2(cwd, "AGENT.md");
161
171
  if (!existsSync2(agentPath) || options.force) {
@@ -182,7 +192,7 @@ async function initCommand(options) {
182
192
  }
183
193
  ]);
184
194
  if (setupLabels) {
185
- const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3IMSEEKN.js");
195
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-IQOZF4U7.js");
186
196
  await setupLabelsCommand2();
187
197
  }
188
198
  }
@@ -194,17 +204,27 @@ import chalk from "chalk";
194
204
  // src/lib/ai-provider.ts
195
205
  import { spawn } from "child_process";
196
206
  import { execa } from "execa";
207
+ async function invokeInternal(provider, options) {
208
+ switch (provider) {
209
+ case "claude":
210
+ return invokeClaudeInternal(options);
211
+ case "gemini":
212
+ return invokeGeminiInternal(options);
213
+ case "codex":
214
+ return invokeCodexInternal(options);
215
+ }
216
+ }
197
217
  async function invokeAI(options, config, providerOverride) {
198
218
  const provider = providerOverride ?? config.ai.provider;
199
219
  try {
200
- const output = provider === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
220
+ const output = await invokeInternal(provider, options);
201
221
  return { output, provider };
202
222
  } catch (error) {
203
223
  if (isRateLimitError(error, provider)) {
204
224
  if (config.ai.auto_fallback && config.ai.fallback_provider && !providerOverride) {
205
225
  const fallback = config.ai.fallback_provider;
206
226
  logger.warning(`Rate limit reached on ${getProviderDisplayName(provider)}, switching to ${getProviderDisplayName(fallback)}...`);
207
- const output = fallback === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
227
+ const output = await invokeInternal(fallback, options);
208
228
  return { output, provider: fallback };
209
229
  }
210
230
  const err = error;
@@ -217,28 +237,52 @@ async function invokeAI(options, config, providerOverride) {
217
237
  }
218
238
  async function invokeAIInteractive(prompt, config, providerOverride) {
219
239
  const provider = providerOverride ?? config.ai.provider;
220
- if (provider === "claude") {
221
- const args = ["--permission-mode", config.claude.permission_mode, prompt];
222
- return {
223
- result: execa("claude", args, { stdio: "inherit" }),
224
- provider
225
- };
226
- } else {
227
- return {
228
- result: execa("gemini", ["-i", prompt], { stdio: "inherit" }),
229
- provider
230
- };
240
+ switch (provider) {
241
+ case "claude": {
242
+ const args = ["--permission-mode", config.claude.permission_mode, prompt];
243
+ return {
244
+ result: execa("claude", args, { stdio: "inherit" }),
245
+ provider
246
+ };
247
+ }
248
+ case "gemini": {
249
+ return {
250
+ result: execa("gemini", ["-i", prompt], { stdio: "inherit" }),
251
+ provider
252
+ };
253
+ }
254
+ case "codex": {
255
+ const args = prompt ? [prompt] : [];
256
+ return {
257
+ result: execa("codex", args, { stdio: "inherit" }),
258
+ provider
259
+ };
260
+ }
231
261
  }
232
262
  }
233
263
  function getProviderDisplayName(provider) {
234
- return provider === "claude" ? "Claude" : "Gemini";
264
+ switch (provider) {
265
+ case "claude":
266
+ return "Claude";
267
+ case "gemini":
268
+ return "Gemini";
269
+ case "codex":
270
+ return "Codex";
271
+ }
235
272
  }
236
273
  function getProviderEmail(provider) {
237
- return provider === "claude" ? "noreply@anthropic.com" : "noreply@google.com";
274
+ switch (provider) {
275
+ case "claude":
276
+ return "noreply@anthropic.com";
277
+ case "gemini":
278
+ return "noreply@google.com";
279
+ case "codex":
280
+ return "noreply@openai.com";
281
+ }
238
282
  }
239
283
  function isRateLimitError(error, provider) {
240
284
  if (!error || typeof error !== "object") return false;
241
- if (provider === "claude" && "exitCode" in error && error.exitCode === 2) {
285
+ if ((provider === "claude" || provider === "codex") && "exitCode" in error && error.exitCode === 2) {
242
286
  return true;
243
287
  }
244
288
  if ("message" in error && typeof error.message === "string") {
@@ -330,6 +374,44 @@ async function invokeGeminiInternal(options) {
330
374
  return stdout;
331
375
  }
332
376
  }
377
+ async function invokeCodexInternal(options) {
378
+ const args = ["exec", options.prompt];
379
+ if (options.printOutput) {
380
+ const subprocess = execa("codex", args, {
381
+ stdio: "inherit"
382
+ });
383
+ await subprocess;
384
+ return "";
385
+ } else if (options.streamOutput) {
386
+ return new Promise((resolve, reject) => {
387
+ const child = spawn("codex", args, {
388
+ stdio: ["inherit", "pipe", "pipe"]
389
+ });
390
+ let output = "";
391
+ child.stdout.on("data", (chunk) => {
392
+ const text = chunk.toString();
393
+ output += text;
394
+ process.stdout.write(text);
395
+ });
396
+ child.stderr.on("data", (chunk) => {
397
+ process.stderr.write(chunk);
398
+ });
399
+ child.on("close", (code) => {
400
+ if (code === 0) {
401
+ resolve(output);
402
+ } else {
403
+ const error = new Error(`Codex exited with code ${code}`);
404
+ error.exitCode = code ?? 1;
405
+ reject(error);
406
+ }
407
+ });
408
+ child.on("error", reject);
409
+ });
410
+ } else {
411
+ const { stdout } = await execa("codex", args);
412
+ return stdout;
413
+ }
414
+ }
333
415
 
334
416
  // src/lib/prompts.ts
335
417
  function buildTicketPrompt(description, agentInstructions, additionalHints = null) {
@@ -386,7 +468,7 @@ META:type=<type>,priority=<priority>,risk=<risk>,area=<area>
386
468
  Example: META:type=feature,priority=high,risk=low,area=ui`;
387
469
  return basePrompt;
388
470
  }
389
- function buildImplementationPrompt(issue, agentInstructions, progressContent, config) {
471
+ function buildImplementationPrompt(issue, agentInstructions, progressContent, config, reviewFeedback = null) {
390
472
  const providerName = getProviderDisplayName(config.ai.provider);
391
473
  const providerEmail = getProviderEmail(config.ai.provider);
392
474
  return `GitHub Issue #${issue.number}: ${issue.title}
@@ -400,6 +482,10 @@ ${agentInstructions}
400
482
  ${progressContent ? `## Previous Progress
401
483
  ${progressContent}
402
484
 
485
+ ` : ""}
486
+ ${reviewFeedback ? `## Review Feedback
487
+ ${reviewFeedback}
488
+
403
489
  ` : ""}
404
490
 
405
491
  ## Your Task
@@ -1353,6 +1439,247 @@ function generateFallbackBody(issue, commits) {
1353
1439
  return body;
1354
1440
  }
1355
1441
 
1442
+ // src/commands/fix.ts
1443
+ import inquirer5 from "inquirer";
1444
+
1445
+ // src/lib/review-feedback.ts
1446
+ var ACTIONABLE_KEYWORDS = [
1447
+ "todo",
1448
+ "fix",
1449
+ "should",
1450
+ "must",
1451
+ "needs",
1452
+ "please",
1453
+ "consider",
1454
+ "can you",
1455
+ "change",
1456
+ "update",
1457
+ "remove",
1458
+ "add"
1459
+ ];
1460
+ var TRIVIAL_COMMENTS = ["lgtm", "looks good", "approved"];
1461
+ function summarizeReviewFeedback(data) {
1462
+ const items = extractReviewFeedbackItems(data);
1463
+ return {
1464
+ items,
1465
+ summary: items.length > 0 ? formatReviewFeedbackSummary(items) : ""
1466
+ };
1467
+ }
1468
+ function extractReviewFeedbackItems(data) {
1469
+ const items = [];
1470
+ for (const review of data.reviews) {
1471
+ const body = review.body?.trim() ?? "";
1472
+ if (!body || isTrivialComment(body)) {
1473
+ continue;
1474
+ }
1475
+ const isChangesRequested = review.state === "CHANGES_REQUESTED";
1476
+ const actionable = isChangesRequested || isActionableText(body);
1477
+ if (!actionable) {
1478
+ continue;
1479
+ }
1480
+ items.push({
1481
+ source: "review",
1482
+ author: review.author,
1483
+ body,
1484
+ state: review.state
1485
+ });
1486
+ }
1487
+ for (const thread of data.reviewThreads) {
1488
+ if (!isActionableThread(thread)) {
1489
+ continue;
1490
+ }
1491
+ const comments = thread.comments ?? [];
1492
+ const latestComment = findLatestMeaningfulComment(comments);
1493
+ if (!latestComment) {
1494
+ continue;
1495
+ }
1496
+ items.push({
1497
+ source: "thread",
1498
+ author: latestComment.author,
1499
+ body: latestComment.body,
1500
+ path: thread.path ?? latestComment.path,
1501
+ line: thread.line ?? latestComment.line ?? null
1502
+ });
1503
+ }
1504
+ return items;
1505
+ }
1506
+ function formatReviewFeedbackSummary(items) {
1507
+ return items.map((item) => {
1508
+ const location = formatLocation(item);
1509
+ const stateLabel = item.state ? formatState(item.state) : null;
1510
+ const author = item.author ? `@${item.author}` : "Reviewer";
1511
+ const body = truncateComment(item.body);
1512
+ const header = item.source === "review" ? stateLabel ? `Review (${stateLabel})` : "Review" : location;
1513
+ return `- [${header}] ${author}: ${body}`;
1514
+ }).join("\n");
1515
+ }
1516
+ function isActionableThread(thread) {
1517
+ if (thread.isResolved === false || thread.isResolved === void 0 || thread.isResolved === null) {
1518
+ return true;
1519
+ }
1520
+ return (thread.comments ?? []).some((comment) => isActionableText(comment.body));
1521
+ }
1522
+ function isActionableText(text) {
1523
+ const normalized = text.toLowerCase();
1524
+ return ACTIONABLE_KEYWORDS.some((keyword) => normalized.includes(keyword));
1525
+ }
1526
+ function isTrivialComment(text) {
1527
+ const normalized = text.trim().toLowerCase();
1528
+ return TRIVIAL_COMMENTS.some((entry) => normalized === entry);
1529
+ }
1530
+ function findLatestMeaningfulComment(comments) {
1531
+ for (let i = comments.length - 1; i >= 0; i -= 1) {
1532
+ const body = comments[i].body?.trim() ?? "";
1533
+ if (body && !isTrivialComment(body)) {
1534
+ return comments[i];
1535
+ }
1536
+ }
1537
+ return null;
1538
+ }
1539
+ function formatLocation(item) {
1540
+ if (item.path && item.line) {
1541
+ return `${item.path}:${item.line}`;
1542
+ }
1543
+ if (item.path) {
1544
+ return item.path;
1545
+ }
1546
+ return "Thread";
1547
+ }
1548
+ function formatState(state) {
1549
+ return state.replace(/_/g, " ").toLowerCase();
1550
+ }
1551
+ function truncateComment(body, maxLength = 200) {
1552
+ const normalized = body.replace(/\s+/g, " ").trim();
1553
+ if (normalized.length <= maxLength) {
1554
+ return normalized;
1555
+ }
1556
+ return `${normalized.slice(0, maxLength - 3)}...`;
1557
+ }
1558
+
1559
+ // src/commands/fix.ts
1560
+ async function fixCommand(options) {
1561
+ logger.bold("Applying PR review feedback with AI...");
1562
+ logger.newline();
1563
+ const config = loadConfig();
1564
+ const provider = options.provider ?? config.ai.provider;
1565
+ const providerName = getProviderDisplayName(provider);
1566
+ const [ghAuth, aiOk] = await Promise.all([
1567
+ checkGhAuth(),
1568
+ checkAIProvider(provider)
1569
+ ]);
1570
+ if (!ghAuth) {
1571
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1572
+ process.exit(1);
1573
+ }
1574
+ if (!aiOk) {
1575
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
1576
+ process.exit(1);
1577
+ }
1578
+ if (await isOnMainBranch()) {
1579
+ logger.error("Cannot apply fixes from main/master branch. Switch to the PR branch first.");
1580
+ process.exit(1);
1581
+ }
1582
+ const hasChanges = await hasUncommittedChanges();
1583
+ if (hasChanges) {
1584
+ logger.warning("You have uncommitted changes.");
1585
+ const { proceed } = await inquirer5.prompt([
1586
+ {
1587
+ type: "confirm",
1588
+ name: "proceed",
1589
+ message: "Continue anyway?",
1590
+ default: false
1591
+ }
1592
+ ]);
1593
+ if (!proceed) {
1594
+ logger.info("Aborting. Please commit or stash your changes first.");
1595
+ process.exit(0);
1596
+ }
1597
+ }
1598
+ const pr = await withSpinner("Resolving pull request...", async () => {
1599
+ return getPrForBranch();
1600
+ });
1601
+ if (!pr) {
1602
+ logger.error("No pull request found for the current branch. Create one with 'gent pr' first.");
1603
+ process.exit(1);
1604
+ }
1605
+ const reviewData = await withSpinner("Fetching review feedback...", async () => {
1606
+ return getPrReviewData(pr.number);
1607
+ });
1608
+ const totalComments = countReviewComments(reviewData);
1609
+ if (totalComments === 0) {
1610
+ logger.error(`No review comments found for PR #${pr.number}.`);
1611
+ process.exit(1);
1612
+ }
1613
+ const { items, summary } = summarizeReviewFeedback(reviewData);
1614
+ if (items.length === 0 || !summary) {
1615
+ logger.error("No actionable review feedback found.");
1616
+ process.exit(1);
1617
+ }
1618
+ logger.newline();
1619
+ logger.box("Review Feedback Summary", summary);
1620
+ logger.newline();
1621
+ const currentBranch = await getCurrentBranch();
1622
+ const issueNumber = extractIssueNumber(currentBranch);
1623
+ if (!issueNumber) {
1624
+ logger.error("Could not determine issue number from branch name.");
1625
+ process.exit(1);
1626
+ }
1627
+ const issue = await withSpinner("Fetching linked issue...", async () => {
1628
+ return getIssue(issueNumber);
1629
+ });
1630
+ const agentInstructions = loadAgentInstructions();
1631
+ const progressContent = readProgress(config);
1632
+ const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config, summary);
1633
+ logger.newline();
1634
+ logger.info(`Starting ${colors.provider(providerName)} fix session...`);
1635
+ logger.dim("Review feedback will be appended to the implementation prompt.");
1636
+ logger.newline();
1637
+ const beforeSha = await getCurrentCommitSha();
1638
+ let wasCancelled = false;
1639
+ const handleSignal = () => {
1640
+ wasCancelled = true;
1641
+ };
1642
+ process.on("SIGINT", handleSignal);
1643
+ process.on("SIGTERM", handleSignal);
1644
+ let aiExitCode;
1645
+ try {
1646
+ const { result } = await invokeAIInteractive(prompt, config, options.provider);
1647
+ aiExitCode = result.exitCode ?? void 0;
1648
+ } catch (error) {
1649
+ if (error && typeof error === "object" && "exitCode" in error) {
1650
+ aiExitCode = error.exitCode;
1651
+ }
1652
+ logger.error(`${providerName} session failed: ${error}`);
1653
+ } finally {
1654
+ process.off("SIGINT", handleSignal);
1655
+ process.off("SIGTERM", handleSignal);
1656
+ }
1657
+ logger.newline();
1658
+ if (wasCancelled) {
1659
+ logger.warning("Operation was cancelled. No changes were recorded.");
1660
+ return;
1661
+ }
1662
+ const commitsCreated = await hasNewCommits(beforeSha);
1663
+ if (commitsCreated) {
1664
+ logger.success(`${providerName} session completed with new commits.`);
1665
+ return;
1666
+ }
1667
+ const isRateLimited = aiExitCode === 2;
1668
+ if (isRateLimited) {
1669
+ logger.warning(`${providerName} session ended due to rate limits. No commits were created.`);
1670
+ return;
1671
+ }
1672
+ logger.warning(`${providerName} session completed but no commits were created.`);
1673
+ }
1674
+ function countReviewComments(data) {
1675
+ const reviewBodies = data.reviews.filter((review) => review.body?.trim()).length;
1676
+ const threadBodies = data.reviewThreads.reduce((count, thread) => {
1677
+ const threadCount = (thread.comments ?? []).filter((comment) => comment.body?.trim()).length;
1678
+ return count + threadCount;
1679
+ }, 0);
1680
+ return reviewBodies + threadBodies;
1681
+ }
1682
+
1356
1683
  // src/lib/version.ts
1357
1684
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1358
1685
  import { join as join3 } from "path";
@@ -1361,8 +1688,8 @@ import { homedir } from "os";
1361
1688
  // package.json
1362
1689
  var package_default = {
1363
1690
  name: "@rotorsoft/gent",
1364
- version: "1.7.1",
1365
- description: "AI-powered GitHub workflow CLI - leverage AI (Claude or Gemini) to create tickets, implement features, and manage PRs",
1691
+ version: "1.9.0",
1692
+ description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
1366
1693
  keywords: [
1367
1694
  "cli",
1368
1695
  "ai",
@@ -1703,7 +2030,7 @@ function startVersionCheck() {
1703
2030
  });
1704
2031
  }
1705
2032
  var program = new Command();
1706
- program.name("gent").description("AI-powered GitHub workflow CLI - leverage AI (Claude or Gemini) to create tickets, implement features, and manage PRs").version(version).option("--skip-update-check", "Skip checking for CLI updates").hook("preAction", (thisCommand) => {
2033
+ program.name("gent").description("AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs").version(version).option("--skip-update-check", "Skip checking for CLI updates").hook("preAction", (thisCommand) => {
1707
2034
  if (!thisCommand.opts().skipUpdateCheck) {
1708
2035
  startVersionCheck();
1709
2036
  }
@@ -1714,7 +2041,7 @@ program.command("init").description("Initialize gent workflow in current reposit
1714
2041
  program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
1715
2042
  await setupLabelsCommand();
1716
2043
  });
1717
- program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").option("-t, --title <title>", "Override the generated issue title").action(async (description, options) => {
2044
+ program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").option("-t, --title <title>", "Override the generated issue title").action(async (description, options) => {
1718
2045
  await createCommand(description, { yes: options.yes, provider: options.provider, title: options.title });
1719
2046
  });
1720
2047
  program.command("list").description("List GitHub issues by label/status").option("-l, --label <label>", "Filter by label").option("-s, --status <status>", "Filter by workflow status (ready, in-progress, completed, blocked, all)").option("-n, --limit <number>", "Maximum number of issues to show", "20").action(async (options) => {
@@ -1724,12 +2051,15 @@ program.command("list").description("List GitHub issues by label/status").option
1724
2051
  limit: parseInt(options.limit, 10)
1725
2052
  });
1726
2053
  });
1727
- program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (issueNumber, options) => {
2054
+ program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (issueNumber, options) => {
1728
2055
  await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
1729
2056
  });
1730
- program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (options) => {
2057
+ program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (options) => {
1731
2058
  await prCommand({ draft: options.draft, provider: options.provider });
1732
2059
  });
2060
+ program.command("fix").description("Apply PR review feedback using AI").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (options) => {
2061
+ await fixCommand({ provider: options.provider });
2062
+ });
1733
2063
  program.command("status").description("Show current workflow status").action(async () => {
1734
2064
  await statusCommand();
1735
2065
  });