commit-agent-cli 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +1 -3
  2. package/dist/index.js +241 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # commit-agent-cli
2
2
 
3
- > AI-powered git commit message generator using Claude Sonnet 4.5 / Opus 4.5 or Google Gemini and LangGraph
4
-
5
3
  Generate intelligent, context-aware commit messages by simply typing `commit`.
6
4
 
7
5
  ## Quick Start
@@ -27,7 +25,7 @@ You'll be prompted to:
27
25
 
28
26
  ## Features
29
27
 
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.
28
+ Powered by Claude and Gemini, this tool intelligently explores your codebase to generate clear, context-aware commit messages with full transparency. Enjoy customizable commit styles, secure local configuration, and seamless switching between AI providers.
31
29
 
32
30
  ## Documentation
33
31
 
package/dist/index.js CHANGED
@@ -100,7 +100,7 @@ var require_picocolors = __commonJS({
100
100
 
101
101
  // src/index.ts
102
102
  import "dotenv/config";
103
- import { intro, outro, text, spinner, isCancel, cancel, note, select } from "@clack/prompts";
103
+ import { intro, outro, text, spinner, isCancel, cancel, note, select, multiselect } from "@clack/prompts";
104
104
 
105
105
  // src/git.ts
106
106
  import { execa } from "execa";
@@ -154,6 +154,50 @@ async function isGitRepository() {
154
154
  return false;
155
155
  }
156
156
  }
157
+ async function getStagedFiles() {
158
+ const { stdout } = await execa("git", ["diff", "--cached", "--name-only"], {
159
+ reject: false
160
+ });
161
+ return stdout ? stdout.split("\n").filter(Boolean) : [];
162
+ }
163
+ async function getUnstagedFiles() {
164
+ const { stdout } = await execa("git", ["diff", "--name-only"], {
165
+ reject: false
166
+ });
167
+ return stdout ? stdout.split("\n").filter(Boolean) : [];
168
+ }
169
+ async function getUntrackedFiles() {
170
+ const { stdout } = await execa("git", ["ls-files", "--others", "--exclude-standard"], {
171
+ reject: false
172
+ });
173
+ return stdout ? stdout.split("\n").filter(Boolean) : [];
174
+ }
175
+ async function getRecentCommits(count = 5) {
176
+ const { stdout } = await execa("git", ["log", `-${count}`, "--pretty=format:%h|%s"], {
177
+ reject: false
178
+ });
179
+ if (!stdout) return [];
180
+ return stdout.split("\n").map((line) => {
181
+ const [hash, ...messageParts] = line.split("|");
182
+ return { hash, message: messageParts.join("|") };
183
+ });
184
+ }
185
+ async function getStagedStats() {
186
+ const { stdout: stats } = await execa("git", ["diff", "--cached", "--stat"], {
187
+ reject: false
188
+ });
189
+ if (!stats) return { files: 0, insertions: 0, deletions: 0 };
190
+ const match = stats.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
191
+ return {
192
+ files: match ? parseInt(match[1]) : 0,
193
+ insertions: match && match[2] ? parseInt(match[2]) : 0,
194
+ deletions: match && match[3] ? parseInt(match[3]) : 0
195
+ };
196
+ }
197
+ async function stageFiles(files) {
198
+ if (files.length === 0) return;
199
+ await execa("git", ["add", ...files]);
200
+ }
157
201
 
158
202
  // src/agent.ts
159
203
  import { ChatAnthropic } from "@langchain/anthropic";
@@ -445,6 +489,7 @@ import updateNotifier from "update-notifier";
445
489
  import { readFileSync } from "fs";
446
490
  import { fileURLToPath } from "url";
447
491
  import { dirname, join as joinPath } from "path";
492
+ import { execa as execa3 } from "execa";
448
493
  import { homedir } from "os";
449
494
  import { join as join2 } from "path";
450
495
  import { readFile as readFile2, writeFile } from "fs/promises";
@@ -455,32 +500,30 @@ var packageJson = JSON.parse(
455
500
  );
456
501
  var notifier = updateNotifier({
457
502
  pkg: packageJson,
458
- updateCheckInterval: 1e3 * 60 * 60
459
- // Check once per hour (was 24h)
503
+ updateCheckInterval: 0
504
+ // Always check for updates (no cache)
460
505
  });
461
506
  if (notifier.update && notifier.update.current !== notifier.update.latest) {
462
507
  const currentVersion = notifier.update.current;
463
508
  const latestVersion = notifier.update.latest;
464
- const boxWidth = 67;
465
- const padLine = (content, width) => {
466
- const visibleLength = content.replace(/\u001b\[[0-9;]*m/g, "").length;
467
- const padding = width - visibleLength;
468
- return content + " ".repeat(Math.max(0, padding));
509
+ const line1Text = `${packageJson.name} update available! ${currentVersion} \u2192 ${latestVersion}`;
510
+ const line2Text = `Run npm install -g ${packageJson.name}@latest to update.`;
511
+ const maxLength = Math.max(line1Text.length, line2Text.length);
512
+ const boxWidth = maxLength + 2;
513
+ const padLine = (text2, visibleLength) => {
514
+ const padding = boxWidth - visibleLength;
515
+ return text2 + " ".repeat(Math.max(0, padding));
469
516
  };
517
+ const line1Colored = ` ${packageJson.name} update available! ${import_picocolors.default.cyan(currentVersion)} \u2192 ${import_picocolors.default.green(import_picocolors.default.bold(latestVersion))}`;
518
+ const line2Colored = ` Run ${import_picocolors.default.cyan(import_picocolors.default.bold(`npm install -g ${packageJson.name}@latest`))} to update.`;
519
+ const horizontalBorder = "\u2500".repeat(boxWidth);
470
520
  console.log("");
471
- 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"));
472
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
473
- 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"));
474
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
475
- 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"));
476
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
477
- console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.dim("Update after you finish by running:"), boxWidth) + import_picocolors.default.yellow("\u2502"));
478
- 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"));
479
- console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
480
- 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"));
521
+ console.log(import_picocolors.default.yellow("\u256D" + horizontalBorder + "\u256E"));
522
+ console.log(import_picocolors.default.yellow("\u2502") + padLine(line1Colored, line1Text.length + 1) + import_picocolors.default.yellow("\u2502"));
523
+ console.log(import_picocolors.default.yellow("\u2502") + padLine(line2Colored, line2Text.length + 1) + import_picocolors.default.yellow("\u2502"));
524
+ console.log(import_picocolors.default.yellow("\u2570" + horizontalBorder + "\u256F"));
481
525
  console.log("");
482
526
  }
483
- notifier.notify({ defer: false, isGlobal: true });
484
527
  var CONFIG_PATH = join2(homedir(), ".commit-cli.json");
485
528
  var ANTHROPIC_MODELS = {
486
529
  "claude-sonnet-4-20250514": "Claude Sonnet 4.5",
@@ -580,6 +623,43 @@ async function promptForApiKey(provider) {
580
623
  return key;
581
624
  }
582
625
  }
626
+ async function showGuidedStaging() {
627
+ const s = spinner();
628
+ s.start("Checking for unstaged and untracked files...");
629
+ const [unstaged, untracked] = await Promise.all([
630
+ getUnstagedFiles(),
631
+ getUntrackedFiles()
632
+ ]);
633
+ s.stop("Files found.");
634
+ const allFiles = [
635
+ ...unstaged.map((f) => ({ file: f, status: "modified" })),
636
+ ...untracked.map((f) => ({ file: f, status: "untracked" }))
637
+ ];
638
+ if (allFiles.length === 0) {
639
+ note("No changes to stage. Make some changes first!", "No Changes");
640
+ return false;
641
+ }
642
+ note(`Found ${allFiles.length} file(s) with changes`, "Available Files");
643
+ const selectedFiles = await multiselect({
644
+ message: "Select files to stage (Space to select, Enter to continue):",
645
+ options: allFiles.map(({ file, status }) => ({
646
+ value: file,
647
+ label: `${file} (${status})`
648
+ })),
649
+ required: false
650
+ });
651
+ if (isCancel(selectedFiles)) {
652
+ return false;
653
+ }
654
+ if (!selectedFiles || selectedFiles.length === 0) {
655
+ note('No files selected. You can run "git add <files>" manually.', "Skipped");
656
+ return false;
657
+ }
658
+ s.start("Staging selected files...");
659
+ await stageFiles(selectedFiles);
660
+ s.stop(`Staged ${selectedFiles.length} file(s).`);
661
+ return true;
662
+ }
583
663
  async function showSettingsMenu(currentConfig2) {
584
664
  const providerLabel = currentConfig2.provider === "anthropic" ? "Anthropic (Claude)" : "Google (Gemini)";
585
665
  const modelName = currentConfig2.provider === "anthropic" ? ANTHROPIC_MODELS[currentConfig2.model] || currentConfig2.model : GOOGLE_MODELS[currentConfig2.model] || currentConfig2.model;
@@ -774,14 +854,116 @@ async function main() {
774
854
  note(`Preferences saved! You can change these anytime in the settings menu.`, "Setup Complete");
775
855
  }
776
856
  const s = spinner();
777
- s.start("Analyzing staged changes...");
778
- const diff = await getStagedDiffSmart();
857
+ s.start("Checking staged changes...");
858
+ let diff = await getStagedDiffSmart();
779
859
  if (!diff) {
780
860
  s.stop("No staged changes found.");
781
- cancel('Please stage your changes using "git add" first.');
782
- process.exit(0);
861
+ const shouldStage = await select({
862
+ message: "No staged changes. Would you like to stage files now?",
863
+ options: [
864
+ { value: true, label: "Yes, show me files to stage" },
865
+ { value: false, label: "No, I'll stage manually" }
866
+ ],
867
+ initialValue: true
868
+ });
869
+ if (isCancel(shouldStage) || !shouldStage) {
870
+ cancel('Please stage your changes using "git add" first.');
871
+ process.exit(0);
872
+ }
873
+ const staged = await showGuidedStaging();
874
+ if (!staged) {
875
+ cancel("No files were staged. Exiting.");
876
+ process.exit(0);
877
+ }
878
+ s.start("Analyzing staged changes...");
879
+ diff = await getStagedDiffSmart();
880
+ if (!diff) {
881
+ s.stop("Still no staged changes.");
882
+ cancel("Something went wrong. Please try again.");
883
+ process.exit(0);
884
+ }
783
885
  }
784
886
  s.stop("Changes detected.");
887
+ const stats = await getStagedStats();
888
+ const stagedFiles = await getStagedFiles();
889
+ let previewMessage = import_picocolors.default.cyan(`${stats.files} file(s) changed`);
890
+ if (stats.insertions > 0) previewMessage += import_picocolors.default.green(`, ${stats.insertions} insertion(s)`);
891
+ if (stats.deletions > 0) previewMessage += import_picocolors.default.red(`, ${stats.deletions} deletion(s)`);
892
+ previewMessage += "\n" + stagedFiles.map((f) => ` \u2022 ${f}`).join("\n");
893
+ note(previewMessage, "Changes Summary");
894
+ const recentCommits = await getRecentCommits(5);
895
+ if (recentCommits.length > 0) {
896
+ const commitsMessage = recentCommits.map((c) => ` ${import_picocolors.default.dim(c.hash)} ${c.message}`).join("\n");
897
+ note(commitsMessage, "Recent Commits");
898
+ }
899
+ const [unstaged, untracked] = await Promise.all([
900
+ getUnstagedFiles(),
901
+ getUntrackedFiles()
902
+ ]);
903
+ if (unstaged.length > 0 || untracked.length > 0) {
904
+ const unstagedCount = unstaged.length + untracked.length;
905
+ const warningMessage = `You have ${unstagedCount} unstaged file(s):
906
+ ` + [...unstaged.map((f) => ` \u2022 ${f} (modified)`), ...untracked.map((f) => ` \u2022 ${f} (untracked)`)].join("\n");
907
+ note(import_picocolors.default.yellow(warningMessage), import_picocolors.default.yellow("\u26A0 Unstaged Changes"));
908
+ const includeUnstaged = await select({
909
+ message: "Include these files in this commit?",
910
+ options: [
911
+ { value: false, label: "No, continue with current staging" },
912
+ { value: true, label: "Yes, let me select which ones" }
913
+ ],
914
+ initialValue: false
915
+ });
916
+ if (!isCancel(includeUnstaged) && includeUnstaged) {
917
+ const additionalFiles = await multiselect({
918
+ message: "Select additional files to stage:",
919
+ options: [
920
+ ...unstaged.map((f) => ({ value: f, label: `${f} (modified)` })),
921
+ ...untracked.map((f) => ({ value: f, label: `${f} (untracked)` }))
922
+ ],
923
+ required: false
924
+ });
925
+ if (!isCancel(additionalFiles) && additionalFiles && additionalFiles.length > 0) {
926
+ s.start("Staging additional files...");
927
+ await stageFiles(additionalFiles);
928
+ diff = await getStagedDiffSmart();
929
+ s.stop("Additional files staged.");
930
+ }
931
+ }
932
+ }
933
+ const totalChanges = stats.insertions + stats.deletions;
934
+ if (stats.files >= 10 || totalChanges >= 500) {
935
+ const largeChangesetMessage = import_picocolors.default.yellow(
936
+ `\u26A0 Large changeset detected (${stats.files} files, ${totalChanges} changes)
937
+ Consider splitting into smaller, focused commits for better review.`
938
+ );
939
+ note(largeChangesetMessage, import_picocolors.default.yellow("Large Changeset Warning"));
940
+ const proceedWithLarge = await select({
941
+ message: "How would you like to proceed?",
942
+ options: [
943
+ { value: "continue", label: "Continue with all changes" },
944
+ { value: "reselect", label: "Let me re-select files to commit" },
945
+ { value: "cancel", label: "Cancel and stage manually" }
946
+ ],
947
+ initialValue: "continue"
948
+ });
949
+ if (isCancel(proceedWithLarge) || proceedWithLarge === "cancel") {
950
+ cancel('Cancelled. Use "git add" to stage specific files.');
951
+ process.exit(0);
952
+ }
953
+ if (proceedWithLarge === "reselect") {
954
+ await execa3("git", ["reset", "HEAD"]);
955
+ const restaged = await showGuidedStaging();
956
+ if (!restaged) {
957
+ cancel("No files were staged. Exiting.");
958
+ process.exit(0);
959
+ }
960
+ diff = await getStagedDiffSmart();
961
+ if (!diff) {
962
+ cancel("No staged changes. Exiting.");
963
+ process.exit(0);
964
+ }
965
+ }
966
+ }
785
967
  let commitMessage = "";
786
968
  let confirmed = false;
787
969
  let userFeedback = void 0;
@@ -856,13 +1038,16 @@ async function main() {
856
1038
  }
857
1039
  const formattedMessage = wrappedLines.map((line) => import_picocolors.default.cyan(line)).join("\n");
858
1040
  note(formattedMessage, import_picocolors.default.bold("Proposed Commit Message"));
1041
+ const isMultiLine = commitMessage.includes("\n");
1042
+ const actionOptions = [
1043
+ { value: "commit", label: "Yes, commit" },
1044
+ ...isMultiLine ? [] : [{ value: "edit", label: "Edit message" }],
1045
+ { value: "regenerate", label: "Regenerate" },
1046
+ { value: "settings", label: "Change settings" }
1047
+ ];
859
1048
  const action = await select({
860
1049
  message: "Do you want to use this message?",
861
- options: [
862
- { value: "commit", label: "Yes, commit" },
863
- { value: "regenerate", label: "No, regenerate" },
864
- { value: "settings", label: "Change settings" }
865
- ]
1050
+ options: actionOptions
866
1051
  });
867
1052
  if (isCancel(action)) {
868
1053
  cancel("Operation cancelled.");
@@ -870,6 +1055,34 @@ async function main() {
870
1055
  }
871
1056
  if (action === "commit") {
872
1057
  confirmed = true;
1058
+ } else if (action === "edit") {
1059
+ const editedMessage = await text({
1060
+ message: "Edit your commit message:",
1061
+ initialValue: commitMessage,
1062
+ validate: (value) => {
1063
+ if (!value || value.trim() === "") return "Commit message cannot be empty";
1064
+ }
1065
+ });
1066
+ if (isCancel(editedMessage)) {
1067
+ continue;
1068
+ }
1069
+ commitMessage = editedMessage;
1070
+ const editedFormattedMessage = editedMessage.split("\n").map((line) => import_picocolors.default.cyan(line)).join("\n");
1071
+ note(editedFormattedMessage, import_picocolors.default.bold("Edited Commit Message"));
1072
+ const confirmEdited = await select({
1073
+ message: "Use this edited message?",
1074
+ options: [
1075
+ { value: true, label: "Yes, commit" },
1076
+ { value: false, label: "No, go back" }
1077
+ ],
1078
+ initialValue: true
1079
+ });
1080
+ if (isCancel(confirmEdited)) {
1081
+ continue;
1082
+ }
1083
+ if (confirmEdited) {
1084
+ confirmed = true;
1085
+ }
873
1086
  } else if (action === "settings") {
874
1087
  const settingsResult = await showSettingsMenu(config);
875
1088
  if (settingsResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-agent-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
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",