commit-agent-cli 0.1.7 → 0.2.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024
3
+ Copyright (c) 2026 Sung Oh
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # commit-agent-cli
2
2
 
3
- > AI-powered git commit message generator using Claude Sonnet 4.5 and LangGraph
3
+ > AI-powered git commit message generator using Claude Sonnet 4.5 / Opus 4.5 or Google Gemini and LangGraph
4
4
 
5
5
  Generate intelligent, context-aware commit messages by simply typing `commit`.
6
6
 
@@ -20,12 +20,14 @@ commit
20
20
  ## First-Time Setup
21
21
 
22
22
  You'll be prompted to:
23
- 1. Enter your [Anthropic API Key](https://console.anthropic.com)
24
- 2. Choose commit message preferences (conventional commits, verbosity)
23
+ 1. Select your AI Provider (Anthropic Claude or Google Gemini)
24
+ 2. Choose your preferred model
25
+ 3. Enter your API Key ([Anthropic Console](https://console.anthropic.com) or [Google AI Studio](https://aistudio.google.com/app/apikey))
26
+ 4. Choose commit message preferences (conventional commits, verbosity)
25
27
 
26
28
  ## Features
27
29
 
28
- Powered by Claude Sonnet 4.5, this tool autonomously explores your codebase to generate intelligent commit messages with full transparency into its reasoning process. Supports customizable commit styles with secure local configuration storage.
30
+ Powered by Claude Sonnet 4.5 / Opus 4.5 or Google Gemini, this tool autonomously explores your codebase to generate intelligent commit messages with full transparency into its reasoning process. Supports customizable commit styles with secure local configuration storage and the ability to switch between AI providers at any time.
29
31
 
30
32
  ## Documentation
31
33
 
@@ -34,7 +36,7 @@ Powered by Claude Sonnet 4.5, this tool autonomously explores your codebase to g
34
36
  ## Requirements
35
37
 
36
38
  - Node.js 18+
37
- - Anthropic API key
39
+ - Anthropic API key OR Google AI API key
38
40
 
39
41
  ## License
40
42
 
package/dist/index.js CHANGED
@@ -157,6 +157,7 @@ async function isGitRepository() {
157
157
 
158
158
  // src/agent.ts
159
159
  import { ChatAnthropic } from "@langchain/anthropic";
160
+ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
160
161
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
161
162
  import { StateGraph, MessagesAnnotation } from "@langchain/langgraph";
162
163
  import { ToolNode } from "@langchain/langgraph/prebuilt";
@@ -323,15 +324,29 @@ var tools = [
323
324
 
324
325
  // src/agent.ts
325
326
  var app = null;
326
- function getApp() {
327
+ var currentConfig = null;
328
+ function getApp(config) {
329
+ if (app && currentConfig && (currentConfig.provider !== config.provider || currentConfig.model !== config.model)) {
330
+ app = null;
331
+ }
327
332
  if (app) return app;
328
- const model = new ChatAnthropic({
329
- model: "claude-sonnet-4-5-20250929",
330
- temperature: 0,
331
- // It will automatically look for ANTHROPIC_API_KEY in process.env if not provided,
332
- // but explicit assignment ensures it picks up dynamic changes if any.
333
- apiKey: process.env.ANTHROPIC_API_KEY
334
- }).bindTools(tools);
333
+ currentConfig = config;
334
+ let model;
335
+ if (config.provider === "anthropic") {
336
+ model = new ChatAnthropic({
337
+ model: config.model,
338
+ temperature: 0,
339
+ apiKey: process.env.ANTHROPIC_API_KEY
340
+ }).bindTools(tools);
341
+ } else {
342
+ model = new ChatGoogleGenerativeAI({
343
+ model: config.model,
344
+ temperature: 0,
345
+ apiKey: process.env.GOOGLE_API_KEY,
346
+ thinkingBudget: 0
347
+ // Disable thinking to prevent quota issues
348
+ }).bindTools(tools);
349
+ }
335
350
  const toolNode = new ToolNode(tools);
336
351
  async function agent(state) {
337
352
  const { messages } = state;
@@ -349,9 +364,13 @@ function getApp() {
349
364
  app = workflow.compile();
350
365
  return app;
351
366
  }
352
- async function generateCommitMessage(diff, preferences, userFeedback) {
353
- const conventionalGuide = preferences.useConventionalCommits ? "Use conventional commit format with prefixes like feat:, fix:, chore:, docs:, style:, refactor:, test:, etc." : "Do NOT use conventional commit prefixes. Write natural commit messages.";
367
+ async function generateCommitMessage(diff, preferences, config, userFeedback) {
368
+ const conventionalGuide = preferences.useConventionalCommits ? "Use conventional commit format with prefixes like feat:, fix:, chore:, docs:, style:, refactor:, test:, etc." : "Do NOT use conventional commit prefixes (no feat:, fix:, etc.). Write natural, plain commit messages without any prefixes.";
354
369
  const styleGuide = preferences.commitMessageStyle === "descriptive" ? 'Be descriptive and detailed. Explain the "why" behind changes when relevant. Multi-line messages are encouraged.' : "Be concise and to the point. Keep it short, ideally one line.";
370
+ const customGuideSection = preferences.customGuideline ? `
371
+
372
+ CUSTOM GUIDELINES (MUST FOLLOW):
373
+ ${preferences.customGuideline}` : "";
355
374
  const systemPrompt = `You are an expert developer. Your task is to generate a commit message for the provided git diff.
356
375
 
357
376
  The diff provided is OPTIMIZED for token efficiency - it includes file changes, stats, and minimal context.
@@ -376,9 +395,10 @@ Commit Message Rules:
376
395
  3. Focus on WHAT changed (clear from diff) and WHY if obvious from context
377
396
  4. OUTPUT FORMAT: Your response must be ONLY the commit message. No explanations.
378
397
  5. Do NOT use markdown code blocks or formatting
379
- 6. If multi-line, use proper git commit format (subject line, blank line, body)
398
+ 6. If multi-line, use proper git commit format (subject line, blank line, body)${customGuideSection}
380
399
 
381
400
  CRITICAL: Your ENTIRE response should be the commit message itself, nothing else.
401
+ IMPORTANT: Follow the conventional commit rule strictly - if told NOT to use prefixes, do NOT use them at all.
382
402
 
383
403
  DEFAULT ACTION: Read the diff, generate the message, done. NO TOOLS.
384
404
  `;
@@ -391,7 +411,7 @@ ${diff}${userFeedback ? `
391
411
  User feedback on previous attempt: ${userFeedback}
392
412
  Please adjust the commit message based on this feedback.` : ""}`)
393
413
  ];
394
- const graph = getApp();
414
+ const graph = getApp(config);
395
415
  const result = await graph.invoke({ messages });
396
416
  const lastMsg = result.messages[result.messages.length - 1];
397
417
  let content = lastMsg.content;
@@ -435,100 +455,265 @@ var packageJson = JSON.parse(
435
455
  );
436
456
  var notifier = updateNotifier({
437
457
  pkg: packageJson,
438
- updateCheckInterval: 1e3 * 60 * 60
439
- // Check once per hour (was 24h)
458
+ updateCheckInterval: 0
459
+ // Always check for updates (no cache)
440
460
  });
441
461
  if (notifier.update && notifier.update.current !== notifier.update.latest) {
442
462
  const currentVersion = notifier.update.current;
443
463
  const latestVersion = notifier.update.latest;
444
- const boxWidth = 67;
445
- const padLine = (content, width) => {
446
- const visibleLength = content.replace(/\u001b\[[0-9;]*m/g, "").length;
447
- const padding = width - visibleLength;
448
- return content + " ".repeat(Math.max(0, padding));
464
+ const line1Text = `${packageJson.name} update available! ${currentVersion} \u2192 ${latestVersion}`;
465
+ const line2Text = `Run npm install -g ${packageJson.name}@latest to update.`;
466
+ const maxLength = Math.max(line1Text.length, line2Text.length);
467
+ const boxWidth = maxLength + 2;
468
+ const padLine = (text2, visibleLength) => {
469
+ const padding = boxWidth - visibleLength;
470
+ return text2 + " ".repeat(Math.max(0, padding));
449
471
  };
472
+ const line1Colored = ` ${packageJson.name} update available! ${import_picocolors.default.cyan(currentVersion)} \u2192 ${import_picocolors.default.green(import_picocolors.default.bold(latestVersion))}`;
473
+ const line2Colored = ` Run ${import_picocolors.default.cyan(import_picocolors.default.bold(`npm install -g ${packageJson.name}@latest`))} to update.`;
474
+ const horizontalBorder = "\u2500".repeat(boxWidth);
450
475
  console.log("");
451
- console.log(import_picocolors.default.yellow("\u256D\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
452
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
453
- console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.bold("New version of commit-cli is available!"), boxWidth) + import_picocolors.default.yellow("\u2502"));
454
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
455
- console.log(import_picocolors.default.yellow("\u2502") + padLine(" Current: " + import_picocolors.default.dim(currentVersion) + " \u2192 Latest: " + import_picocolors.default.green(import_picocolors.default.bold(latestVersion)), boxWidth) + import_picocolors.default.yellow("\u2502"));
456
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
457
- console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.dim("Update after you finish by running:"), boxWidth) + import_picocolors.default.yellow("\u2502"));
458
- console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.cyan(import_picocolors.default.bold(`npm install -g ${packageJson.name}@latest`)), boxWidth) + import_picocolors.default.yellow("\u2502"));
459
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
460
- console.log(import_picocolors.default.yellow("\u2570\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
476
+ console.log(import_picocolors.default.yellow("\u256D" + horizontalBorder + "\u256E"));
477
+ console.log(import_picocolors.default.yellow("\u2502") + padLine(line1Colored, line1Text.length + 1) + import_picocolors.default.yellow("\u2502"));
478
+ console.log(import_picocolors.default.yellow("\u2502") + padLine(line2Colored, line2Text.length + 1) + import_picocolors.default.yellow("\u2502"));
479
+ console.log(import_picocolors.default.yellow("\u2570" + horizontalBorder + "\u256F"));
461
480
  console.log("");
462
481
  }
463
- notifier.notify({ defer: false, isGlobal: true });
464
482
  var CONFIG_PATH = join2(homedir(), ".commit-cli.json");
465
- async function getStoredKey() {
483
+ var ANTHROPIC_MODELS = {
484
+ "claude-sonnet-4-20250514": "Claude Sonnet 4.5",
485
+ "claude-opus-4-20250514": "Claude Opus 4.5"
486
+ };
487
+ var GOOGLE_MODELS = {
488
+ "gemini-3-flash-preview": "Gemini 3.0 Flash Preview",
489
+ "gemini-3-pro-preview": "Gemini 3.0 Pro Preview"
490
+ };
491
+ async function getStoredConfig() {
466
492
  try {
467
493
  const data = await readFile2(CONFIG_PATH, "utf-8");
468
- return JSON.parse(data).ANTHROPIC_API_KEY;
494
+ return JSON.parse(data);
469
495
  } catch {
470
496
  return null;
471
497
  }
472
498
  }
473
- async function storeKey(key) {
499
+ async function storeConfig(config) {
474
500
  try {
475
- await writeFile(CONFIG_PATH, JSON.stringify({ ANTHROPIC_API_KEY: key }), { mode: 384 });
501
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
476
502
  } catch (err) {
477
503
  }
478
504
  }
479
- async function getStoredPreferences() {
480
- try {
481
- const data = await readFile2(CONFIG_PATH, "utf-8");
482
- const config = JSON.parse(data);
483
- return config.preferences || null;
484
- } catch {
485
- return null;
505
+ async function setupProviderAndModel() {
506
+ const providerChoice = await select({
507
+ message: "Select AI Provider:",
508
+ options: [
509
+ { value: "anthropic", label: "Anthropic (Claude)" },
510
+ { value: "google", label: "Google (Gemini)" }
511
+ ],
512
+ initialValue: "anthropic"
513
+ });
514
+ if (isCancel(providerChoice)) {
515
+ cancel("Operation cancelled.");
516
+ process.exit(0);
486
517
  }
487
- }
488
- async function storePreferences(prefs) {
489
- try {
490
- let config = {};
491
- try {
492
- const data = await readFile2(CONFIG_PATH, "utf-8");
493
- config = JSON.parse(data);
494
- } catch {
518
+ const provider = providerChoice;
519
+ let model;
520
+ if (provider === "anthropic") {
521
+ const modelChoice = await select({
522
+ message: "Select Anthropic Model:",
523
+ options: [
524
+ { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4.5 (Recommended)" },
525
+ { value: "claude-opus-4-20250514", label: "Claude Opus 4.5 (Most Capable)" }
526
+ ],
527
+ initialValue: "claude-sonnet-4-20250514"
528
+ });
529
+ if (isCancel(modelChoice)) {
530
+ cancel("Operation cancelled.");
531
+ process.exit(0);
495
532
  }
496
- config.preferences = prefs;
497
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
498
- } catch (err) {
533
+ model = modelChoice;
534
+ } else {
535
+ const modelChoice = await select({
536
+ message: "Select Google Model:",
537
+ options: [
538
+ { value: "gemini-3-flash-preview", label: "Gemini 3.0 Flash Preview (Fast)" },
539
+ { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro Preview (Most Capable)" }
540
+ ],
541
+ initialValue: "gemini-3-flash-preview"
542
+ });
543
+ if (isCancel(modelChoice)) {
544
+ cancel("Operation cancelled.");
545
+ process.exit(0);
546
+ }
547
+ model = modelChoice;
499
548
  }
549
+ return { provider, model };
500
550
  }
501
- async function main() {
502
- intro(import_picocolors.default.bgBlue(import_picocolors.default.white(" commit-cli ")));
503
- let apiKey = process.env.ANTHROPIC_API_KEY;
504
- if (!apiKey) {
505
- apiKey = await getStoredKey() || void 0;
506
- }
507
- if (!apiKey) {
551
+ async function promptForApiKey(provider) {
552
+ if (provider === "anthropic") {
508
553
  const key = await text({
509
- message: "Enter your Anthropic API Key (sk-...):",
554
+ message: "Enter your Anthropic API Key (sk-ant-...):",
510
555
  placeholder: "sk-ant-api...",
511
556
  validate: (value) => {
512
557
  if (!value) return "API Key is required";
513
- if (!value.startsWith("sk-")) return "Invalid API Key format (should start with sk-)";
558
+ if (!value.startsWith("sk-ant-")) return "Invalid API Key format (should start with sk-ant-)";
514
559
  }
515
560
  });
516
561
  if (isCancel(key)) {
517
562
  cancel("Operation cancelled.");
518
563
  process.exit(0);
519
564
  }
520
- apiKey = key;
521
- await storeKey(apiKey);
565
+ return key;
566
+ } else {
567
+ const key = await text({
568
+ message: "Enter your Google AI API Key:",
569
+ placeholder: "AIza...",
570
+ validate: (value) => {
571
+ if (!value) return "API Key is required";
572
+ }
573
+ });
574
+ if (isCancel(key)) {
575
+ cancel("Operation cancelled.");
576
+ process.exit(0);
577
+ }
578
+ return key;
579
+ }
580
+ }
581
+ async function showSettingsMenu(currentConfig2) {
582
+ const providerLabel = currentConfig2.provider === "anthropic" ? "Anthropic (Claude)" : "Google (Gemini)";
583
+ const modelName = currentConfig2.provider === "anthropic" ? ANTHROPIC_MODELS[currentConfig2.model] || currentConfig2.model : GOOGLE_MODELS[currentConfig2.model] || currentConfig2.model;
584
+ note(`Current: ${providerLabel} - ${modelName}`, "Current Configuration");
585
+ const settingChoice = await select({
586
+ message: "What would you like to change?",
587
+ options: [
588
+ { value: "provider", label: "Change AI Provider & Model" },
589
+ { value: "apikey", label: "Update API Key" },
590
+ { value: "preferences", label: "Change Commit Preferences" },
591
+ { value: "cancel", label: "Cancel" }
592
+ ]
593
+ });
594
+ if (isCancel(settingChoice) || settingChoice === "cancel") {
595
+ return null;
596
+ }
597
+ const newConfig = { ...currentConfig2 };
598
+ if (settingChoice === "provider") {
599
+ const modelConfig = await setupProviderAndModel();
600
+ newConfig.provider = modelConfig.provider;
601
+ newConfig.model = modelConfig.model;
602
+ const keyField = modelConfig.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
603
+ if (!newConfig[keyField]) {
604
+ const apiKey = await promptForApiKey(modelConfig.provider);
605
+ newConfig[keyField] = apiKey;
606
+ }
607
+ await storeConfig(newConfig);
608
+ return newConfig;
609
+ } else if (settingChoice === "apikey") {
610
+ const apiKey = await promptForApiKey(currentConfig2.provider);
611
+ const keyField = currentConfig2.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
612
+ newConfig[keyField] = apiKey;
613
+ await storeConfig(newConfig);
614
+ return newConfig;
615
+ } else if (settingChoice === "preferences") {
616
+ const useConventional = await select({
617
+ message: "Use conventional commit prefixes (feat:, fix:, chore:, etc.)?",
618
+ options: [
619
+ { value: true, label: "Yes" },
620
+ { value: false, label: "No" }
621
+ ],
622
+ initialValue: currentConfig2.preferences?.useConventionalCommits ?? true
623
+ });
624
+ if (isCancel(useConventional)) {
625
+ return null;
626
+ }
627
+ const styleChoice = await select({
628
+ message: "Prefer descriptive commit messages?",
629
+ options: [
630
+ { value: true, label: "Descriptive (detailed explanations)" },
631
+ { value: false, label: "Concise (short and to the point)" }
632
+ ],
633
+ initialValue: currentConfig2.preferences?.commitMessageStyle === "descriptive"
634
+ });
635
+ if (isCancel(styleChoice)) {
636
+ return null;
637
+ }
638
+ const addCustomGuideline = await select({
639
+ message: "Add custom commit message guideline?",
640
+ options: [
641
+ { value: true, label: "Yes, add custom guideline" },
642
+ { value: false, label: "No, use defaults" }
643
+ ],
644
+ initialValue: false
645
+ });
646
+ if (isCancel(addCustomGuideline)) {
647
+ return null;
648
+ }
649
+ let customGuideline = currentConfig2.preferences?.customGuideline;
650
+ if (addCustomGuideline) {
651
+ const guidelineInput = await text({
652
+ message: "Enter your custom commit message guideline:",
653
+ placeholder: "e.g., Always mention ticket number, use imperative mood...",
654
+ initialValue: currentConfig2.preferences?.customGuideline || ""
655
+ });
656
+ if (isCancel(guidelineInput)) {
657
+ return null;
658
+ }
659
+ customGuideline = guidelineInput.trim() || void 0;
660
+ } else {
661
+ customGuideline = void 0;
662
+ }
663
+ newConfig.preferences = {
664
+ useConventionalCommits: useConventional,
665
+ commitMessageStyle: styleChoice ? "descriptive" : "concise",
666
+ customGuideline
667
+ };
668
+ await storeConfig(newConfig);
669
+ return newConfig;
670
+ }
671
+ return null;
672
+ }
673
+ async function main() {
674
+ intro(import_picocolors.default.bgBlue(import_picocolors.default.white(" commit-cli ")));
675
+ let config = await getStoredConfig();
676
+ if (config && config.ANTHROPIC_API_KEY && !config.provider) {
677
+ note("Upgrading your configuration to support multiple AI providers...", "Configuration Update");
678
+ config.provider = "anthropic";
679
+ config.model = "claude-sonnet-4-20250514";
680
+ await storeConfig(config);
681
+ note("Configuration upgraded! You can now switch providers anytime using the settings menu.", "Upgrade Complete");
682
+ }
683
+ if (!config || !config.provider || !config.model) {
684
+ note("Welcome! Let's set up your AI provider and preferences", "First Time Setup");
685
+ const modelConfig = await setupProviderAndModel();
686
+ const apiKey2 = await promptForApiKey(modelConfig.provider);
687
+ config = {
688
+ provider: modelConfig.provider,
689
+ model: modelConfig.model,
690
+ ...modelConfig.provider === "anthropic" ? { ANTHROPIC_API_KEY: apiKey2 } : { GOOGLE_API_KEY: apiKey2 }
691
+ };
692
+ await storeConfig(config);
693
+ }
694
+ const apiKeyField = config.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
695
+ const envKeyField = config.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
696
+ let apiKey = process.env[envKeyField];
697
+ if (!apiKey) {
698
+ apiKey = config[apiKeyField] || void 0;
699
+ }
700
+ if (!apiKey) {
701
+ apiKey = await promptForApiKey(config.provider);
702
+ config[apiKeyField] = apiKey;
703
+ await storeConfig(config);
704
+ }
705
+ if (config.provider === "anthropic") {
706
+ process.env.ANTHROPIC_API_KEY = apiKey;
707
+ } else {
708
+ process.env.GOOGLE_API_KEY = apiKey;
522
709
  }
523
- process.env.ANTHROPIC_API_KEY = apiKey;
524
710
  const isRepo = await isGitRepository();
525
711
  if (!isRepo) {
526
712
  cancel("Current directory is not a git repository.");
527
713
  process.exit(1);
528
714
  }
529
- let preferences = await getStoredPreferences();
715
+ let preferences = config.preferences;
530
716
  if (!preferences) {
531
- note("Let's set up your commit message preferences (one-time setup)", "First Time Setup");
532
717
  const useConventional = await select({
533
718
  message: "Use conventional commit prefixes (feat:, fix:, chore:, etc.)?",
534
719
  options: [
@@ -553,12 +738,38 @@ async function main() {
553
738
  cancel("Operation cancelled.");
554
739
  process.exit(0);
555
740
  }
741
+ const addCustomGuideline = await select({
742
+ message: "Add custom commit message guideline? (optional)",
743
+ options: [
744
+ { value: false, label: "No, use defaults" },
745
+ { value: true, label: "Yes, add custom guideline" }
746
+ ],
747
+ initialValue: false
748
+ });
749
+ if (isCancel(addCustomGuideline)) {
750
+ cancel("Operation cancelled.");
751
+ process.exit(0);
752
+ }
753
+ let customGuideline;
754
+ if (addCustomGuideline) {
755
+ const guidelineInput = await text({
756
+ message: "Enter your custom commit message guideline:",
757
+ placeholder: "e.g., Always mention ticket number, use imperative mood..."
758
+ });
759
+ if (isCancel(guidelineInput)) {
760
+ cancel("Operation cancelled.");
761
+ process.exit(0);
762
+ }
763
+ customGuideline = guidelineInput.trim() || void 0;
764
+ }
556
765
  preferences = {
557
766
  useConventionalCommits: useConventional,
558
- commitMessageStyle: styleChoice ? "descriptive" : "concise"
767
+ commitMessageStyle: styleChoice ? "descriptive" : "concise",
768
+ customGuideline
559
769
  };
560
- await storePreferences(preferences);
561
- note(`Preferences saved! You can change these by editing ${CONFIG_PATH}`, "Setup Complete");
770
+ config.preferences = preferences;
771
+ await storeConfig(config);
772
+ note(`Preferences saved! You can change these anytime in the settings menu.`, "Setup Complete");
562
773
  }
563
774
  const s = spinner();
564
775
  s.start("Analyzing staged changes...");
@@ -575,11 +786,11 @@ async function main() {
575
786
  while (!confirmed) {
576
787
  s.start("Generating commit message (Agent is exploring)...");
577
788
  try {
578
- commitMessage = await generateCommitMessage(diff, preferences, userFeedback);
789
+ commitMessage = await generateCommitMessage(diff, preferences, config, userFeedback);
579
790
  userFeedback = void 0;
580
791
  } catch (error) {
581
792
  s.stop("Generation failed.");
582
- if (error.message?.includes("401") || error.message?.includes("authentication_error") || error.message?.includes("invalid x-api-key") || error.message?.includes("invalid api key")) {
793
+ if (error.message?.includes("401") || error.message?.includes("authentication_error") || error.message?.includes("invalid x-api-key") || error.message?.includes("invalid api key") || error.message?.includes("API key not valid")) {
583
794
  cancel("Invalid API Key detected.");
584
795
  const retryWithNewKey = await select({
585
796
  message: "Would you like to enter a new API key?",
@@ -593,21 +804,16 @@ async function main() {
593
804
  cancel("Operation cancelled.");
594
805
  process.exit(0);
595
806
  }
596
- const newKey = await text({
597
- message: "Enter your Anthropic API Key (sk-...):",
598
- placeholder: "sk-ant-api...",
599
- validate: (value) => {
600
- if (!value) return "API Key is required";
601
- if (!value.startsWith("sk-")) return "Invalid API Key format (should start with sk-)";
602
- }
603
- });
604
- if (isCancel(newKey)) {
605
- cancel("Operation cancelled.");
606
- process.exit(0);
607
- }
807
+ const newKey = await promptForApiKey(config.provider);
608
808
  apiKey = newKey;
609
- process.env.ANTHROPIC_API_KEY = apiKey;
610
- await storeKey(apiKey);
809
+ const keyField = config.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
810
+ config[keyField] = apiKey;
811
+ await storeConfig(config);
812
+ if (config.provider === "anthropic") {
813
+ process.env.ANTHROPIC_API_KEY = apiKey;
814
+ } else {
815
+ process.env.GOOGLE_API_KEY = apiKey;
816
+ }
611
817
  note("API Key updated. Retrying...", "Key Updated");
612
818
  continue;
613
819
  }
@@ -651,16 +857,31 @@ async function main() {
651
857
  const action = await select({
652
858
  message: "Do you want to use this message?",
653
859
  options: [
654
- { value: true, label: "Yes, commit" },
655
- { value: false, label: "No, regenerate or cancel" }
860
+ { value: "commit", label: "Yes, commit" },
861
+ { value: "regenerate", label: "No, regenerate" },
862
+ { value: "settings", label: "Change settings" }
656
863
  ]
657
864
  });
658
865
  if (isCancel(action)) {
659
866
  cancel("Operation cancelled.");
660
867
  process.exit(0);
661
868
  }
662
- if (action) {
869
+ if (action === "commit") {
663
870
  confirmed = true;
871
+ } else if (action === "settings") {
872
+ const settingsResult = await showSettingsMenu(config);
873
+ if (settingsResult) {
874
+ config = settingsResult;
875
+ preferences = config.preferences || preferences;
876
+ if (config.provider === "anthropic") {
877
+ process.env.ANTHROPIC_API_KEY = config.ANTHROPIC_API_KEY || "";
878
+ delete process.env.GOOGLE_API_KEY;
879
+ } else {
880
+ process.env.GOOGLE_API_KEY = config.GOOGLE_API_KEY || "";
881
+ delete process.env.ANTHROPIC_API_KEY;
882
+ }
883
+ note("Settings updated! Regenerating with new configuration...", "Updated");
884
+ }
664
885
  } else {
665
886
  const nextStep = await select({
666
887
  message: "Try again?",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "commit-agent-cli",
3
- "version": "0.1.7",
4
- "description": "AI-powered git commit CLI using LangGraph and Claude 4.5",
3
+ "version": "0.2.1",
4
+ "description": "AI-powered git commit CLI using LangGraph and Claude 4.5 or Gemini",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "type": "module",
@@ -38,6 +38,7 @@
38
38
  "@clack/prompts": "^0.7.0",
39
39
  "@langchain/anthropic": "^0.3.0",
40
40
  "@langchain/core": "^0.3.0",
41
+ "@langchain/google-genai": "^0.1.2",
41
42
  "@langchain/langgraph": "^0.2.0",
42
43
  "ai": "^3.3.33",
43
44
  "commit-agent-cli": "^0.1.0",