@rotorsoft/gent 1.1.2 → 1.2.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,7 @@ 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);
424
523
  const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
425
524
  const labels = buildIssueLabels(finalMeta);
426
525
  displayTicketPreview(title, finalMeta, issueBody);
@@ -621,9 +720,9 @@ function getStatusColor(status) {
621
720
  import inquirer3 from "inquirer";
622
721
 
623
722
  // src/lib/git.ts
624
- import { execa as execa2 } from "execa";
723
+ import { execa as execa3 } from "execa";
625
724
  async function getCurrentBranch() {
626
- const { stdout } = await execa2("git", ["branch", "--show-current"]);
725
+ const { stdout } = await execa3("git", ["branch", "--show-current"]);
627
726
  return stdout.trim();
628
727
  }
629
728
  async function isOnMainBranch() {
@@ -632,14 +731,14 @@ async function isOnMainBranch() {
632
731
  }
633
732
  async function getDefaultBranch() {
634
733
  try {
635
- const { stdout } = await execa2("git", [
734
+ const { stdout } = await execa3("git", [
636
735
  "symbolic-ref",
637
736
  "refs/remotes/origin/HEAD"
638
737
  ]);
639
738
  return stdout.trim().replace("refs/remotes/origin/", "");
640
739
  } catch {
641
740
  try {
642
- await execa2("git", ["rev-parse", "--verify", "main"]);
741
+ await execa3("git", ["rev-parse", "--verify", "main"]);
643
742
  return "main";
644
743
  } catch {
645
744
  return "master";
@@ -648,7 +747,7 @@ async function getDefaultBranch() {
648
747
  }
649
748
  async function branchExists(name) {
650
749
  try {
651
- await execa2("git", ["rev-parse", "--verify", name]);
750
+ await execa3("git", ["rev-parse", "--verify", name]);
652
751
  return true;
653
752
  } catch {
654
753
  return false;
@@ -656,21 +755,21 @@ async function branchExists(name) {
656
755
  }
657
756
  async function createBranch(name, from) {
658
757
  if (from) {
659
- await execa2("git", ["checkout", "-b", name, from]);
758
+ await execa3("git", ["checkout", "-b", name, from]);
660
759
  } else {
661
- await execa2("git", ["checkout", "-b", name]);
760
+ await execa3("git", ["checkout", "-b", name]);
662
761
  }
663
762
  }
664
763
  async function checkoutBranch(name) {
665
- await execa2("git", ["checkout", name]);
764
+ await execa3("git", ["checkout", name]);
666
765
  }
667
766
  async function hasUncommittedChanges() {
668
- const { stdout } = await execa2("git", ["status", "--porcelain"]);
767
+ const { stdout } = await execa3("git", ["status", "--porcelain"]);
669
768
  return stdout.trim().length > 0;
670
769
  }
671
770
  async function getUnpushedCommits() {
672
771
  try {
673
- const { stdout } = await execa2("git", [
772
+ const { stdout } = await execa3("git", [
674
773
  "log",
675
774
  "@{u}..HEAD",
676
775
  "--oneline"
@@ -682,18 +781,18 @@ async function getUnpushedCommits() {
682
781
  }
683
782
  async function pushBranch(branch) {
684
783
  const branchName = branch || await getCurrentBranch();
685
- await execa2("git", ["push", "-u", "origin", branchName]);
784
+ await execa3("git", ["push", "-u", "origin", branchName]);
686
785
  }
687
786
  async function getAuthorInitials() {
688
787
  try {
689
- const { stdout } = await execa2("git", ["config", "user.initials"]);
788
+ const { stdout } = await execa3("git", ["config", "user.initials"]);
690
789
  if (stdout.trim()) {
691
790
  return stdout.trim();
692
791
  }
693
792
  } catch {
694
793
  }
695
794
  try {
696
- const { stdout } = await execa2("git", ["config", "user.name"]);
795
+ const { stdout } = await execa3("git", ["config", "user.name"]);
697
796
  const name = stdout.trim();
698
797
  if (name) {
699
798
  const parts = name.split(/\s+/);
@@ -705,7 +804,7 @@ async function getAuthorInitials() {
705
804
  }
706
805
  async function getCommitsSinceBase(base = "main") {
707
806
  try {
708
- const { stdout } = await execa2("git", [
807
+ const { stdout } = await execa3("git", [
709
808
  "log",
710
809
  `${base}..HEAD`,
711
810
  "--pretty=format:%s"
@@ -717,7 +816,7 @@ async function getCommitsSinceBase(base = "main") {
717
816
  }
718
817
  async function getDiffSummary(base = "main") {
719
818
  try {
720
- const { stdout } = await execa2("git", [
819
+ const { stdout } = await execa3("git", [
721
820
  "diff",
722
821
  `${base}...HEAD`,
723
822
  "--stat"
@@ -728,7 +827,7 @@ async function getDiffSummary(base = "main") {
728
827
  }
729
828
  }
730
829
  async function getCurrentCommitSha() {
731
- const { stdout } = await execa2("git", ["rev-parse", "HEAD"]);
830
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
732
831
  return stdout.trim();
733
832
  }
734
833
  async function hasNewCommits(beforeSha) {
@@ -811,13 +910,19 @@ function extractIssueNumber(branchName) {
811
910
  async function runCommand(issueNumberArg, options) {
812
911
  logger.bold("Running AI implementation workflow...");
813
912
  logger.newline();
814
- const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
913
+ const config = loadConfig();
914
+ const provider = options.provider ?? config.ai.provider;
915
+ const providerName = getProviderDisplayName(provider);
916
+ const [ghAuth, aiOk] = await Promise.all([
917
+ checkGhAuth(),
918
+ checkAIProvider(provider)
919
+ ]);
815
920
  if (!ghAuth) {
816
921
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
817
922
  process.exit(1);
818
923
  }
819
- if (!claudeOk) {
820
- logger.error("Claude CLI not found. Please install claude CLI first.");
924
+ if (!aiOk) {
925
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
821
926
  process.exit(1);
822
927
  }
823
928
  const hasChanges = await hasUncommittedChanges();
@@ -836,7 +941,6 @@ async function runCommand(issueNumberArg, options) {
836
941
  process.exit(0);
837
942
  }
838
943
  }
839
- const config = loadConfig();
840
944
  const workflowLabels = getWorkflowLabels(config);
841
945
  let issueNumber;
842
946
  if (options.auto) {
@@ -943,8 +1047,8 @@ Labels: ${issue.labels.join(", ")}`);
943
1047
  const progressContent = readProgress(config);
944
1048
  const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
945
1049
  logger.newline();
946
- logger.info("Starting Claude implementation session...");
947
- logger.dim("Claude will implement the feature and create a commit.");
1050
+ logger.info(`Starting ${colors.provider(providerName)} implementation session...`);
1051
+ logger.dim(`${providerName} will implement the feature and create a commit.`);
948
1052
  logger.dim("Review the changes before pushing.");
949
1053
  logger.newline();
950
1054
  const beforeSha = await getCurrentCommitSha();
@@ -954,15 +1058,17 @@ Labels: ${issue.labels.join(", ")}`);
954
1058
  };
955
1059
  process.on("SIGINT", handleSignal);
956
1060
  process.on("SIGTERM", handleSignal);
957
- let claudeExitCode;
1061
+ let aiExitCode;
1062
+ let usedProvider = provider;
958
1063
  try {
959
- const result = await invokeClaudeInteractive(prompt, config);
960
- claudeExitCode = result.exitCode;
1064
+ const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
1065
+ usedProvider = actualProvider;
1066
+ aiExitCode = result.exitCode ?? void 0;
961
1067
  } catch (error) {
962
1068
  if (error && typeof error === "object" && "exitCode" in error) {
963
- claudeExitCode = error.exitCode;
1069
+ aiExitCode = error.exitCode;
964
1070
  }
965
- logger.error(`Claude session failed: ${error}`);
1071
+ logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
966
1072
  } finally {
967
1073
  process.off("SIGINT", handleSignal);
968
1074
  process.off("SIGTERM", handleSignal);
@@ -973,8 +1079,9 @@ Labels: ${issue.labels.join(", ")}`);
973
1079
  logger.warning("Operation was cancelled. Labels unchanged.");
974
1080
  return;
975
1081
  }
1082
+ const usedProviderName = getProviderDisplayName(usedProvider);
976
1083
  if (commitsCreated) {
977
- logger.success("Claude session completed with new commits.");
1084
+ logger.success(`${usedProviderName} session completed with new commits.`);
978
1085
  try {
979
1086
  await updateIssueLabels(issueNumber, {
980
1087
  add: [workflowLabels.completed],
@@ -987,7 +1094,7 @@ Labels: ${issue.labels.join(", ")}`);
987
1094
  try {
988
1095
  await addIssueComment(
989
1096
  issueNumber,
990
- `AI implementation completed on branch \`${branchName}\`.
1097
+ `AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
991
1098
 
992
1099
  Please review the changes and create a PR when ready.`
993
1100
  );
@@ -996,9 +1103,9 @@ Please review the changes and create a PR when ready.`
996
1103
  logger.warning(`Failed to post comment: ${error}`);
997
1104
  }
998
1105
  } else {
999
- const isRateLimited = claudeExitCode === 2;
1106
+ const isRateLimited = aiExitCode === 2;
1000
1107
  if (isRateLimited) {
1001
- logger.warning("Claude session ended due to rate limits. No commits were created.");
1108
+ logger.warning(`${usedProviderName} session ended due to rate limits. No commits were created.`);
1002
1109
  try {
1003
1110
  await updateIssueLabels(issueNumber, {
1004
1111
  add: [workflowLabels.blocked],
@@ -1011,7 +1118,7 @@ Please review the changes and create a PR when ready.`
1011
1118
  try {
1012
1119
  await addIssueComment(
1013
1120
  issueNumber,
1014
- `AI implementation was blocked due to API rate limits on branch \`${branchName}\`.
1121
+ `AI implementation was blocked due to API rate limits on branch \`${branchName}\` (${usedProviderName}).
1015
1122
 
1016
1123
  No commits were created. Please retry later.`
1017
1124
  );
@@ -1020,7 +1127,7 @@ No commits were created. Please retry later.`
1020
1127
  logger.warning(`Failed to post comment: ${error}`);
1021
1128
  }
1022
1129
  } else {
1023
- logger.warning("Claude session completed but no commits were created. Labels unchanged.");
1130
+ logger.warning(`${usedProviderName} session completed but no commits were created. Labels unchanged.`);
1024
1131
  }
1025
1132
  return;
1026
1133
  }
@@ -1058,13 +1165,19 @@ import inquirer4 from "inquirer";
1058
1165
  async function prCommand(options) {
1059
1166
  logger.bold("Creating AI-enhanced pull request...");
1060
1167
  logger.newline();
1061
- const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
1168
+ const config = loadConfig();
1169
+ const provider = options.provider ?? config.ai.provider;
1170
+ const providerName = getProviderDisplayName(provider);
1171
+ const [ghAuth, aiOk] = await Promise.all([
1172
+ checkGhAuth(),
1173
+ checkAIProvider(provider)
1174
+ ]);
1062
1175
  if (!ghAuth) {
1063
1176
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1064
1177
  process.exit(1);
1065
1178
  }
1066
- if (!claudeOk) {
1067
- logger.error("Claude CLI not found. Please install claude CLI first.");
1179
+ if (!aiOk) {
1180
+ logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
1068
1181
  process.exit(1);
1069
1182
  }
1070
1183
  if (await isOnMainBranch()) {
@@ -1121,12 +1234,13 @@ async function prCommand(options) {
1121
1234
  const prompt = buildPrPrompt(issue, commits, diffSummary);
1122
1235
  let prBody;
1123
1236
  try {
1124
- logger.info("Generating PR description with Claude...");
1237
+ logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
1125
1238
  logger.newline();
1126
- prBody = await invokeClaude({ prompt, streamOutput: true });
1239
+ const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
1240
+ prBody = result.output;
1127
1241
  logger.newline();
1128
1242
  } catch (error) {
1129
- logger.warning(`Claude invocation failed: ${error}`);
1243
+ logger.warning(`${providerName} invocation failed: ${error}`);
1130
1244
  prBody = generateFallbackBody(issue, commits);
1131
1245
  }
1132
1246
  const prTitle = issue?.title || commits[0] || currentBranch;
@@ -1206,6 +1320,14 @@ async function statusCommand() {
1206
1320
  logger.warning(` ${config.progress.file} not found`);
1207
1321
  }
1208
1322
  logger.newline();
1323
+ logger.bold("AI Provider:");
1324
+ const providerName = getProviderDisplayName(config.ai.provider);
1325
+ logger.info(` Active: ${colors.provider(providerName)}`);
1326
+ if (config.ai.fallback_provider) {
1327
+ const fallbackName = getProviderDisplayName(config.ai.fallback_provider);
1328
+ logger.info(` Fallback: ${fallbackName} (auto: ${config.ai.auto_fallback ? "enabled" : "disabled"})`);
1329
+ }
1330
+ logger.newline();
1209
1331
  logger.bold("Prerequisites:");
1210
1332
  const ghAuth = await checkGhAuth();
1211
1333
  if (ghAuth) {
@@ -1214,10 +1336,22 @@ async function statusCommand() {
1214
1336
  logger.error(" GitHub CLI not authenticated");
1215
1337
  }
1216
1338
  const claudeOk = await checkClaudeCli();
1339
+ const geminiOk = await checkGeminiCli();
1340
+ const getProviderStatus = (provider) => {
1341
+ const isActive = config.ai.provider === provider;
1342
+ const isFallback = config.ai.fallback_provider === provider;
1343
+ const suffix = isActive ? " (active)" : isFallback ? " (fallback)" : "";
1344
+ return suffix;
1345
+ };
1217
1346
  if (claudeOk) {
1218
- logger.success(" Claude CLI available");
1347
+ logger.success(` Claude CLI available${getProviderStatus("claude")}`);
1348
+ } else {
1349
+ logger.error(` Claude CLI not found${getProviderStatus("claude")}`);
1350
+ }
1351
+ if (geminiOk) {
1352
+ logger.success(` Gemini CLI available${getProviderStatus("gemini")}`);
1219
1353
  } else {
1220
- logger.error(" Claude CLI not found");
1354
+ logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
1221
1355
  }
1222
1356
  logger.newline();
1223
1357
  logger.bold("Git Status:");
@@ -1313,8 +1447,8 @@ program.command("init").description("Initialize gent workflow in current reposit
1313
1447
  program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
1314
1448
  await setupLabelsCommand();
1315
1449
  });
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 });
1450
+ 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) => {
1451
+ await createCommand(description, { yes: options.yes, provider: options.provider });
1318
1452
  });
1319
1453
  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
1454
  await listCommand({
@@ -1323,11 +1457,11 @@ program.command("list").description("List GitHub issues by label/status").option
1323
1457
  limit: parseInt(options.limit, 10)
1324
1458
  });
1325
1459
  });
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 });
1460
+ 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) => {
1461
+ await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
1328
1462
  });
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 });
1463
+ 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) => {
1464
+ await prCommand({ draft: options.draft, provider: options.provider });
1331
1465
  });
1332
1466
  program.command("status").description("Show current workflow status").action(async () => {
1333
1467
  await statusCommand();