@rotorsoft/gent 1.1.2 → 1.3.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
@@ -3,7 +3,9 @@ import {
3
3
  addIssueComment,
4
4
  assignIssue,
5
5
  buildIssueLabels,
6
+ checkAIProvider,
6
7
  checkClaudeCli,
8
+ checkGeminiCli,
7
9
  checkGhAuth,
8
10
  checkGitRepo,
9
11
  colors,
@@ -28,7 +30,7 @@ import {
28
30
  sortByPriority,
29
31
  updateIssueLabels,
30
32
  withSpinner
31
- } from "./chunk-32TIYLFY.js";
33
+ } from "./chunk-2LGYNV6S.js";
32
34
 
33
35
  // src/index.ts
34
36
  import { Command } from "commander";
@@ -183,7 +185,7 @@ async function initCommand(options) {
183
185
  }
184
186
  ]);
185
187
  if (setupLabels) {
186
- const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3VZA2QD6.js");
188
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3IMSEEKN.js");
187
189
  await setupLabelsCommand2();
188
190
  }
189
191
  }
@@ -193,54 +195,7 @@ import inquirer2 from "inquirer";
193
195
  import chalk from "chalk";
194
196
 
195
197
  // src/lib/claude.ts
196
- import { spawn } from "child_process";
197
198
  import { execa } from "execa";
198
- async function invokeClaude(options) {
199
- const args = ["--print"];
200
- if (options.permissionMode) {
201
- args.push("--permission-mode", options.permissionMode);
202
- }
203
- args.push(options.prompt);
204
- if (options.printOutput) {
205
- const subprocess = execa("claude", args, {
206
- stdio: "inherit"
207
- });
208
- await subprocess;
209
- return "";
210
- } else if (options.streamOutput) {
211
- return new Promise((resolve, reject) => {
212
- const child = spawn("claude", args, {
213
- stdio: ["inherit", "pipe", "pipe"]
214
- });
215
- let output = "";
216
- child.stdout.on("data", (chunk) => {
217
- const text = chunk.toString();
218
- output += text;
219
- process.stdout.write(text);
220
- });
221
- child.stderr.on("data", (chunk) => {
222
- process.stderr.write(chunk);
223
- });
224
- child.on("close", (code) => {
225
- if (code === 0) {
226
- resolve(output);
227
- } else {
228
- reject(new Error(`Claude exited with code ${code}`));
229
- }
230
- });
231
- child.on("error", reject);
232
- });
233
- } else {
234
- const { stdout } = await execa("claude", args);
235
- return stdout;
236
- }
237
- }
238
- async function invokeClaudeInteractive(prompt, config) {
239
- const args = ["--permission-mode", config.claude.permission_mode, prompt];
240
- return execa("claude", args, {
241
- stdio: "inherit"
242
- });
243
- }
244
199
  function buildTicketPrompt(description, agentInstructions, additionalHints = null) {
245
200
  const basePrompt = `You are creating a GitHub issue for a software project following an AI-assisted development workflow.
246
201
 
@@ -381,36 +336,180 @@ function extractIssueBody(output) {
381
336
  return body;
382
337
  }
383
338
 
339
+ // src/lib/ai-provider.ts
340
+ import { spawn } from "child_process";
341
+ import { execa as execa2 } from "execa";
342
+ async function invokeAI(options, config, providerOverride) {
343
+ const provider = providerOverride ?? config.ai.provider;
344
+ try {
345
+ const output = provider === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
346
+ return { output, provider };
347
+ } catch (error) {
348
+ if (isRateLimitError(error, provider)) {
349
+ if (config.ai.auto_fallback && config.ai.fallback_provider && !providerOverride) {
350
+ const fallback = config.ai.fallback_provider;
351
+ logger.warning(`Rate limit reached on ${getProviderDisplayName(provider)}, switching to ${getProviderDisplayName(fallback)}...`);
352
+ const output = fallback === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
353
+ return { output, provider: fallback };
354
+ }
355
+ const err = error;
356
+ err.message = `Rate limited on ${getProviderDisplayName(provider)}`;
357
+ err.rateLimited = true;
358
+ throw err;
359
+ }
360
+ throw error;
361
+ }
362
+ }
363
+ async function invokeAIInteractive(prompt, config, providerOverride) {
364
+ const provider = providerOverride ?? config.ai.provider;
365
+ if (provider === "claude") {
366
+ const args = ["--permission-mode", config.claude.permission_mode, prompt];
367
+ return {
368
+ result: execa2("claude", args, { stdio: "inherit" }),
369
+ provider
370
+ };
371
+ } else {
372
+ return {
373
+ result: execa2("gemini", [prompt], { stdio: "inherit" }),
374
+ provider
375
+ };
376
+ }
377
+ }
378
+ function getProviderDisplayName(provider) {
379
+ return provider === "claude" ? "Claude" : "Gemini";
380
+ }
381
+ function isRateLimitError(error, provider) {
382
+ if (!error || typeof error !== "object") return false;
383
+ if (provider === "claude" && "exitCode" in error && error.exitCode === 2) {
384
+ return true;
385
+ }
386
+ if ("message" in error && typeof error.message === "string") {
387
+ const msg = error.message.toLowerCase();
388
+ if (msg.includes("rate limit") || msg.includes("quota exceeded") || msg.includes("too many requests")) {
389
+ return true;
390
+ }
391
+ }
392
+ return false;
393
+ }
394
+ async function invokeClaudeInternal(options) {
395
+ const args = ["--print"];
396
+ if (options.permissionMode) {
397
+ args.push("--permission-mode", options.permissionMode);
398
+ }
399
+ args.push(options.prompt);
400
+ if (options.printOutput) {
401
+ const subprocess = execa2("claude", args, {
402
+ stdio: "inherit"
403
+ });
404
+ await subprocess;
405
+ return "";
406
+ } else if (options.streamOutput) {
407
+ return new Promise((resolve, reject) => {
408
+ const child = spawn("claude", args, {
409
+ stdio: ["inherit", "pipe", "pipe"]
410
+ });
411
+ let output = "";
412
+ child.stdout.on("data", (chunk) => {
413
+ const text = chunk.toString();
414
+ output += text;
415
+ process.stdout.write(text);
416
+ });
417
+ child.stderr.on("data", (chunk) => {
418
+ process.stderr.write(chunk);
419
+ });
420
+ child.on("close", (code) => {
421
+ if (code === 0) {
422
+ resolve(output);
423
+ } else {
424
+ const error = new Error(`Claude exited with code ${code}`);
425
+ error.exitCode = code ?? 1;
426
+ reject(error);
427
+ }
428
+ });
429
+ child.on("error", reject);
430
+ });
431
+ } else {
432
+ const { stdout } = await execa2("claude", args);
433
+ return stdout;
434
+ }
435
+ }
436
+ async function invokeGeminiInternal(options) {
437
+ const args = [];
438
+ args.push(options.prompt);
439
+ if (options.printOutput) {
440
+ const subprocess = execa2("gemini", args, {
441
+ stdio: "inherit"
442
+ });
443
+ await subprocess;
444
+ return "";
445
+ } else if (options.streamOutput) {
446
+ return new Promise((resolve, reject) => {
447
+ const child = spawn("gemini", args, {
448
+ stdio: ["inherit", "pipe", "pipe"]
449
+ });
450
+ let output = "";
451
+ child.stdout.on("data", (chunk) => {
452
+ const text = chunk.toString();
453
+ output += text;
454
+ process.stdout.write(text);
455
+ });
456
+ child.stderr.on("data", (chunk) => {
457
+ process.stderr.write(chunk);
458
+ });
459
+ child.on("close", (code) => {
460
+ if (code === 0) {
461
+ resolve(output);
462
+ } else {
463
+ const error = new Error(`Gemini exited with code ${code}`);
464
+ error.exitCode = code ?? 1;
465
+ reject(error);
466
+ }
467
+ });
468
+ child.on("error", reject);
469
+ });
470
+ } else {
471
+ const { stdout } = await execa2("gemini", args);
472
+ return stdout;
473
+ }
474
+ }
475
+
384
476
  // src/commands/create.ts
385
477
  async function createCommand(description, options) {
386
478
  logger.bold("Creating AI-enhanced ticket...");
387
479
  logger.newline();
388
- const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
480
+ const config = loadConfig();
481
+ const provider = options.provider ?? config.ai.provider;
482
+ const providerName = getProviderDisplayName(provider);
483
+ const [ghAuth, aiOk] = await Promise.all([
484
+ checkGhAuth(),
485
+ checkAIProvider(provider)
486
+ ]);
389
487
  if (!ghAuth) {
390
488
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
391
489
  process.exit(1);
392
490
  }
393
- if (!claudeOk) {
394
- logger.error("Claude CLI not found. Please install claude CLI first.");
491
+ if (!aiOk) {
492
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
395
493
  process.exit(1);
396
494
  }
397
495
  const agentInstructions = loadAgentInstructions();
398
- let claudeOutput;
496
+ let aiOutput;
399
497
  let additionalHints = null;
400
498
  while (true) {
401
499
  const prompt = buildTicketPrompt(description, agentInstructions, additionalHints);
402
500
  try {
403
- console.log(chalk.dim("\u250C\u2500 Generating ticket... \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
501
+ console.log(chalk.dim(`\u250C\u2500 Generating ticket with ${providerName}... \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
404
502
  logger.newline();
405
- claudeOutput = await invokeClaude({ prompt, streamOutput: true });
503
+ const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
504
+ aiOutput = result.output;
406
505
  logger.newline();
407
506
  console.log(chalk.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
408
507
  logger.newline();
409
508
  } catch (error) {
410
- logger.error(`Claude invocation failed: ${error}`);
509
+ logger.error(`${providerName} invocation failed: ${error}`);
411
510
  return;
412
511
  }
413
- const meta = parseTicketMeta(claudeOutput);
512
+ const meta = parseTicketMeta(aiOutput);
414
513
  if (!meta) {
415
514
  logger.warning("Could not parse metadata from Claude output. Using defaults.");
416
515
  }
@@ -420,7 +519,10 @@ async function createCommand(description, options) {
420
519
  risk: "low",
421
520
  area: "shared"
422
521
  };
423
- const issueBody = extractIssueBody(claudeOutput);
522
+ const issueBody = extractIssueBody(aiOutput) + `
523
+
524
+ ---
525
+ *Created with ${providerName} by [gent](https://github.com/Rotorsoft/gent)*`;
424
526
  const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
425
527
  const labels = buildIssueLabels(finalMeta);
426
528
  displayTicketPreview(title, finalMeta, issueBody);
@@ -621,9 +723,9 @@ function getStatusColor(status) {
621
723
  import inquirer3 from "inquirer";
622
724
 
623
725
  // src/lib/git.ts
624
- import { execa as execa2 } from "execa";
726
+ import { execa as execa3 } from "execa";
625
727
  async function getCurrentBranch() {
626
- const { stdout } = await execa2("git", ["branch", "--show-current"]);
728
+ const { stdout } = await execa3("git", ["branch", "--show-current"]);
627
729
  return stdout.trim();
628
730
  }
629
731
  async function isOnMainBranch() {
@@ -632,14 +734,14 @@ async function isOnMainBranch() {
632
734
  }
633
735
  async function getDefaultBranch() {
634
736
  try {
635
- const { stdout } = await execa2("git", [
737
+ const { stdout } = await execa3("git", [
636
738
  "symbolic-ref",
637
739
  "refs/remotes/origin/HEAD"
638
740
  ]);
639
741
  return stdout.trim().replace("refs/remotes/origin/", "");
640
742
  } catch {
641
743
  try {
642
- await execa2("git", ["rev-parse", "--verify", "main"]);
744
+ await execa3("git", ["rev-parse", "--verify", "main"]);
643
745
  return "main";
644
746
  } catch {
645
747
  return "master";
@@ -648,7 +750,7 @@ async function getDefaultBranch() {
648
750
  }
649
751
  async function branchExists(name) {
650
752
  try {
651
- await execa2("git", ["rev-parse", "--verify", name]);
753
+ await execa3("git", ["rev-parse", "--verify", name]);
652
754
  return true;
653
755
  } catch {
654
756
  return false;
@@ -656,21 +758,21 @@ async function branchExists(name) {
656
758
  }
657
759
  async function createBranch(name, from) {
658
760
  if (from) {
659
- await execa2("git", ["checkout", "-b", name, from]);
761
+ await execa3("git", ["checkout", "-b", name, from]);
660
762
  } else {
661
- await execa2("git", ["checkout", "-b", name]);
763
+ await execa3("git", ["checkout", "-b", name]);
662
764
  }
663
765
  }
664
766
  async function checkoutBranch(name) {
665
- await execa2("git", ["checkout", name]);
767
+ await execa3("git", ["checkout", name]);
666
768
  }
667
769
  async function hasUncommittedChanges() {
668
- const { stdout } = await execa2("git", ["status", "--porcelain"]);
770
+ const { stdout } = await execa3("git", ["status", "--porcelain"]);
669
771
  return stdout.trim().length > 0;
670
772
  }
671
773
  async function getUnpushedCommits() {
672
774
  try {
673
- const { stdout } = await execa2("git", [
775
+ const { stdout } = await execa3("git", [
674
776
  "log",
675
777
  "@{u}..HEAD",
676
778
  "--oneline"
@@ -682,18 +784,18 @@ async function getUnpushedCommits() {
682
784
  }
683
785
  async function pushBranch(branch) {
684
786
  const branchName = branch || await getCurrentBranch();
685
- await execa2("git", ["push", "-u", "origin", branchName]);
787
+ await execa3("git", ["push", "-u", "origin", branchName]);
686
788
  }
687
789
  async function getAuthorInitials() {
688
790
  try {
689
- const { stdout } = await execa2("git", ["config", "user.initials"]);
791
+ const { stdout } = await execa3("git", ["config", "user.initials"]);
690
792
  if (stdout.trim()) {
691
793
  return stdout.trim();
692
794
  }
693
795
  } catch {
694
796
  }
695
797
  try {
696
- const { stdout } = await execa2("git", ["config", "user.name"]);
798
+ const { stdout } = await execa3("git", ["config", "user.name"]);
697
799
  const name = stdout.trim();
698
800
  if (name) {
699
801
  const parts = name.split(/\s+/);
@@ -705,7 +807,7 @@ async function getAuthorInitials() {
705
807
  }
706
808
  async function getCommitsSinceBase(base = "main") {
707
809
  try {
708
- const { stdout } = await execa2("git", [
810
+ const { stdout } = await execa3("git", [
709
811
  "log",
710
812
  `${base}..HEAD`,
711
813
  "--pretty=format:%s"
@@ -717,7 +819,7 @@ async function getCommitsSinceBase(base = "main") {
717
819
  }
718
820
  async function getDiffSummary(base = "main") {
719
821
  try {
720
- const { stdout } = await execa2("git", [
822
+ const { stdout } = await execa3("git", [
721
823
  "diff",
722
824
  `${base}...HEAD`,
723
825
  "--stat"
@@ -728,7 +830,7 @@ async function getDiffSummary(base = "main") {
728
830
  }
729
831
  }
730
832
  async function getCurrentCommitSha() {
731
- const { stdout } = await execa2("git", ["rev-parse", "HEAD"]);
833
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
732
834
  return stdout.trim();
733
835
  }
734
836
  async function hasNewCommits(beforeSha) {
@@ -811,13 +913,19 @@ function extractIssueNumber(branchName) {
811
913
  async function runCommand(issueNumberArg, options) {
812
914
  logger.bold("Running AI implementation workflow...");
813
915
  logger.newline();
814
- const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
916
+ const config = loadConfig();
917
+ const provider = options.provider ?? config.ai.provider;
918
+ const providerName = getProviderDisplayName(provider);
919
+ const [ghAuth, aiOk] = await Promise.all([
920
+ checkGhAuth(),
921
+ checkAIProvider(provider)
922
+ ]);
815
923
  if (!ghAuth) {
816
924
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
817
925
  process.exit(1);
818
926
  }
819
- if (!claudeOk) {
820
- logger.error("Claude CLI not found. Please install claude CLI first.");
927
+ if (!aiOk) {
928
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
821
929
  process.exit(1);
822
930
  }
823
931
  const hasChanges = await hasUncommittedChanges();
@@ -836,7 +944,6 @@ async function runCommand(issueNumberArg, options) {
836
944
  process.exit(0);
837
945
  }
838
946
  }
839
- const config = loadConfig();
840
947
  const workflowLabels = getWorkflowLabels(config);
841
948
  let issueNumber;
842
949
  if (options.auto) {
@@ -943,8 +1050,8 @@ Labels: ${issue.labels.join(", ")}`);
943
1050
  const progressContent = readProgress(config);
944
1051
  const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
945
1052
  logger.newline();
946
- logger.info("Starting Claude implementation session...");
947
- logger.dim("Claude will implement the feature and create a commit.");
1053
+ logger.info(`Starting ${colors.provider(providerName)} implementation session...`);
1054
+ logger.dim(`${providerName} will implement the feature and create a commit.`);
948
1055
  logger.dim("Review the changes before pushing.");
949
1056
  logger.newline();
950
1057
  const beforeSha = await getCurrentCommitSha();
@@ -954,15 +1061,17 @@ Labels: ${issue.labels.join(", ")}`);
954
1061
  };
955
1062
  process.on("SIGINT", handleSignal);
956
1063
  process.on("SIGTERM", handleSignal);
957
- let claudeExitCode;
1064
+ let aiExitCode;
1065
+ let usedProvider = provider;
958
1066
  try {
959
- const result = await invokeClaudeInteractive(prompt, config);
960
- claudeExitCode = result.exitCode;
1067
+ const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
1068
+ usedProvider = actualProvider;
1069
+ aiExitCode = result.exitCode ?? void 0;
961
1070
  } catch (error) {
962
1071
  if (error && typeof error === "object" && "exitCode" in error) {
963
- claudeExitCode = error.exitCode;
1072
+ aiExitCode = error.exitCode;
964
1073
  }
965
- logger.error(`Claude session failed: ${error}`);
1074
+ logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
966
1075
  } finally {
967
1076
  process.off("SIGINT", handleSignal);
968
1077
  process.off("SIGTERM", handleSignal);
@@ -973,8 +1082,9 @@ Labels: ${issue.labels.join(", ")}`);
973
1082
  logger.warning("Operation was cancelled. Labels unchanged.");
974
1083
  return;
975
1084
  }
1085
+ const usedProviderName = getProviderDisplayName(usedProvider);
976
1086
  if (commitsCreated) {
977
- logger.success("Claude session completed with new commits.");
1087
+ logger.success(`${usedProviderName} session completed with new commits.`);
978
1088
  try {
979
1089
  await updateIssueLabels(issueNumber, {
980
1090
  add: [workflowLabels.completed],
@@ -987,7 +1097,7 @@ Labels: ${issue.labels.join(", ")}`);
987
1097
  try {
988
1098
  await addIssueComment(
989
1099
  issueNumber,
990
- `AI implementation completed on branch \`${branchName}\`.
1100
+ `AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
991
1101
 
992
1102
  Please review the changes and create a PR when ready.`
993
1103
  );
@@ -996,9 +1106,9 @@ Please review the changes and create a PR when ready.`
996
1106
  logger.warning(`Failed to post comment: ${error}`);
997
1107
  }
998
1108
  } else {
999
- const isRateLimited = claudeExitCode === 2;
1109
+ const isRateLimited = aiExitCode === 2;
1000
1110
  if (isRateLimited) {
1001
- logger.warning("Claude session ended due to rate limits. No commits were created.");
1111
+ logger.warning(`${usedProviderName} session ended due to rate limits. No commits were created.`);
1002
1112
  try {
1003
1113
  await updateIssueLabels(issueNumber, {
1004
1114
  add: [workflowLabels.blocked],
@@ -1011,7 +1121,7 @@ Please review the changes and create a PR when ready.`
1011
1121
  try {
1012
1122
  await addIssueComment(
1013
1123
  issueNumber,
1014
- `AI implementation was blocked due to API rate limits on branch \`${branchName}\`.
1124
+ `AI implementation was blocked due to API rate limits on branch \`${branchName}\` (${usedProviderName}).
1015
1125
 
1016
1126
  No commits were created. Please retry later.`
1017
1127
  );
@@ -1020,7 +1130,7 @@ No commits were created. Please retry later.`
1020
1130
  logger.warning(`Failed to post comment: ${error}`);
1021
1131
  }
1022
1132
  } else {
1023
- logger.warning("Claude session completed but no commits were created. Labels unchanged.");
1133
+ logger.warning(`${usedProviderName} session completed but no commits were created. Labels unchanged.`);
1024
1134
  }
1025
1135
  return;
1026
1136
  }
@@ -1058,13 +1168,19 @@ import inquirer4 from "inquirer";
1058
1168
  async function prCommand(options) {
1059
1169
  logger.bold("Creating AI-enhanced pull request...");
1060
1170
  logger.newline();
1061
- const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
1171
+ const config = loadConfig();
1172
+ const provider = options.provider ?? config.ai.provider;
1173
+ const providerName = getProviderDisplayName(provider);
1174
+ const [ghAuth, aiOk] = await Promise.all([
1175
+ checkGhAuth(),
1176
+ checkAIProvider(provider)
1177
+ ]);
1062
1178
  if (!ghAuth) {
1063
1179
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1064
1180
  process.exit(1);
1065
1181
  }
1066
- if (!claudeOk) {
1067
- logger.error("Claude CLI not found. Please install claude CLI first.");
1182
+ if (!aiOk) {
1183
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
1068
1184
  process.exit(1);
1069
1185
  }
1070
1186
  if (await isOnMainBranch()) {
@@ -1121,14 +1237,19 @@ async function prCommand(options) {
1121
1237
  const prompt = buildPrPrompt(issue, commits, diffSummary);
1122
1238
  let prBody;
1123
1239
  try {
1124
- logger.info("Generating PR description with Claude...");
1240
+ logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
1125
1241
  logger.newline();
1126
- prBody = await invokeClaude({ prompt, streamOutput: true });
1242
+ const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
1243
+ prBody = result.output;
1127
1244
  logger.newline();
1128
1245
  } catch (error) {
1129
- logger.warning(`Claude invocation failed: ${error}`);
1246
+ logger.warning(`${providerName} invocation failed: ${error}`);
1130
1247
  prBody = generateFallbackBody(issue, commits);
1131
1248
  }
1249
+ prBody += `
1250
+
1251
+ ---
1252
+ *Created with ${providerName} by [gent](https://github.com/Rotorsoft/gent)*`;
1132
1253
  const prTitle = issue?.title || commits[0] || currentBranch;
1133
1254
  let prUrl;
1134
1255
  try {
@@ -1206,6 +1327,14 @@ async function statusCommand() {
1206
1327
  logger.warning(` ${config.progress.file} not found`);
1207
1328
  }
1208
1329
  logger.newline();
1330
+ logger.bold("AI Provider:");
1331
+ const providerName = getProviderDisplayName(config.ai.provider);
1332
+ logger.info(` Active: ${colors.provider(providerName)}`);
1333
+ if (config.ai.fallback_provider) {
1334
+ const fallbackName = getProviderDisplayName(config.ai.fallback_provider);
1335
+ logger.info(` Fallback: ${fallbackName} (auto: ${config.ai.auto_fallback ? "enabled" : "disabled"})`);
1336
+ }
1337
+ logger.newline();
1209
1338
  logger.bold("Prerequisites:");
1210
1339
  const ghAuth = await checkGhAuth();
1211
1340
  if (ghAuth) {
@@ -1214,10 +1343,22 @@ async function statusCommand() {
1214
1343
  logger.error(" GitHub CLI not authenticated");
1215
1344
  }
1216
1345
  const claudeOk = await checkClaudeCli();
1346
+ const geminiOk = await checkGeminiCli();
1347
+ const getProviderStatus = (provider) => {
1348
+ const isActive = config.ai.provider === provider;
1349
+ const isFallback = config.ai.fallback_provider === provider;
1350
+ const suffix = isActive ? " (active)" : isFallback ? " (fallback)" : "";
1351
+ return suffix;
1352
+ };
1217
1353
  if (claudeOk) {
1218
- logger.success(" Claude CLI available");
1354
+ logger.success(` Claude CLI available${getProviderStatus("claude")}`);
1355
+ } else {
1356
+ logger.error(` Claude CLI not found${getProviderStatus("claude")}`);
1357
+ }
1358
+ if (geminiOk) {
1359
+ logger.success(` Gemini CLI available${getProviderStatus("gemini")}`);
1219
1360
  } else {
1220
- logger.error(" Claude CLI not found");
1361
+ logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
1221
1362
  }
1222
1363
  logger.newline();
1223
1364
  logger.bold("Git Status:");
@@ -1313,8 +1454,8 @@ program.command("init").description("Initialize gent workflow in current reposit
1313
1454
  program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
1314
1455
  await setupLabelsCommand();
1315
1456
  });
1316
- program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").action(async (description, options) => {
1317
- await createCommand(description, { yes: options.yes });
1457
+ 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)").action(async (description, options) => {
1458
+ await createCommand(description, { yes: options.yes, provider: options.provider });
1318
1459
  });
1319
1460
  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) => {
1320
1461
  await listCommand({
@@ -1323,11 +1464,11 @@ program.command("list").description("List GitHub issues by label/status").option
1323
1464
  limit: parseInt(options.limit, 10)
1324
1465
  });
1325
1466
  });
1326
- program.command("run [issue-number]").description("Run Claude to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").action(async (issueNumber, options) => {
1327
- await runCommand(issueNumber, { auto: options.auto });
1467
+ 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) => {
1468
+ await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
1328
1469
  });
1329
- program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").action(async (options) => {
1330
- await prCommand({ draft: options.draft });
1470
+ 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) => {
1471
+ await prCommand({ draft: options.draft, provider: options.provider });
1331
1472
  });
1332
1473
  program.command("status").description("Show current workflow status").action(async () => {
1333
1474
  await statusCommand();