From 6bf3b19fc1f411679006597786b764f0a476fd9d Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Tue, 3 Mar 2026 21:27:17 +0530 Subject: [PATCH 1/9] add files --- git-ai/src/cli/pr-command.ts | 55 ++++++++++ git-ai/src/cli/resolve-command.ts | 42 ++++++++ git-ai/src/core/GitService.ts | 46 ++++++++ git-ai/src/index.ts | 39 +++++++ git-ai/src/services/AIService.ts | 118 +++++++++++++++++++++ git-ai/src/services/ConfigService.ts | 61 +++++++++++ git-ai/src/services/ConflictResolver.ts | 135 ++++++++++++++++++++++++ git-ai/src/services/GitHubService.ts | 118 +++++++++++++++++++++ git-ai/src/ui/PRList.tsx | 96 +++++++++++++++++ git-ai/src/utils/logger.ts | 12 +++ git-ai/tsconfig.json | 9 +- 11 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 git-ai/src/cli/pr-command.ts create mode 100644 git-ai/src/cli/resolve-command.ts create mode 100644 git-ai/src/core/GitService.ts create mode 100644 git-ai/src/index.ts create mode 100644 git-ai/src/services/AIService.ts create mode 100644 git-ai/src/services/ConfigService.ts create mode 100644 git-ai/src/services/ConflictResolver.ts create mode 100644 git-ai/src/services/GitHubService.ts create mode 100644 git-ai/src/ui/PRList.tsx create mode 100644 git-ai/src/utils/logger.ts diff --git a/git-ai/src/cli/pr-command.ts b/git-ai/src/cli/pr-command.ts new file mode 100644 index 0000000..0ed4148 --- /dev/null +++ b/git-ai/src/cli/pr-command.ts @@ -0,0 +1,55 @@ +import React from 'react'; +import { render } from 'ink'; +import { GitService } from '../core/GitService.js'; +import { ConfigService } from '../services/ConfigService.js'; +import { GitHubService, PullRequestMetadata } from '../services/GitHubService.js'; +import { PRList } from '../ui/PRList.js'; +import { logger } from '../utils/logger.js'; + +/** + * Orchestrates the Interactive PR Selection UI + */ +export async function runPRCommand(): Promise { + try { + const configService = new ConfigService(); + const gitService = new GitService(); + + // 1. Get the remote URL to identify the GitHub repository + // Accessing the underlying git instance safely: + const remotes = await (gitService as any).git.getRemotes(true); + const origin = remotes.find((r: any) => r.name === 'origin'); + + if (!origin || !origin.refs.fetch) { + console.error('❌ Error: No remote "origin" found. Ensure your repo is hosted on GitHub.'); + return; + } + + const githubService = new GitHubService(configService, origin.refs.fetch); + + // 2. Define the selection handler + const handleSelect = (pr: PullRequestMetadata) => { + console.log('\n-----------------------------------'); + console.log(`🚀 Selected PR: #${pr.number}`); + console.log(`🔗 URL: ${pr.url}`); + console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); + console.log('-----------------------------------\n'); + + // In a future update, we can trigger gitService.checkout(pr.branch) + process.exit(0); + }; + + // 3. Launch the Ink TUI + const { waitUntilExit } = render( + React.createElement(PRList, { + githubService, + onSelect: handleSelect + }) + ); + + await waitUntilExit(); + } catch (error) { + logger.error(error instanceof Error ? error : new Error(String(error)), 'Failed to initialize PR command'); + console.error('❌ Critical Error: Could not launch PR interface.'); + process.exit(1); + } +} \ No newline at end of file diff --git a/git-ai/src/cli/resolve-command.ts b/git-ai/src/cli/resolve-command.ts new file mode 100644 index 0000000..241ca3d --- /dev/null +++ b/git-ai/src/cli/resolve-command.ts @@ -0,0 +1,42 @@ +import { ConflictResolver } from '../services/ConflictResolver.js'; +import { AIService } from '../services/AIService.js'; +import { GitService } from '../core/GitService.js'; +import { ConfigService } from '../services/ConfigService.js'; + +export async function runResolveCommand() { + try { + const config = new ConfigService(); + const git = new GitService(); + const ai = new AIService(config); + const resolver = new ConflictResolver(ai, git); + + const conflicts = await resolver.getConflicts(); + + if (conflicts.length === 0) { + console.log('✅ No merge conflicts detected.'); + return; + } + + console.log(`🔍 Found ${conflicts.length} files with conflicts.`); + + for (const conflict of conflicts) { + try { + console.log(`🤖 Analyzing ${conflict.file}...`); + const suggestion = await resolver.suggestResolution(conflict); + + console.log(`\n--- AI Suggested Resolution for ${conflict.file} ---`); + console.log(suggestion); + console.log('--------------------------------------------------\n'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`⚠️ Failed to process conflict for ${conflict.file}: ${message}`); + } + + // In the final UI/Ink phase, we would add a [Apply] / [Skip] prompt here. + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`❌ Failed to initialize conflict resolution: ${message}`); + process.exit(1); + } +} \ No newline at end of file diff --git a/git-ai/src/core/GitService.ts b/git-ai/src/core/GitService.ts new file mode 100644 index 0000000..06df4f7 --- /dev/null +++ b/git-ai/src/core/GitService.ts @@ -0,0 +1,46 @@ +import { simpleGit, SimpleGit, StatusResult, LogResult } from 'simple-git'; +import { logger } from './../utils/logger.js'; + +export class GitService { + private git: SimpleGit; + + constructor(workingDir: string = process.cwd()) { + this.git = simpleGit(workingDir); + } + + public async getStatus(): Promise { + try { + return await this.git.status(); + } catch (error) { + logger.error(`Failed to fetch git status: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } + } + + public async getDiff(): Promise { + return await this.git.diff(['--cached']); + } + + public async commit(message: string): Promise { + const normalizedMessage = message.trim(); + if (!normalizedMessage) { + throw new Error('Commit message cannot be empty or whitespace.'); + } + + try { + await this.git.commit(normalizedMessage); + } catch (error) { + const original = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create git commit: ${original}`); + } + } + + public async getLog(limit: number = 10): Promise { + return await this.git.log({ maxCount: limit }); + } + + public async getCurrentBranch(): Promise { + const branchData = await this.git.branch(); + return branchData.current; + } +} \ No newline at end of file diff --git a/git-ai/src/index.ts b/git-ai/src/index.ts new file mode 100644 index 0000000..7570efc --- /dev/null +++ b/git-ai/src/index.ts @@ -0,0 +1,39 @@ +import { Command } from 'commander'; +import { GitService } from './core/GitService.js'; +import { ConfigService } from './services/ConfigService.js'; +import { AIService } from './services/AIService.js'; +import { logger } from './utils/logger.js'; + +const program = new Command(); +const configService = new ConfigService(); +const gitService = new GitService(); +const aiService = new AIService(configService); + +program + .name('ai-git') + .version('0.1.0'); + +program + .command('ai-commit') + .description('Generate a commit message using AI and commit staged changes') + .action(async () => { + try { + const diff = await gitService.getDiff(); + if (!diff) { + console.log('No staged changes found. Please stage files first.'); + return; + } + + console.log('🤖 Generating commit message...'); + const message = await aiService.generateCommitMessage(diff); + + console.log(`\nSuggested Message: "${message}"`); + await gitService.commit(message); + console.log('✅ Changes committed successfully.'); + } catch (err) { + logger.error(`AI Commit failed: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); + +program.parse(process.argv); \ No newline at end of file diff --git a/git-ai/src/services/AIService.ts b/git-ai/src/services/AIService.ts new file mode 100644 index 0000000..2b3e814 --- /dev/null +++ b/git-ai/src/services/AIService.ts @@ -0,0 +1,118 @@ +import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai"; +import { ConfigService } from "./ConfigService.js"; +import { logger } from "../utils/logger.js"; + +export interface AIProvider { + generateCommitMessage(diff: string): Promise; + analyzeConflicts(conflictFileContents: Record): Promise; + generateContent(prompt: string): Promise; +} + +export class AIService implements AIProvider { + private genAI: GoogleGenerativeAI | null = null; + private model: GenerativeModel | null = null; + private configService: ConfigService; + + constructor(configService: ConfigService) { + this.configService = configService; + this.initClient(); + } + + private initClient(): void { + const config = this.configService.getConfig(); + + // Ensure we only init if the provider is gemini + if (config.ai.provider === "gemini") { + if (!config.ai.apiKey) { + throw new Error("Gemini API Key is missing in .aigitrc"); + } + + this.genAI = new GoogleGenerativeAI(config.ai.apiKey); + this.model = this.genAI.getGenerativeModel({ + model: config.ai.model || "gemini-1.5-flash", + generationConfig: { + temperature: 0.2, + topP: 0.8, + maxOutputTokens: 200, + }, + }); + } + } + + /** + * Generates a conventional commit message based on git diff + */ + public async generateCommitMessage(diff: string): Promise { + if (!this.model) { + throw new Error("Gemini AI model not initialized. Check your config."); + } + + const prompt = ` + You are an expert software engineer. + Generate a professional, concise conventional commit message based on this git diff: + + "${diff}" + + Instructions: + 1. Use the format: (): + 2. Common types: feat, fix, docs, style, refactor, test, chore. + 3. Description should be in present tense and lowercase. + 4. Return ONLY the commit message text. + `; + + try { + const result = await this.model.generateContent(prompt); + const response = await result.response; + const text = response.text().trim(); + + // Clean up potential markdown formatting if Gemini returns backticks + return text.replace(/`/g, ""); + } catch (error) { + logger.error( + `Gemini API Error: ${error instanceof Error ? error.message : String(error)}`, + ); + throw new Error("Failed to generate commit message via Gemini."); + } + } + + /** + * Analyzes merge conflicts and suggests resolutions + */ + public async analyzeConflicts(conflictFileContents: Record): Promise { + if (!this.model) throw new Error("AI Service not ready"); + + const conflictsWithContent = Object.entries(conflictFileContents) + .map(([fileName, content]) => `FILE: ${fileName}\n${content}`) + .join("\n\n"); + + const prompt = `Analyze the following files currently in a git conflict state and provide a high-level summary of the clashing changes. Include key differences and likely intent from both sides of each conflict marker block.\n\n${conflictsWithContent}`; + + try { + const result = await this.model.generateContent(prompt); + return result.response.text(); + } catch (error) { + logger.error( + `Conflict Analysis Error: ${error instanceof Error ? error.message : String(error)}`, + ); + return "Could not analyze conflicts at this time."; + } + } + + public async generateContent(prompt: string): Promise { + if (!this.model) { + throw new Error("Gemini AI model not initialized. Check your config."); + } + + try { + const result = await this.model.generateContent(prompt); + const response = await result.response; + return response.text(); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error( + `AI generateContent Error: ${errorMsg}`, + ); + throw new Error("Failed to generate content via AI service."); + } + } +} diff --git a/git-ai/src/services/ConfigService.ts b/git-ai/src/services/ConfigService.ts new file mode 100644 index 0000000..f4f33d0 --- /dev/null +++ b/git-ai/src/services/ConfigService.ts @@ -0,0 +1,61 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { z } from 'zod'; + +export const ConfigSchema = z.object({ + ai: z.object({ + provider: z.enum(['openai', 'gemini']), + apiKey: z.string(), + model: z.string().optional(), + }), + github: z.object({ + token: z.string().min(1), + }).optional(), + git: z.object({ + autoStage: z.boolean().default(false), + messagePrefix: z.string().optional(), + }), + ui: z.object({ + theme: z.enum(['dark', 'light', 'system']).default('dark'), + showIcons: z.boolean().default(true), + }), +}); + +export type Config = z.infer; + +export class ConfigService { + private static readonly CONFIG_PATH = path.join(os.homedir(), '.aigitrc'); + private config: Config | null = null; + + constructor() { + this.loadConfig(); + } + + private loadConfig(): void { + if (!fs.existsSync(ConfigService.CONFIG_PATH)) { + this.config = null; + return; + } + + try { + const rawConfig = JSON.parse(fs.readFileSync(ConfigService.CONFIG_PATH, 'utf-8')); + this.config = ConfigSchema.parse(rawConfig); + } catch (error) { + throw new Error(`Invalid configuration file at ${ConfigService.CONFIG_PATH}: ${error}`); + } + } + + public getConfig(): Config { + if (!this.config) { + throw new Error("Configuration not initialized. Please run 'ai-git init'."); + } + return this.config; + } + + public saveConfig(newConfig: Config): void { + const validated = ConfigSchema.parse(newConfig); + fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2)); + this.config = validated; + } +} \ No newline at end of file diff --git a/git-ai/src/services/ConflictResolver.ts b/git-ai/src/services/ConflictResolver.ts new file mode 100644 index 0000000..6c4334b --- /dev/null +++ b/git-ai/src/services/ConflictResolver.ts @@ -0,0 +1,135 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { AIService } from './AIService.js'; +import { GitService } from '../core/GitService.js'; +import { logger } from '../utils/logger.js'; + +export interface ConflictDetail { + file: string; + content: string; + suggestion?: string; +} + +export class ConflictResolver { + constructor( + private aiService: AIService, + private gitService: GitService + ) {} + + /** + * Identifies files with merge conflicts and fetches their content + */ + public async getConflicts(): Promise { + const status = await this.gitService.getStatus(); + const conflictFiles = status.conflicted; + + if (conflictFiles.length === 0) return []; + + const results = await Promise.allSettled( + conflictFiles.map(async (file) => { + const filePath = path.resolve(process.cwd(), file); + const content = await fs.readFile(filePath, 'utf-8'); + return { file, content }; + }) + ); + + const conflicts: ConflictDetail[] = []; + for (let index = 0; index < results.length; index++) { + const result = results[index]; + const file = conflictFiles[index]; + if (result.status === 'fulfilled') { + conflicts.push(result.value); + continue; + } + + const reason = result.reason instanceof Error ? result.reason.message : String(result.reason); + logger.error(`Failed to read conflicted file ${file}: ${reason}`); + } + + return conflicts; + } + + /** + * Uses Gemini to analyze the conflict markers and suggest a fix + */ + public async suggestResolution(conflict: ConflictDetail): Promise { + const prompt = ` + You are a senior software architect. I have a merge conflict in the file: ${conflict.file}. + Below is the file content containing git conflict markers (<<<<<<<, =======, >>>>>>>). + + FILE CONTENT: + ${conflict.content} + + INSTRUCTIONS: + 1. Analyze the changes from both branches. + 2. Provide the full RESOLVED file content. + 3. Remove all git conflict markers. + 4. Ensure the code is syntactically correct and merges the intent of both changes. + 5. Return ONLY the code for the resolved file. + `; + + try { + const responseText = await this.aiService.generateContent(prompt); + return responseText + .replace(/^```[a-z]*\s*\n?/gim, '') + .replace(/\n?```\s*$/gim, '') + .trim(); + } catch (error) { + logger.error(error instanceof Error ? error : new Error(String(error)), `AI Resolution failed for ${conflict.file}`); + throw new Error('Could not generate resolution suggestion.'); + } + } + + /** + * Applies the AI's suggested resolution to the physical file. + * Uses atomic write with O_NOFOLLOW to prevent symlink attacks and TOCTOU races. + */ + public async applyResolution(file: string, resolvedContent: string): Promise { + const realRepoRoot = await fs.realpath(process.cwd()); + const targetPath = path.resolve(realRepoRoot, file); + + // Check if file exists and reject if it's a symlink + try { + const targetStats = await fs.lstat(targetPath); + if (targetStats.isSymbolicLink()) { + throw new Error(`Refusing to write to symlink: ${file}`); + } + } catch (error: unknown) { + // Handle ENOENT gracefully - file doesn't exist yet, which is fine + if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { + // File does not exist, proceeding with creation is safe + } else { + // Re-throw any other error + throw error; + } + } + + // Validate repository boundary by checking parent directory + const targetParent = path.dirname(targetPath); + let realParent: string; + try { + realParent = await fs.realpath(targetParent); + } catch (error: unknown) { + if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { + throw new Error(`Parent directory does not exist: ${targetParent}`); + } + throw error; + } + const normalizedRoot = realRepoRoot.endsWith(path.sep) ? realRepoRoot : `${realRepoRoot}${path.sep}`; + if (!realParent.startsWith(normalizedRoot) && realParent !== realRepoRoot) { + throw new Error(`Refusing to write outside repository root: ${file}`); + } + + // Use atomic write with O_NOFOLLOW to prevent TOCTOU races and symlink escape + const flags = fs.constants.O_NOFOLLOW | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY; + const buffer = Buffer.from(resolvedContent, 'utf-8'); + const fileHandle = await fs.open(targetPath, flags, 0o644); + try { + await fileHandle.write(buffer, 0, buffer.length); + await fileHandle.sync(); + } finally { + await fileHandle.close(); + } + // Note: User should still 'git add' the file manually or via CLI flow + } +} \ No newline at end of file diff --git a/git-ai/src/services/GitHubService.ts b/git-ai/src/services/GitHubService.ts new file mode 100644 index 0000000..250e3c5 --- /dev/null +++ b/git-ai/src/services/GitHubService.ts @@ -0,0 +1,118 @@ +import { Octokit } from "@octokit/rest"; +import { ConfigService } from "./ConfigService.js"; +import { logger } from "../utils/logger.js"; + +export interface PullRequestMetadata { + id: number; + number: number; + title: string; + author: string; + branch: string; + base: string; + state: string; + url: string; + isMergeable: boolean | null; +} + +export class GitHubService { + private octokit: Octokit; + private owner: string = ""; + private repo: string = ""; + + constructor(configService: ConfigService, repoUrl: string) { + const config = configService.getConfig(); + const githubToken = config.github?.token; + if (!githubToken) { + throw new Error("GitHub token is missing in configuration. Set github.token in .aigitrc."); + } + + this.octokit = new Octokit({ + auth: githubToken, + }); + + this.parseRepoInfo(repoUrl); + } + + /** + * Extracts owner and repo name from a git remote URL + */ + private parseRepoInfo(url: string): void { + const match = url.match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i); + if (match) { + this.owner = match[1]; + this.repo = match[2]; + } else { + logger.error(`Could not parse GitHub owner/repo from remote URL: ${url}`); + throw new Error("Invalid GitHub remote URL. Expected github.com//."); + } + + if (!this.owner || !this.repo) { + logger.error(`Parsed empty GitHub owner/repo from remote URL: ${url}`); + throw new Error("Could not determine GitHub owner and repository from remote URL."); + } + } + + /** + * Lists all open pull requests for the current repository. + * Note: pulls.list does not provide mergeability; isMergeable is set to null. + */ + public async listOpenPRs(): Promise { + try { + const { data } = await this.octokit.pulls.list({ + owner: this.owner, + repo: this.repo, + state: "open", + }); + + return data.map((pr) => ({ + id: pr.id, + number: pr.number, + title: pr.title, + author: pr.user?.login || "unknown", + branch: pr.head.ref, + base: pr.base.ref, + state: pr.state, + url: pr.html_url, + isMergeable: null, + })); + } catch (error: unknown) { + if (error instanceof Error) { + logger.error( + { err: error, owner: this.owner, repo: this.repo }, + "Failed to fetch Pull Requests" + ); + } else { + logger.error( + { err: error, owner: this.owner, repo: this.repo }, + "Failed to fetch Pull Requests (non-Error thrown)" + ); + } + + throw new Error("GitHub API request failed."); + } + } + + /** + * Merges a Pull Request + */ + public async mergePR( + prNumber: number, + method: "merge" | "squash" | "rebase" = "merge", + ): Promise { + try { + const { data } = await this.octokit.pulls.merge({ + owner: this.owner, + repo: this.repo, + pull_number: prNumber, + merge_method: method, + }); + + return data.merged; + } catch (error) { + logger.error( + `Failed to merge PR #${prNumber}: ${error instanceof Error ? error.message : String(error)}`, + ); + return false; + } + } +} diff --git a/git-ai/src/ui/PRList.tsx b/git-ai/src/ui/PRList.tsx new file mode 100644 index 0000000..a95da6c --- /dev/null +++ b/git-ai/src/ui/PRList.tsx @@ -0,0 +1,96 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Box, Text, useInput } from 'ink'; +import { GitHubService, PullRequestMetadata } from '../services/GitHubService.js'; + +interface PRListProps { + githubService: GitHubService; + onSelect: (pr: PullRequestMetadata) => void; +} + +export const PRList: React.FC = ({ githubService, onSelect }) => { + const [prs, setPrs] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const prsRef = useRef([]); + const selectedIndexRef = useRef(0); + + useEffect(() => { + prsRef.current = prs; + }, [prs]); + + useEffect(() => { + selectedIndexRef.current = selectedIndex; + }, [selectedIndex]); + + useEffect(() => { + async function fetchPRs() { + try { + const data = await githubService.listOpenPRs(); + setPrs(data); + } catch (err) { + setError('Failed to load Pull Requests'); + } finally { + setLoading(false); + } + } + fetchPRs(); + }, [githubService]); + + useInput((_input, key) => { + const currentPrs = prsRef.current; + if (currentPrs.length === 0) { + return; + } + + if (key.upArrow) { + setSelectedIndex((prev) => { + const nextIndex = prev > 0 ? prev - 1 : currentPrs.length - 1; + return Math.max(0, Math.min(nextIndex, currentPrs.length - 1)); + }); + } + if (key.downArrow) { + setSelectedIndex((prev) => { + const nextIndex = prev < currentPrs.length - 1 ? prev + 1 : 0; + return Math.max(0, Math.min(nextIndex, currentPrs.length - 1)); + }); + } + if (key.return) { + const currentIndex = selectedIndexRef.current; + if (currentIndex >= 0 && currentIndex < currentPrs.length) { + onSelect(currentPrs[currentIndex]); + } + } + }); + + if (loading) return ⏳ Loading Pull Requests...; + if (error) return ✖ {error}; + if (prs.length === 0) return No open Pull Requests found.; + + return ( + + + Open Pull Requests + + + {prs.map((pr, index) => { + const isSelected = index === selectedIndex; + return ( + + + {isSelected ? '❯ ' : ' '} + + #{pr.number} {pr.title} + + ({pr.author}) + + + ); + })} + + + Use ↑/↓ to navigate, Enter to select + + + ); +}; \ No newline at end of file diff --git a/git-ai/src/utils/logger.ts b/git-ai/src/utils/logger.ts new file mode 100644 index 0000000..49ec66a --- /dev/null +++ b/git-ai/src/utils/logger.ts @@ -0,0 +1,12 @@ +import pino from 'pino'; + +export const logger = pino({ + level: process.env.LOG_LEVEL || 'info', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + ignore: 'pid,hostname', + }, + }, +}); \ No newline at end of file diff --git a/git-ai/tsconfig.json b/git-ai/tsconfig.json index b5b3ca0..7bea1a6 100644 --- a/git-ai/tsconfig.json +++ b/git-ai/tsconfig.json @@ -22,8 +22,13 @@ /* Support for Ink/React (Phase 2 readiness) */ "jsx": "react-jsx", - "jsxImportSource": "react" + "jsxImportSource": "react", + + /* ESM Specifics */ + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] } \ No newline at end of file From 70997205c1c292ae21a57d60e303d29429faa941 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:02:30 +0000 Subject: [PATCH 2/9] Initial plan From 33ce722519056d9032d01efafdc3259ace52ae88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:05:16 +0000 Subject: [PATCH 3/9] fix: add missing npm dependencies to resolve TypeScript lint errors Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> --- git-ai/package-lock.json | 482 ++++++++++++++++++++++++++++++++++++++- git-ai/package.json | 7 +- 2 files changed, 487 insertions(+), 2 deletions(-) diff --git a/git-ai/package-lock.json b/git-ai/package-lock.json index 8740ad7..2d8886b 100644 --- a/git-ai/package-lock.json +++ b/git-ai/package-lock.json @@ -9,11 +9,16 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^22.0.1", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.23.8" }, "bin": { "ai-git": "dist/cli.js" @@ -483,6 +488,15 @@ "node": ">=18" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -498,6 +512,167 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", @@ -565,6 +740,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/auto-bind": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", @@ -577,6 +761,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -660,6 +850,12 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -685,6 +881,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -708,6 +913,15 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -781,6 +995,34 @@ "node": ">=8" } }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -821,6 +1063,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -908,12 +1156,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -935,12 +1198,39 @@ "node": ">=6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -965,6 +1255,99 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -993,6 +1376,15 @@ "react": "^18.3.1" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1019,6 +1411,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1028,6 +1429,22 @@ "loose-envify": "^1.1.0" } }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -1080,6 +1497,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -1124,6 +1559,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1177,6 +1636,12 @@ "dev": true, "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -1209,6 +1674,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -1235,6 +1706,15 @@ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/git-ai/package.json b/git-ai/package.json index adaeb73..b505272 100644 --- a/git-ai/package.json +++ b/git-ai/package.json @@ -31,11 +31,16 @@ "author": "", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^22.0.1", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20.11.0", From c8467513133e9f95694263cf4a68abae6e61ecb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:11:39 +0000 Subject: [PATCH 4/9] Initial plan From 173466cc7c479d2171e0b4f0865b153e2a0d3b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:16:54 +0000 Subject: [PATCH 5/9] fix: add missing deps, fix security/quality issues from review Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> --- git-ai/package-lock.json | 340 +++++++++++++++++++++++- git-ai/package.json | 6 +- git-ai/src/cli/pr-command.ts | 31 +-- git-ai/src/index.ts | 2 +- git-ai/src/services/AIService.ts | 2 + git-ai/src/services/ConfigService.ts | 2 +- git-ai/src/services/ConflictResolver.ts | 91 +++++-- git-ai/src/services/GitHubService.ts | 24 +- 8 files changed, 460 insertions(+), 38 deletions(-) diff --git a/git-ai/package-lock.json b/git-ai/package-lock.json index 8740ad7..33421df 100644 --- a/git-ai/package-lock.json +++ b/git-ai/package-lock.json @@ -9,11 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^20.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^9.5.0", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.24.2" }, "bin": { "ai-git": "dist/cli.js" @@ -483,6 +487,15 @@ "node": ">=18" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -498,6 +511,167 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", @@ -565,6 +739,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/auto-bind": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", @@ -577,6 +760,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -702,6 +891,12 @@ } } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -941,6 +1136,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -965,6 +1178,65 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -993,6 +1265,15 @@ "react": "^18.3.1" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1019,6 +1300,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1080,6 +1370,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -1124,6 +1432,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1177,6 +1494,12 @@ "dev": true, "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -1209,6 +1532,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -1235,6 +1564,15 @@ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/git-ai/package.json b/git-ai/package.json index adaeb73..743a55e 100644 --- a/git-ai/package.json +++ b/git-ai/package.json @@ -31,11 +31,15 @@ "author": "", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^20.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^9.5.0", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^20.11.0", diff --git a/git-ai/src/cli/pr-command.ts b/git-ai/src/cli/pr-command.ts index 0ed4148..fa161b2 100644 --- a/git-ai/src/cli/pr-command.ts +++ b/git-ai/src/cli/pr-command.ts @@ -21,32 +21,29 @@ export async function runPRCommand(): Promise { if (!origin || !origin.refs.fetch) { console.error('❌ Error: No remote "origin" found. Ensure your repo is hosted on GitHub.'); - return; + process.exit(1); } const githubService = new GitHubService(configService, origin.refs.fetch); - // 2. Define the selection handler - const handleSelect = (pr: PullRequestMetadata) => { - console.log('\n-----------------------------------'); - console.log(`🚀 Selected PR: #${pr.number}`); - console.log(`🔗 URL: ${pr.url}`); - console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); - console.log('-----------------------------------\n'); - - // In a future update, we can trigger gitService.checkout(pr.branch) - process.exit(0); - }; - - // 3. Launch the Ink TUI - const { waitUntilExit } = render( + // 2. Launch the Ink TUI + const renderInstance = render( React.createElement(PRList, { githubService, - onSelect: handleSelect + onSelect: (pr: PullRequestMetadata) => { + console.log('\n-----------------------------------'); + console.log(`🚀 Selected PR: #${pr.number}`); + console.log(`🔗 URL: ${pr.url}`); + console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); + console.log('-----------------------------------\n'); + // In a future update, we can trigger gitService.checkout(pr.branch) + renderInstance.unmount(); + } }) ); - await waitUntilExit(); + // 3. Await clean exit + await renderInstance.waitUntilExit(); } catch (error) { logger.error(error instanceof Error ? error : new Error(String(error)), 'Failed to initialize PR command'); console.error('❌ Critical Error: Could not launch PR interface.'); diff --git a/git-ai/src/index.ts b/git-ai/src/index.ts index 7570efc..8618465 100644 --- a/git-ai/src/index.ts +++ b/git-ai/src/index.ts @@ -7,7 +7,6 @@ import { logger } from './utils/logger.js'; const program = new Command(); const configService = new ConfigService(); const gitService = new GitService(); -const aiService = new AIService(configService); program .name('ai-git') @@ -18,6 +17,7 @@ program .description('Generate a commit message using AI and commit staged changes') .action(async () => { try { + const aiService = new AIService(configService); const diff = await gitService.getDiff(); if (!diff) { console.log('No staged changes found. Please stage files first.'); diff --git a/git-ai/src/services/AIService.ts b/git-ai/src/services/AIService.ts index 2b3e814..511e5d7 100644 --- a/git-ai/src/services/AIService.ts +++ b/git-ai/src/services/AIService.ts @@ -36,6 +36,8 @@ export class AIService implements AIProvider { maxOutputTokens: 200, }, }); + } else { + throw new Error(`Unsupported AI provider: ${config.ai.provider}`); } } diff --git a/git-ai/src/services/ConfigService.ts b/git-ai/src/services/ConfigService.ts index f4f33d0..16d7453 100644 --- a/git-ai/src/services/ConfigService.ts +++ b/git-ai/src/services/ConfigService.ts @@ -55,7 +55,7 @@ export class ConfigService { public saveConfig(newConfig: Config): void { const validated = ConfigSchema.parse(newConfig); - fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2)); + fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 }); this.config = validated; } } \ No newline at end of file diff --git a/git-ai/src/services/ConflictResolver.ts b/git-ai/src/services/ConflictResolver.ts index 6c4334b..85782c2 100644 --- a/git-ai/src/services/ConflictResolver.ts +++ b/git-ai/src/services/ConflictResolver.ts @@ -1,5 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; +import { randomBytes } from 'crypto'; import { AIService } from './AIService.js'; import { GitService } from '../core/GitService.js'; import { logger } from '../utils/logger.js'; @@ -10,6 +11,54 @@ export interface ConflictDetail { suggestion?: string; } +/** + * Patterns for detecting secrets and sensitive data in conflict content. + */ +const SECRET_PATTERNS: RegExp[] = [ + /-----BEGIN\s+(?:RSA\s+)?PRIVATE KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE KEY-----/gi, + /(?:api[_\s-]?key|apikey|secret|token|password|passwd|pwd|auth)['":\s=]+['"]?[A-Za-z0-9/+_\-]{16,}['"]?/gi, + // Long base64-like strings that resemble encoded tokens (standalone, not part of identifiers) + /(?>>>>>>) from file content. + */ +function extractConflictHunks(content: string): string { + const lines = content.split('\n'); + const hunks: string[] = []; + let inConflict = false; + let hunk: string[] = []; + + for (const line of lines) { + if (line.startsWith('<<<<<<<')) { + inConflict = true; + hunk = [line]; + } else if (inConflict) { + hunk.push(line); + if (line.startsWith('>>>>>>>')) { + hunks.push(hunk.join('\n')); + hunk = []; + inConflict = false; + } + } + } + + return hunks.length > 0 ? hunks.join('\n\n') : content; +} + +/** + * Redacts common secret patterns from content before sending to an external model. + */ +function sanitizeContent(content: string): string { + let sanitized = extractConflictHunks(content); + for (const pattern of SECRET_PATTERNS) { + sanitized = sanitized.replace(pattern, '[REDACTED]'); + } + return sanitized; +} + export class ConflictResolver { constructor( private aiService: AIService, @@ -50,15 +99,18 @@ export class ConflictResolver { } /** - * Uses Gemini to analyze the conflict markers and suggest a fix + * Uses Gemini to analyze the conflict markers and suggest a fix. + * Sanitizes the content before sending to the external model. */ public async suggestResolution(conflict: ConflictDetail): Promise { + const sanitizedContent = sanitizeContent(conflict.content); + const prompt = ` You are a senior software architect. I have a merge conflict in the file: ${conflict.file}. - Below is the file content containing git conflict markers (<<<<<<<, =======, >>>>>>>). + Below are the conflict hunks containing git conflict markers (<<<<<<<, =======, >>>>>>>). - FILE CONTENT: - ${conflict.content} + CONFLICT HUNKS: + ${sanitizedContent} INSTRUCTIONS: 1. Analyze the changes from both branches. @@ -81,8 +133,8 @@ export class ConflictResolver { } /** - * Applies the AI's suggested resolution to the physical file. - * Uses atomic write with O_NOFOLLOW to prevent symlink attacks and TOCTOU races. + * Applies the AI's suggested resolution to the physical file using a truly atomic + * write: write to a temp file in the same directory, fsync, then rename to target. */ public async applyResolution(file: string, resolvedContent: string): Promise { const realRepoRoot = await fs.realpath(process.cwd()); @@ -95,11 +147,9 @@ export class ConflictResolver { throw new Error(`Refusing to write to symlink: ${file}`); } } catch (error: unknown) { - // Handle ENOENT gracefully - file doesn't exist yet, which is fine if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { // File does not exist, proceeding with creation is safe } else { - // Re-throw any other error throw error; } } @@ -120,15 +170,26 @@ export class ConflictResolver { throw new Error(`Refusing to write outside repository root: ${file}`); } - // Use atomic write with O_NOFOLLOW to prevent TOCTOU races and symlink escape - const flags = fs.constants.O_NOFOLLOW | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY; + // Atomic write: write to a temp file, fsync, then rename into place. + const tempPath = path.join(targetParent, `.tmp-${process.pid}-${randomBytes(8).toString('hex')}`); const buffer = Buffer.from(resolvedContent, 'utf-8'); - const fileHandle = await fs.open(targetPath, flags, 0o644); + const tempHandle = await fs.open(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o644); try { - await fileHandle.write(buffer, 0, buffer.length); - await fileHandle.sync(); - } finally { - await fileHandle.close(); + await tempHandle.write(buffer, 0, buffer.length); + await tempHandle.sync(); + await tempHandle.close(); + } catch (error) { + await tempHandle.close().catch(() => {}); + await fs.unlink(tempPath).catch(() => {}); + throw error; + } + + // Atomically replace target with temp file + try { + await fs.rename(tempPath, targetPath); + } catch (error) { + await fs.unlink(tempPath).catch(() => {}); + throw error; } // Note: User should still 'git add' the file manually or via CLI flow } diff --git a/git-ai/src/services/GitHubService.ts b/git-ai/src/services/GitHubService.ts index 250e3c5..062fd85 100644 --- a/git-ai/src/services/GitHubService.ts +++ b/git-ai/src/services/GitHubService.ts @@ -33,6 +33,26 @@ export class GitHubService { this.parseRepoInfo(repoUrl); } + /** + * Strips credentials (userinfo) from a URL for safe logging. + */ + private sanitizeUrl(url: string): string { + try { + // Handle SSH-style git URLs like git@github.com:owner/repo.git + const sshMatch = url.match(/^[^@]+@([^:]+):(.+)$/); + if (sshMatch) { + return `${sshMatch[1]}:${sshMatch[2]}`; + } + const parsed = new URL(url); + parsed.username = ''; + parsed.password = ''; + return parsed.toString(); + } catch { + // If URL parsing fails, return a placeholder + return '[unparseable URL]'; + } + } + /** * Extracts owner and repo name from a git remote URL */ @@ -42,12 +62,12 @@ export class GitHubService { this.owner = match[1]; this.repo = match[2]; } else { - logger.error(`Could not parse GitHub owner/repo from remote URL: ${url}`); + logger.error(`Could not parse GitHub owner/repo from remote URL: ${this.sanitizeUrl(url)}`); throw new Error("Invalid GitHub remote URL. Expected github.com//."); } if (!this.owner || !this.repo) { - logger.error(`Parsed empty GitHub owner/repo from remote URL: ${url}`); + logger.error(`Parsed empty GitHub owner/repo from remote URL: ${this.sanitizeUrl(url)}`); throw new Error("Could not determine GitHub owner and repository from remote URL."); } } From a7d74776a8e47ca377025cee181c042d081e7043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:18:59 +0000 Subject: [PATCH 6/9] Initial plan From 0a9918a302bd3a6c1d5f8710c56b5697c5c29e0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:21:21 +0000 Subject: [PATCH 7/9] fix: install deps and replace short-write with writeFile in ConflictResolver Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> --- git-ai/package-lock.json | 6 ------ git-ai/src/services/ConflictResolver.ts | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/git-ai/package-lock.json b/git-ai/package-lock.json index 3b1ca6f..2d8886b 100644 --- a/git-ai/package-lock.json +++ b/git-ai/package-lock.json @@ -907,12 +907,6 @@ } } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", diff --git a/git-ai/src/services/ConflictResolver.ts b/git-ai/src/services/ConflictResolver.ts index 85782c2..460ad5b 100644 --- a/git-ai/src/services/ConflictResolver.ts +++ b/git-ai/src/services/ConflictResolver.ts @@ -175,7 +175,7 @@ export class ConflictResolver { const buffer = Buffer.from(resolvedContent, 'utf-8'); const tempHandle = await fs.open(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o644); try { - await tempHandle.write(buffer, 0, buffer.length); + await tempHandle.writeFile(buffer); await tempHandle.sync(); await tempHandle.close(); } catch (error) { From 70a25c22dcb86b75cc9586ce031e2629b75f3782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:48:08 +0000 Subject: [PATCH 8/9] Initial plan From d177c5ff39ea7c83dab9b4bad738f7214d311d66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:49:29 +0000 Subject: [PATCH 9/9] fix: preserve file mode in ConflictResolver atomic write Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> --- git-ai/src/services/ConflictResolver.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/git-ai/src/services/ConflictResolver.ts b/git-ai/src/services/ConflictResolver.ts index 460ad5b..7fa2809 100644 --- a/git-ai/src/services/ConflictResolver.ts +++ b/git-ai/src/services/ConflictResolver.ts @@ -173,7 +173,14 @@ export class ConflictResolver { // Atomic write: write to a temp file, fsync, then rename into place. const tempPath = path.join(targetParent, `.tmp-${process.pid}-${randomBytes(8).toString('hex')}`); const buffer = Buffer.from(resolvedContent, 'utf-8'); - const tempHandle = await fs.open(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o644); + let fileMode = 0o644; + try { + const stat = await fs.lstat(targetPath); + fileMode = stat.mode & 0o777; + } catch { + // Target does not exist; use default mode 0o644 + } + const tempHandle = await fs.open(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, fileMode); try { await tempHandle.writeFile(buffer); await tempHandle.sync();