@rotorsoft/gent 1.1.1 → 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
 
@@ -254,7 +209,9 @@ ${additionalHints}
254
209
 
255
210
  ` : ""}
256
211
 
257
- Create a detailed GitHub issue following this exact template:
212
+ Create a detailed GitHub issue following this exact template.
213
+
214
+ IMPORTANT: Start your output IMMEDIATELY with "## Description" - do not include any preamble, commentary, or introduction before the template.
258
215
 
259
216
  ## Description
260
217
  [Clear user-facing description of what needs to be done]
@@ -371,39 +328,188 @@ function parseTicketMeta(output) {
371
328
  };
372
329
  }
373
330
  function extractIssueBody(output) {
374
- return output.replace(/\n?META:type=\w+,priority=\w+,risk=\w+,area=\w+\s*$/, "").trim();
331
+ let body = output.replace(/\n?META:type=\w+,priority=\w+,risk=\w+,area=\w+\s*$/, "").trim();
332
+ const descriptionIndex = body.indexOf("## Description");
333
+ if (descriptionIndex > 0) {
334
+ body = body.substring(descriptionIndex);
335
+ }
336
+ return body;
337
+ }
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
+ }
375
474
  }
376
475
 
377
476
  // src/commands/create.ts
378
477
  async function createCommand(description, options) {
379
478
  logger.bold("Creating AI-enhanced ticket...");
380
479
  logger.newline();
381
- 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
+ ]);
382
487
  if (!ghAuth) {
383
488
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
384
489
  process.exit(1);
385
490
  }
386
- if (!claudeOk) {
387
- 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.`);
388
493
  process.exit(1);
389
494
  }
390
495
  const agentInstructions = loadAgentInstructions();
391
- let claudeOutput;
496
+ let aiOutput;
392
497
  let additionalHints = null;
393
498
  while (true) {
394
499
  const prompt = buildTicketPrompt(description, agentInstructions, additionalHints);
395
500
  try {
396
- 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`));
397
502
  logger.newline();
398
- claudeOutput = await invokeClaude({ prompt, streamOutput: true });
503
+ const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
504
+ aiOutput = result.output;
399
505
  logger.newline();
400
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"));
401
507
  logger.newline();
402
508
  } catch (error) {
403
- logger.error(`Claude invocation failed: ${error}`);
509
+ logger.error(`${providerName} invocation failed: ${error}`);
404
510
  return;
405
511
  }
406
- const meta = parseTicketMeta(claudeOutput);
512
+ const meta = parseTicketMeta(aiOutput);
407
513
  if (!meta) {
408
514
  logger.warning("Could not parse metadata from Claude output. Using defaults.");
409
515
  }
@@ -413,7 +519,7 @@ async function createCommand(description, options) {
413
519
  risk: "low",
414
520
  area: "shared"
415
521
  };
416
- const issueBody = extractIssueBody(claudeOutput);
522
+ const issueBody = extractIssueBody(aiOutput);
417
523
  const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
418
524
  const labels = buildIssueLabels(finalMeta);
419
525
  displayTicketPreview(title, finalMeta, issueBody);
@@ -614,9 +720,9 @@ function getStatusColor(status) {
614
720
  import inquirer3 from "inquirer";
615
721
 
616
722
  // src/lib/git.ts
617
- import { execa as execa2 } from "execa";
723
+ import { execa as execa3 } from "execa";
618
724
  async function getCurrentBranch() {
619
- const { stdout } = await execa2("git", ["branch", "--show-current"]);
725
+ const { stdout } = await execa3("git", ["branch", "--show-current"]);
620
726
  return stdout.trim();
621
727
  }
622
728
  async function isOnMainBranch() {
@@ -625,14 +731,14 @@ async function isOnMainBranch() {
625
731
  }
626
732
  async function getDefaultBranch() {
627
733
  try {
628
- const { stdout } = await execa2("git", [
734
+ const { stdout } = await execa3("git", [
629
735
  "symbolic-ref",
630
736
  "refs/remotes/origin/HEAD"
631
737
  ]);
632
738
  return stdout.trim().replace("refs/remotes/origin/", "");
633
739
  } catch {
634
740
  try {
635
- await execa2("git", ["rev-parse", "--verify", "main"]);
741
+ await execa3("git", ["rev-parse", "--verify", "main"]);
636
742
  return "main";
637
743
  } catch {
638
744
  return "master";
@@ -641,7 +747,7 @@ async function getDefaultBranch() {
641
747
  }
642
748
  async function branchExists(name) {
643
749
  try {
644
- await execa2("git", ["rev-parse", "--verify", name]);
750
+ await execa3("git", ["rev-parse", "--verify", name]);
645
751
  return true;
646
752
  } catch {
647
753
  return false;
@@ -649,21 +755,21 @@ async function branchExists(name) {
649
755
  }
650
756
  async function createBranch(name, from) {
651
757
  if (from) {
652
- await execa2("git", ["checkout", "-b", name, from]);
758
+ await execa3("git", ["checkout", "-b", name, from]);
653
759
  } else {
654
- await execa2("git", ["checkout", "-b", name]);
760
+ await execa3("git", ["checkout", "-b", name]);
655
761
  }
656
762
  }
657
763
  async function checkoutBranch(name) {
658
- await execa2("git", ["checkout", name]);
764
+ await execa3("git", ["checkout", name]);
659
765
  }
660
766
  async function hasUncommittedChanges() {
661
- const { stdout } = await execa2("git", ["status", "--porcelain"]);
767
+ const { stdout } = await execa3("git", ["status", "--porcelain"]);
662
768
  return stdout.trim().length > 0;
663
769
  }
664
770
  async function getUnpushedCommits() {
665
771
  try {
666
- const { stdout } = await execa2("git", [
772
+ const { stdout } = await execa3("git", [
667
773
  "log",
668
774
  "@{u}..HEAD",
669
775
  "--oneline"
@@ -675,18 +781,18 @@ async function getUnpushedCommits() {
675
781
  }
676
782
  async function pushBranch(branch) {
677
783
  const branchName = branch || await getCurrentBranch();
678
- await execa2("git", ["push", "-u", "origin", branchName]);
784
+ await execa3("git", ["push", "-u", "origin", branchName]);
679
785
  }
680
786
  async function getAuthorInitials() {
681
787
  try {
682
- const { stdout } = await execa2("git", ["config", "user.initials"]);
788
+ const { stdout } = await execa3("git", ["config", "user.initials"]);
683
789
  if (stdout.trim()) {
684
790
  return stdout.trim();
685
791
  }
686
792
  } catch {
687
793
  }
688
794
  try {
689
- const { stdout } = await execa2("git", ["config", "user.name"]);
795
+ const { stdout } = await execa3("git", ["config", "user.name"]);
690
796
  const name = stdout.trim();
691
797
  if (name) {
692
798
  const parts = name.split(/\s+/);
@@ -698,7 +804,7 @@ async function getAuthorInitials() {
698
804
  }
699
805
  async function getCommitsSinceBase(base = "main") {
700
806
  try {
701
- const { stdout } = await execa2("git", [
807
+ const { stdout } = await execa3("git", [
702
808
  "log",
703
809
  `${base}..HEAD`,
704
810
  "--pretty=format:%s"
@@ -710,7 +816,7 @@ async function getCommitsSinceBase(base = "main") {
710
816
  }
711
817
  async function getDiffSummary(base = "main") {
712
818
  try {
713
- const { stdout } = await execa2("git", [
819
+ const { stdout } = await execa3("git", [
714
820
  "diff",
715
821
  `${base}...HEAD`,
716
822
  "--stat"
@@ -721,7 +827,7 @@ async function getDiffSummary(base = "main") {
721
827
  }
722
828
  }
723
829
  async function getCurrentCommitSha() {
724
- const { stdout } = await execa2("git", ["rev-parse", "HEAD"]);
830
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
725
831
  return stdout.trim();
726
832
  }
727
833
  async function hasNewCommits(beforeSha) {
@@ -804,13 +910,19 @@ function extractIssueNumber(branchName) {
804
910
  async function runCommand(issueNumberArg, options) {
805
911
  logger.bold("Running AI implementation workflow...");
806
912
  logger.newline();
807
- 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
+ ]);
808
920
  if (!ghAuth) {
809
921
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
810
922
  process.exit(1);
811
923
  }
812
- if (!claudeOk) {
813
- 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.`);
814
926
  process.exit(1);
815
927
  }
816
928
  const hasChanges = await hasUncommittedChanges();
@@ -829,7 +941,6 @@ async function runCommand(issueNumberArg, options) {
829
941
  process.exit(0);
830
942
  }
831
943
  }
832
- const config = loadConfig();
833
944
  const workflowLabels = getWorkflowLabels(config);
834
945
  let issueNumber;
835
946
  if (options.auto) {
@@ -936,8 +1047,8 @@ Labels: ${issue.labels.join(", ")}`);
936
1047
  const progressContent = readProgress(config);
937
1048
  const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
938
1049
  logger.newline();
939
- logger.info("Starting Claude implementation session...");
940
- 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.`);
941
1052
  logger.dim("Review the changes before pushing.");
942
1053
  logger.newline();
943
1054
  const beforeSha = await getCurrentCommitSha();
@@ -947,15 +1058,17 @@ Labels: ${issue.labels.join(", ")}`);
947
1058
  };
948
1059
  process.on("SIGINT", handleSignal);
949
1060
  process.on("SIGTERM", handleSignal);
950
- let claudeExitCode;
1061
+ let aiExitCode;
1062
+ let usedProvider = provider;
951
1063
  try {
952
- const result = await invokeClaudeInteractive(prompt, config);
953
- claudeExitCode = result.exitCode;
1064
+ const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
1065
+ usedProvider = actualProvider;
1066
+ aiExitCode = result.exitCode ?? void 0;
954
1067
  } catch (error) {
955
1068
  if (error && typeof error === "object" && "exitCode" in error) {
956
- claudeExitCode = error.exitCode;
1069
+ aiExitCode = error.exitCode;
957
1070
  }
958
- logger.error(`Claude session failed: ${error}`);
1071
+ logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
959
1072
  } finally {
960
1073
  process.off("SIGINT", handleSignal);
961
1074
  process.off("SIGTERM", handleSignal);
@@ -966,8 +1079,9 @@ Labels: ${issue.labels.join(", ")}`);
966
1079
  logger.warning("Operation was cancelled. Labels unchanged.");
967
1080
  return;
968
1081
  }
1082
+ const usedProviderName = getProviderDisplayName(usedProvider);
969
1083
  if (commitsCreated) {
970
- logger.success("Claude session completed with new commits.");
1084
+ logger.success(`${usedProviderName} session completed with new commits.`);
971
1085
  try {
972
1086
  await updateIssueLabels(issueNumber, {
973
1087
  add: [workflowLabels.completed],
@@ -980,7 +1094,7 @@ Labels: ${issue.labels.join(", ")}`);
980
1094
  try {
981
1095
  await addIssueComment(
982
1096
  issueNumber,
983
- `AI implementation completed on branch \`${branchName}\`.
1097
+ `AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
984
1098
 
985
1099
  Please review the changes and create a PR when ready.`
986
1100
  );
@@ -989,9 +1103,9 @@ Please review the changes and create a PR when ready.`
989
1103
  logger.warning(`Failed to post comment: ${error}`);
990
1104
  }
991
1105
  } else {
992
- const isRateLimited = claudeExitCode === 2;
1106
+ const isRateLimited = aiExitCode === 2;
993
1107
  if (isRateLimited) {
994
- 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.`);
995
1109
  try {
996
1110
  await updateIssueLabels(issueNumber, {
997
1111
  add: [workflowLabels.blocked],
@@ -1004,7 +1118,7 @@ Please review the changes and create a PR when ready.`
1004
1118
  try {
1005
1119
  await addIssueComment(
1006
1120
  issueNumber,
1007
- `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}).
1008
1122
 
1009
1123
  No commits were created. Please retry later.`
1010
1124
  );
@@ -1013,7 +1127,7 @@ No commits were created. Please retry later.`
1013
1127
  logger.warning(`Failed to post comment: ${error}`);
1014
1128
  }
1015
1129
  } else {
1016
- 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.`);
1017
1131
  }
1018
1132
  return;
1019
1133
  }
@@ -1051,13 +1165,19 @@ import inquirer4 from "inquirer";
1051
1165
  async function prCommand(options) {
1052
1166
  logger.bold("Creating AI-enhanced pull request...");
1053
1167
  logger.newline();
1054
- 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
+ ]);
1055
1175
  if (!ghAuth) {
1056
1176
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1057
1177
  process.exit(1);
1058
1178
  }
1059
- if (!claudeOk) {
1060
- 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.`);
1061
1181
  process.exit(1);
1062
1182
  }
1063
1183
  if (await isOnMainBranch()) {
@@ -1114,12 +1234,13 @@ async function prCommand(options) {
1114
1234
  const prompt = buildPrPrompt(issue, commits, diffSummary);
1115
1235
  let prBody;
1116
1236
  try {
1117
- logger.info("Generating PR description with Claude...");
1237
+ logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
1118
1238
  logger.newline();
1119
- prBody = await invokeClaude({ prompt, streamOutput: true });
1239
+ const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
1240
+ prBody = result.output;
1120
1241
  logger.newline();
1121
1242
  } catch (error) {
1122
- logger.warning(`Claude invocation failed: ${error}`);
1243
+ logger.warning(`${providerName} invocation failed: ${error}`);
1123
1244
  prBody = generateFallbackBody(issue, commits);
1124
1245
  }
1125
1246
  const prTitle = issue?.title || commits[0] || currentBranch;
@@ -1199,6 +1320,14 @@ async function statusCommand() {
1199
1320
  logger.warning(` ${config.progress.file} not found`);
1200
1321
  }
1201
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();
1202
1331
  logger.bold("Prerequisites:");
1203
1332
  const ghAuth = await checkGhAuth();
1204
1333
  if (ghAuth) {
@@ -1207,10 +1336,22 @@ async function statusCommand() {
1207
1336
  logger.error(" GitHub CLI not authenticated");
1208
1337
  }
1209
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
+ };
1210
1346
  if (claudeOk) {
1211
- 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")}`);
1212
1353
  } else {
1213
- logger.error(" Claude CLI not found");
1354
+ logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
1214
1355
  }
1215
1356
  logger.newline();
1216
1357
  logger.bold("Git Status:");
@@ -1306,8 +1447,8 @@ program.command("init").description("Initialize gent workflow in current reposit
1306
1447
  program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
1307
1448
  await setupLabelsCommand();
1308
1449
  });
1309
- program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").action(async (description, options) => {
1310
- 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 });
1311
1452
  });
1312
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) => {
1313
1454
  await listCommand({
@@ -1316,11 +1457,11 @@ program.command("list").description("List GitHub issues by label/status").option
1316
1457
  limit: parseInt(options.limit, 10)
1317
1458
  });
1318
1459
  });
1319
- 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) => {
1320
- 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 });
1321
1462
  });
1322
- program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").action(async (options) => {
1323
- 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 });
1324
1465
  });
1325
1466
  program.command("status").description("Show current workflow status").action(async () => {
1326
1467
  await statusCommand();