feat: git commands (#41)

* feat: add /commit-and-push for AI-generated commits and pushes

* fix: handle missing upstream branch in git push

* feat: add git commit-and-push subcommand with AI assistance

* feat: automate staging changes with git add in commit workflow

* chore: bump version to 0.0.12

* revert package json

* chore(release): bump version to 0.0.12
This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2025-07-25 00:45:30 +02:00
1 parent c00feda728
commit 9642bdaeef
4 files changed
+349 -5

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@vibe-kit/grok-cli",
"version": "0.0.10",
"version": "0.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@vibe-kit/grok-cli",
"version": "0.0.10",
"version": "0.0.12",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vibe-kit/grok-cli",
"version": "0.0.11",
"version": "0.0.12",
"description": "An open-source AI agent that brings the power of Grok directly into your terminal.",
"main": "dist/index.js",
"bin": {
+187
View File
@@ -57,6 +57,7 @@ export function useInputHandler({
{ command: "/help", description: "Show help information" },
{ command: "/clear", description: "Clear chat history" },
{ command: "/models", description: "Switch Grok Model" },
{ command: "/commit-and-push", description: "AI commit & push to remote" },
{ command: "/exit", description: "Exit the application" },
];
@@ -104,6 +105,9 @@ Built-in Commands:
/exit - Exit application
exit, quit - Exit application
Git Commands:
/commit-and-push - AI-generated commit + push to remote
Keyboard Shortcuts:
Shift+Tab - Toggle auto-edit mode (bypass confirmations)
@@ -162,6 +166,189 @@ Available models: ${modelNames.join(", ")}`,
return true;
}
if (trimmedInput === "/commit-and-push") {
const userEntry: ChatEntry = {
type: "user",
content: "/commit-and-push",
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, userEntry]);
setIsProcessing(true);
setIsStreaming(true);
try {
// First check if there are any changes at all
const initialStatusResult = await agent.executeBashCommand("git status --porcelain");
if (!initialStatusResult.success || !initialStatusResult.output?.trim()) {
const noChangesEntry: ChatEntry = {
type: "assistant",
content: "No changes to commit. Working directory is clean.",
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, noChangesEntry]);
setIsProcessing(false);
setIsStreaming(false);
setInput("");
return true;
}
// Add all changes
const addResult = await agent.executeBashCommand("git add .");
if (!addResult.success) {
const addErrorEntry: ChatEntry = {
type: "assistant",
content: `Failed to stage changes: ${addResult.error || 'Unknown error'}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, addErrorEntry]);
setIsProcessing(false);
setIsStreaming(false);
setInput("");
return true;
}
// Show that changes were staged
const addEntry: ChatEntry = {
type: "tool_result",
content: "Changes staged successfully",
timestamp: new Date(),
toolCall: {
id: `git_add_${Date.now()}`,
type: "function",
function: {
name: "bash",
arguments: JSON.stringify({ command: "git add ." }),
},
},
toolResult: addResult,
};
setChatHistory((prev) => [...prev, addEntry]);
// Get staged changes for commit message generation
const diffResult = await agent.executeBashCommand("git diff --cached");
// Generate commit message using AI
const commitPrompt = `Generate a concise, professional git commit message for these changes:
Git Status:
${initialStatusResult.output}
Git Diff (staged changes):
${diffResult.output || "No staged changes shown"}
Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters.
Respond with ONLY the commit message, no additional text.`;
let commitMessage = "";
let streamingEntry: ChatEntry | null = null;
for await (const chunk of agent.processUserMessageStream(commitPrompt)) {
if (chunk.type === "content" && chunk.content) {
if (!streamingEntry) {
const newEntry = {
type: "assistant" as const,
content: `Generating commit message...\n\n${chunk.content}`,
timestamp: new Date(),
isStreaming: true,
};
setChatHistory((prev) => [...prev, newEntry]);
streamingEntry = newEntry;
commitMessage = chunk.content;
} else {
commitMessage += chunk.content;
setChatHistory((prev) =>
prev.map((entry, idx) =>
idx === prev.length - 1 && entry.isStreaming
? { ...entry, content: `Generating commit message...\n\n${commitMessage}` }
: entry
)
);
}
} else if (chunk.type === "done") {
if (streamingEntry) {
setChatHistory((prev) =>
prev.map((entry) =>
entry.isStreaming
? { ...entry, content: `Generated commit message: "${commitMessage.trim()}"`, isStreaming: false }
: entry
)
);
}
break;
}
}
// Execute the commit
const cleanCommitMessage = commitMessage.trim().replace(/^["']|["']$/g, '');
const commitCommand = `git commit -m "${cleanCommitMessage}"`;
const commitResult = await agent.executeBashCommand(commitCommand);
const commitEntry: ChatEntry = {
type: "tool_result",
content: commitResult.success
? commitResult.output || "Commit successful"
: commitResult.error || "Commit failed",
timestamp: new Date(),
toolCall: {
id: `git_commit_${Date.now()}`,
type: "function",
function: {
name: "bash",
arguments: JSON.stringify({ command: commitCommand }),
},
},
toolResult: commitResult,
};
setChatHistory((prev) => [...prev, commitEntry]);
// If commit was successful, push to remote
if (commitResult.success) {
// First try regular push, if it fails try with upstream setup
let pushResult = await agent.executeBashCommand("git push");
let pushCommand = "git push";
if (!pushResult.success && pushResult.error?.includes("no upstream branch")) {
pushCommand = "git push -u origin HEAD";
pushResult = await agent.executeBashCommand(pushCommand);
}
const pushEntry: ChatEntry = {
type: "tool_result",
content: pushResult.success
? pushResult.output || "Push successful"
: pushResult.error || "Push failed",
timestamp: new Date(),
toolCall: {
id: `git_push_${Date.now()}`,
type: "function",
function: {
name: "bash",
arguments: JSON.stringify({ command: pushCommand }),
},
},
toolResult: pushResult,
};
setChatHistory((prev) => [...prev, pushEntry]);
}
} catch (error: any) {
const errorEntry: ChatEntry = {
type: "assistant",
content: `Error during commit and push: ${error.message}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, errorEntry]);
}
setIsProcessing(false);
setIsStreaming(false);
setInput("");
return true;
}
const directBashCommands = [
"ls",
"pwd",
+159 -2
View File
@@ -88,6 +88,112 @@ function loadBaseURL(): string | undefined {
return baseURL;
}
// Handle commit-and-push command in headless mode
async function handleCommitAndPushHeadless(
apiKey: string,
baseURL?: string,
model?: string
): Promise<void> {
try {
const agent = new GrokAgent(apiKey, baseURL, model);
// Configure confirmation service for headless mode (auto-approve all operations)
const confirmationService = ConfirmationService.getInstance();
confirmationService.setSessionFlag("allOperations", true);
console.log("🤖 Processing commit and push...\n");
console.log("> /commit-and-push\n");
// First check if there are any changes at all
const initialStatusResult = await agent.executeBashCommand("git status --porcelain");
if (!initialStatusResult.success || !initialStatusResult.output?.trim()) {
console.log("❌ No changes to commit. Working directory is clean.");
process.exit(1);
}
console.log("✅ git status: Changes detected");
// Add all changes
const addResult = await agent.executeBashCommand("git add .");
if (!addResult.success) {
console.log(`❌ git add: ${addResult.error || 'Failed to stage changes'}`);
process.exit(1);
}
console.log("✅ git add: Changes staged");
// Get staged changes for commit message generation
const diffResult = await agent.executeBashCommand("git diff --cached");
// Generate commit message using AI
const commitPrompt = `Generate a concise, professional git commit message for these changes:
Git Status:
${initialStatusResult.output}
Git Diff (staged changes):
${diffResult.output || "No staged changes shown"}
Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters.
Respond with ONLY the commit message, no additional text.`;
console.log("🤖 Generating commit message...");
const commitMessageEntries = await agent.processUserMessage(commitPrompt);
let commitMessage = "";
// Extract the commit message from the AI response
for (const entry of commitMessageEntries) {
if (entry.type === "assistant" && entry.content.trim()) {
commitMessage = entry.content.trim();
break;
}
}
if (!commitMessage) {
console.log("❌ Failed to generate commit message");
process.exit(1);
}
// Clean the commit message
const cleanCommitMessage = commitMessage.replace(/^["']|["']$/g, '');
console.log(`✅ Generated commit message: "${cleanCommitMessage}"`);
// Execute the commit
const commitCommand = `git commit -m "${cleanCommitMessage}"`;
const commitResult = await agent.executeBashCommand(commitCommand);
if (commitResult.success) {
console.log(`✅ git commit: ${commitResult.output?.split('\n')[0] || 'Commit successful'}`);
// If commit was successful, push to remote
// First try regular push, if it fails try with upstream setup
let pushResult = await agent.executeBashCommand("git push");
if (!pushResult.success && pushResult.error?.includes("no upstream branch")) {
console.log("🔄 Setting upstream and pushing...");
pushResult = await agent.executeBashCommand("git push -u origin HEAD");
}
if (pushResult.success) {
console.log(`✅ git push: ${pushResult.output?.split('\n')[0] || 'Push successful'}`);
} else {
console.log(`❌ git push: ${pushResult.error || 'Push failed'}`);
process.exit(1);
}
} else {
console.log(`❌ git commit: ${commitResult.error || 'Commit failed'}`);
process.exit(1);
}
} catch (error: any) {
console.error("❌ Error during commit and push:", error.message);
process.exit(1);
}
}
// Headless mode processing function
async function processPromptHeadless(
prompt: string,
@@ -158,9 +264,9 @@ async function processPromptHeadless(
program
.name("grok")
.description(
"A conversational AI CLI tool powered by Grok-3 with text editor capabilities"
"A conversational AI CLI tool powered by Grok with text editor capabilities"
)
.version("1.0.0")
.version("1.0.1")
.option("-d, --directory <dir>", "set working directory", process.cwd())
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
.option(
@@ -217,4 +323,55 @@ program
}
});
// Git subcommand
const gitCommand = program
.command("git")
.description("Git operations with AI assistance");
gitCommand
.command("commit-and-push")
.description("Generate AI commit message and push to remote")
.option("-d, --directory <dir>", "set working directory", process.cwd())
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
.option(
"-u, --base-url <url>",
"Grok API base URL (or set GROK_BASE_URL env var)"
)
.option(
"-m, --model <model>",
"AI model to use (e.g., gemini-2.5-pro, grok-4-latest)"
)
.action(async (options) => {
if (options.directory) {
try {
process.chdir(options.directory);
} catch (error: any) {
console.error(
`Error changing directory to ${options.directory}:`,
error.message
);
process.exit(1);
}
}
try {
// Get API key from options, environment, or user settings
const apiKey = options.apiKey || loadApiKey();
const baseURL = options.baseUrl || loadBaseURL();
const model = options.model;
if (!apiKey) {
console.error(
"❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or save to ~/.grok/user-settings.json"
);
process.exit(1);
}
await handleCommitAndPushHeadless(apiKey, baseURL, model);
} catch (error: any) {
console.error("❌ Error during git commit-and-push:", error.message);
process.exit(1);
}
});
program.parse();