@amnesia2k/git-aic 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ import axios from "axios";
2
+ import chalk from "chalk";
3
+ import { getStoredApiKey } from "./config.js";
4
+ import { buildBatchDiffExplanationsPrompt, buildDiffExplanationPrompt, buildDiffFileNamePrompt, buildPrompt, } from "./prompt.js";
5
+ const VALID_DIFF_FILE_TYPES = new Set([
6
+ "feat",
7
+ "fix",
8
+ "refactor",
9
+ "chore",
10
+ "docs",
11
+ "style",
12
+ "test",
13
+ "perf",
14
+ "bugfix",
15
+ ]);
16
+ const API_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent";
17
+ const MAX_RETRIES = 4;
18
+ const getApiKey = () => process.env.GEMINI_COMMIT_MESSAGE_API_KEY || getStoredApiKey() || "";
19
+ const ensureApiKey = () => {
20
+ const API_KEY = getApiKey();
21
+ if (!API_KEY) {
22
+ console.error(chalk.red("\nMissing GEMINI_COMMIT_MESSAGE_API_KEY environment variable.\n"));
23
+ console.log("Please set your API key before running this command.\n");
24
+ console.log(chalk.yellow("How to fix this:\n"));
25
+ console.log(chalk.cyan("macOS / Linux:"));
26
+ console.log(" export GEMINI_COMMIT_MESSAGE_API_KEY=your_api_key_here\n");
27
+ console.log(chalk.cyan("Windows (PowerShell):"));
28
+ console.log(' setx GEMINI_COMMIT_MESSAGE_API_KEY "your_api_key_here"\n');
29
+ console.log(chalk.gray("After setting the variable, restart your terminal.\n"));
30
+ process.exit(1);
31
+ }
32
+ return API_KEY;
33
+ };
34
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
35
+ const getOperationLabel = ({ operation, target }) => {
36
+ if (operation === "diff-filename") {
37
+ return "generating diff filename";
38
+ }
39
+ if (operation === "diff-explanations") {
40
+ return target
41
+ ? `generating diff explanations for ${target}`
42
+ : "generating diff explanations";
43
+ }
44
+ return "generating commit message";
45
+ };
46
+ const getFallbackLabel = ({ operation, target }) => {
47
+ if (operation === "diff-filename") {
48
+ return "using fallback filename";
49
+ }
50
+ if (operation === "diff-explanations") {
51
+ return target
52
+ ? `using fallback explanations for ${target}`
53
+ : "using fallback explanations";
54
+ }
55
+ return "using fallback commit message";
56
+ };
57
+ const isRetryableError = (error) => {
58
+ if (!axios.isAxiosError(error)) {
59
+ return false;
60
+ }
61
+ const status = error.response?.status;
62
+ if (status === 429) {
63
+ return true;
64
+ }
65
+ if (status && status >= 500) {
66
+ return true;
67
+ }
68
+ return [
69
+ "ECONNABORTED",
70
+ "ETIMEDOUT",
71
+ "ECONNRESET",
72
+ "ENOTFOUND",
73
+ "EAI_AGAIN",
74
+ ].includes(error.code || "");
75
+ };
76
+ const getRetryDelay = (error, attempt) => {
77
+ const retryAfterHeader = error.response?.headers?.["retry-after"];
78
+ if (typeof retryAfterHeader === "string") {
79
+ const retryAfterSeconds = Number(retryAfterHeader);
80
+ if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) {
81
+ return retryAfterSeconds * 1000;
82
+ }
83
+ }
84
+ return Math.min(750 * 2 ** (attempt - 1), 3000);
85
+ };
86
+ const requestText = async (prompt, fallback, context) => {
87
+ const API_KEY = ensureApiKey();
88
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) {
89
+ try {
90
+ const response = await axios.post(API_URL, {
91
+ contents: [{ parts: [{ text: prompt }] }],
92
+ }, {
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ "x-goog-api-key": API_KEY,
96
+ },
97
+ });
98
+ return response.data.candidates?.[0]?.content?.parts?.[0]?.text?.trim()
99
+ ? response.data.candidates[0].content.parts[0].text.trim()
100
+ : fallback;
101
+ }
102
+ catch (error) {
103
+ if (axios.isAxiosError(error) && isRetryableError(error)) {
104
+ if (attempt < MAX_RETRIES) {
105
+ const delay = getRetryDelay(error, attempt);
106
+ const status = error.response?.status || error.code || "unknown";
107
+ console.log(chalk.yellow(`Gemini request retry ${attempt}/${MAX_RETRIES - 1} while ${getOperationLabel(context)} (${status}). Waiting ${Math.ceil(delay / 1000)}s...`));
108
+ await sleep(delay);
109
+ continue;
110
+ }
111
+ }
112
+ const status = axios.isAxiosError(error)
113
+ ? error.response?.status || error.code || "request-error"
114
+ : "request-error";
115
+ console.log(chalk.yellow(`Gemini request failed while ${getOperationLabel(context)} (${status}); ${getFallbackLabel(context)}.`));
116
+ return fallback;
117
+ }
118
+ }
119
+ return fallback;
120
+ };
121
+ const sanitizeTopic = (value) => {
122
+ const sanitized = value
123
+ .trim()
124
+ .toLowerCase()
125
+ .replace(/\.md$/i, "")
126
+ .replace(/[^a-z0-9-]+/g, "-")
127
+ .replace(/-+/g, "-")
128
+ .replace(/^-|-$/g, "");
129
+ const tokens = sanitized.split("-").filter(Boolean).slice(0, 3);
130
+ return tokens.join("-");
131
+ };
132
+ const parseDiffFileName = (value) => {
133
+ const lines = value
134
+ .split(/\r?\n/)
135
+ .map((line) => line.trim())
136
+ .filter(Boolean);
137
+ const typeLine = lines.find((line) => /^type\s*:/i.test(line));
138
+ const topicLine = lines.find((line) => /^topic\s*:/i.test(line));
139
+ const rawType = typeLine
140
+ ?.replace(/^type\s*:/i, "")
141
+ .trim()
142
+ .toLowerCase() || "";
143
+ const rawTopic = topicLine?.replace(/^topic\s*:/i, "").trim() || "";
144
+ const type = VALID_DIFF_FILE_TYPES.has(rawType) ? rawType : "chore";
145
+ const topic = sanitizeTopic(rawTopic) || "changes";
146
+ return `${type}-${topic}`;
147
+ };
148
+ export const generateCommitMessage = async (rawDiff, branchName) => {
149
+ const prompt = buildPrompt(rawDiff, branchName);
150
+ return requestText(prompt, "chore: update code", {
151
+ operation: "commit-message",
152
+ });
153
+ };
154
+ export const generateDiffExplanation = async (filePath, rawDiff, branchName) => {
155
+ const prompt = buildDiffExplanationPrompt(filePath, rawDiff, branchName);
156
+ return requestText(prompt, `Updates ${filePath} with the selected changes shown below.`, {
157
+ operation: "diff-explanations",
158
+ target: filePath,
159
+ });
160
+ };
161
+ const parseBatchDiffExplanations = (value, filePaths) => {
162
+ const result = new Map();
163
+ const blockRegex = /FILE:\s*(.+?)\r?\nEXPLANATION:\s*([\s\S]*?)\r?\nEND_FILE/g;
164
+ let match;
165
+ while ((match = blockRegex.exec(value)) !== null) {
166
+ const filePath = match[1].trim();
167
+ const explanation = match[2].trim();
168
+ if (filePaths.includes(filePath) && explanation.length > 0) {
169
+ result.set(filePath, explanation);
170
+ }
171
+ }
172
+ return result;
173
+ };
174
+ export const generateDiffExplanations = async (fileDiffs, branchName) => {
175
+ const prompt = buildBatchDiffExplanationsPrompt(fileDiffs, branchName);
176
+ const response = await requestText(prompt, "", {
177
+ operation: "diff-explanations",
178
+ target: `${fileDiffs.length} files`,
179
+ });
180
+ const explanations = parseBatchDiffExplanations(response, fileDiffs.map(({ filePath }) => filePath));
181
+ if (explanations.size === fileDiffs.length) {
182
+ return explanations;
183
+ }
184
+ if (response.trim().length > 0) {
185
+ console.log(chalk.yellow("Gemini returned incomplete diff explanations; using fallback text for missing files."));
186
+ }
187
+ fileDiffs.forEach(({ filePath }) => {
188
+ if (!explanations.has(filePath)) {
189
+ explanations.set(filePath, `Updates ${filePath} with the selected changes shown below.`);
190
+ }
191
+ });
192
+ return explanations;
193
+ };
194
+ export const generateDiffFileName = async (rawDiff) => {
195
+ const prompt = buildDiffFileNamePrompt(rawDiff);
196
+ const rawName = await requestText(prompt, "type: chore\ntopic: changes", {
197
+ operation: "diff-filename",
198
+ });
199
+ return parseDiffFileName(rawName);
200
+ };
201
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/llm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,gCAAgC,EAChC,0BAA0B,EAC1B,uBAAuB,EACvB,WAAW,GACZ,MAAM,aAAa,CAAC;AA4BrB,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,MAAM;IACN,KAAK;IACL,UAAU;IACV,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;CACT,CAAC,CAAC;AAEH,MAAM,OAAO,GACX,sFAAsF,CAAC;AACzF,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,MAAM,SAAS,GAAG,GAAG,EAAE,CACrB,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,eAAe,EAAE,IAAI,EAAE,CAAC;AAEvE,MAAM,YAAY,GAAG,GAAG,EAAE;IACxB,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC;IAE5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CACP,iEAAiE,CAClE,CACF,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;QAEtE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAEhD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAC;QAE1E,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAC;QAE1E,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,IAAI,CAAC,sDAAsD,CAAC,CACnE,CAAC;QAEF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,iBAAiB,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAkB,EAAE,EAAE;IAClE,IAAI,SAAS,KAAK,eAAe,EAAE,CAAC;QAClC,OAAO,0BAA0B,CAAC;IACpC,CAAC;IAED,IAAI,SAAS,KAAK,mBAAmB,EAAE,CAAC;QACtC,OAAO,MAAM;YACX,CAAC,CAAC,oCAAoC,MAAM,EAAE;YAC9C,CAAC,CAAC,8BAA8B,CAAC;IACrC,CAAC;IAED,OAAO,2BAA2B,CAAC;AACrC,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAkB,EAAE,EAAE;IACjE,IAAI,SAAS,KAAK,eAAe,EAAE,CAAC;QAClC,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,IAAI,SAAS,KAAK,mBAAmB,EAAE,CAAC;QACtC,OAAO,MAAM;YACX,CAAC,CAAC,mCAAmC,MAAM,EAAE;YAC7C,CAAC,CAAC,6BAA6B,CAAC;IACpC,CAAC;IAED,OAAO,+BAA+B,CAAC;AACzC,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,CAAC,KAAc,EAAE,EAAE;IAC1C,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC;IAEtC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,MAAM,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,cAAc;QACd,WAAW;QACX,YAAY;QACZ,WAAW;QACX,WAAW;KACZ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAiB,EAAE,OAAe,EAAE,EAAE;IAC3D,MAAM,gBAAgB,GAAG,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,aAAa,CAAC,CAAC;IAElE,IAAI,OAAO,gBAAgB,KAAK,QAAQ,EAAE,CAAC;QACzC,MAAM,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAEnD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;YAC9D,OAAO,iBAAiB,GAAG,IAAI,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,KAAK,EACvB,MAAc,EACd,QAAgB,EAChB,OAAuB,EACvB,EAAE;IACF,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAE/B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAC/B,OAAO,EACP;gBACE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;aAC1C,EACD;gBACE,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,gBAAgB,EAAE,OAAO;iBAC1B;aACF,CACF,CAAC;YAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;gBACrE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAQ,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC,IAAK,CAAC,IAAI,EAAE;gBAC7D,CAAC,CAAC,QAAQ,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzD,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC;oBAC1B,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;oBAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC;oBACjE,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,MAAM,CACV,wBAAwB,OAAO,IAAI,WAAW,GAAG,CAAC,UAAU,iBAAiB,CAAC,OAAO,CAAC,KAAK,MAAM,cAAc,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAC7I,CACF,CAAC;oBACF,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;oBACnB,SAAS;gBACX,CAAC;YACH,CAAC;YAED,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC;gBACtC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,eAAe;gBACzD,CAAC,CAAC,eAAe,CAAC;YACpB,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,MAAM,CACV,+BAA+B,iBAAiB,CAAC,OAAO,CAAC,KAAK,MAAM,MAAM,gBAAgB,CAAC,OAAO,CAAC,GAAG,CACvG,CACF,CAAC;YACF,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAa,EAAE,EAAE;IACtC,MAAM,SAAS,GAAG,KAAK;SACpB,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAEzB,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEhE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAE,EAAE;IAC1C,MAAM,KAAK,GAAG,KAAK;SAChB,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEjE,MAAM,OAAO,GACX,QAAQ;QACN,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SAC1B,IAAI,EAAE;SACN,WAAW,EAAE,IAAI,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAG,SAAS,EAAE,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IAEpE,MAAM,IAAI,GAAG,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACpE,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC;IAEnD,OAAO,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC;AAC5B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EACxC,OAAe,EACf,UAAkB,EACD,EAAE;IACnB,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEhD,OAAO,WAAW,CAAC,MAAM,EAAE,oBAAoB,EAAE;QAC/C,SAAS,EAAE,gBAAgB;KAC5B,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC1C,QAAgB,EAChB,OAAe,EACf,UAAkB,EACD,EAAE;IACnB,MAAM,MAAM,GAAG,0BAA0B,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAEzE,OAAO,WAAW,CAChB,MAAM,EACN,WAAW,QAAQ,yCAAyC,EAC5D;QACE,SAAS,EAAE,mBAAmB;QAC9B,MAAM,EAAE,QAAQ;KACjB,CACF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,0BAA0B,GAAG,CAAC,KAAa,EAAE,SAAmB,EAAE,EAAE;IACxE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,UAAU,GACd,2DAA2D,CAAC;IAC9D,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEpC,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3D,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,EAC3C,SAA0B,EAC1B,UAAkB,EACY,EAAE;IAChC,MAAM,MAAM,GAAG,gCAAgC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACvE,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,EAAE,EAAE;QAC7C,SAAS,EAAE,mBAAmB;QAC9B,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,QAAQ;KACpC,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,0BAA0B,CAC7C,QAAQ,EACR,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,CAC1C,CAAC;IAEF,IAAI,YAAY,CAAC,IAAI,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC3C,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,MAAM,CACV,sFAAsF,CACvF,CACF,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;QACjC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,YAAY,CAAC,GAAG,CACd,QAAQ,EACR,WAAW,QAAQ,yCAAyC,CAC7D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,OAAe,EACE,EAAE;IACnB,MAAM,MAAM,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,6BAA6B,EAAE;QACvE,SAAS,EAAE,eAAe;KAC3B,CAAC,CAAC;IAEH,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare const buildPrompt: (diff: string, branchName: string) => string;
2
+ export declare const buildDiffExplanationPrompt: (filePath: string, diff: string, branchName: string) => string;
3
+ export declare const buildBatchDiffExplanationsPrompt: (fileDiffs: Array<{
4
+ filePath: string;
5
+ diff: string;
6
+ }>, branchName: string) => string;
7
+ export declare const buildDiffFileNamePrompt: (diff: string) => string;
@@ -0,0 +1,127 @@
1
+ export const buildPrompt = (diff, branchName) => `
2
+ CRITICAL INSTRUCTIONS - READ CAREFULLY:
3
+ You are an expert Git commit message writer. You MUST follow ALL these rules:
4
+
5
+ 1. FORMAT: Use Conventional Commits format: <type>(<scope>/<branch_name>): <description>
6
+ - type: MUST be one of: feat, fix, refactor, chore, docs, style, test, perf, bugfix
7
+ - scope: Should be the module/file affected (e.g., "auth", "api", "ui", "config", "feature")
8
+ - branch_name: The current Git branch you are on. It is "${branchName || "unknown"}"
9
+ - description: Clear, imperative description in present tense
10
+
11
+ 2. DESCRIPTION REQUIREMENTS:
12
+ - Start with an imperative verb (add, fix, remove, update, refactor, etc.)
13
+ - Be specific about what changed
14
+ - Keep the first line (the summary) under 72 characters total
15
+ - NO trailing punctuation on the summary line
16
+ - NO emojis ever
17
+ - MUST be a complete sentence
18
+ - Group related file changes together. If you change a service, controller, and route for the same feature (e.g. "auth"), summarize the collective change in just ONE line.
19
+ - Do NOT list every file that changed.
20
+
21
+ 3. MESSAGE STRUCTURE:
22
+ - The first line must be the summary: type(scope/${branchName || "unknown"}): description
23
+ - If there is ONLY ONE logical feature/fix being made (even if across multiple files), the commit message MUST BE EXACTLY ONE LINE. Do not use bullet points.
24
+ - ONLY if there are entirely UNRELATED distinct features changed at once, you may add a blank line after the summary, followed by a bulleted list summarizing those distinct features.
25
+ - Example 1 (Single logical change spanning multiple files): "feat(auth/${branchName || "unknown"}): implement jwt authentication flow"
26
+ - Example 2 (Unrelated distinct changes):
27
+ feat(core/${branchName || "unknown"}): update foundational systems
28
+
29
+ - handle null response in user endpoint
30
+ - add rate limiting to requests
31
+
32
+ 4. QUALITY CHECKS - YOUR OUTPUT MUST PASS:
33
+ - Contains opening and closing parentheses
34
+ - Has a colon after the parentheses
35
+ - Description exists and is not empty
36
+ - Summary line total length ≤ 72 characters
37
+ - No markdown formatting
38
+ - No code blocks
39
+ - No explanations or notes
40
+
41
+ 5. FAILURE MODE:
42
+ - If you cannot generate a proper message, return exactly: "chore: update code"
43
+
44
+ YOUR TASK:
45
+ Analyze this git diff and generate exactly ONE proper commit message following all rules above.
46
+
47
+ Git diff:
48
+ ${diff}
49
+
50
+ Commit message:
51
+ `.trim();
52
+ export const buildDiffExplanationPrompt = (filePath, diff, branchName) => `
53
+ You are explaining a staged git diff to a developer.
54
+
55
+ Rules:
56
+ - Explain only the changes shown for this file
57
+ - Write 2 to 4 short sentences
58
+ - Be concrete and precise
59
+ - Focus on what changed and why it matters
60
+ - No markdown headings
61
+ - No bullet points
62
+ - No code fences
63
+ - Do not restate the entire diff line by line
64
+ - The current branch is "${branchName || "unknown"}"
65
+
66
+ File:
67
+ ${filePath}
68
+
69
+ Diff:
70
+ ${diff}
71
+
72
+ Explanation:
73
+ `.trim();
74
+ export const buildBatchDiffExplanationsPrompt = (fileDiffs, branchName) => `
75
+ You are explaining staged git diffs to a developer.
76
+
77
+ Rules:
78
+ - Write exactly one explanation block for each file shown below
79
+ - Keep each explanation to 2 to 4 short sentences
80
+ - Be concrete and precise
81
+ - Focus on what changed and why it matters
82
+ - No markdown headings
83
+ - No bullet points
84
+ - No code fences
85
+ - Do not restate the diff line by line
86
+ - Use the exact file path provided
87
+ - Return blocks in this exact format:
88
+ FILE: <exact file path>
89
+ EXPLANATION: <plain text explanation>
90
+ END_FILE
91
+ - Return nothing except these blocks
92
+ - The current branch is "${branchName || "unknown"}"
93
+
94
+ Files and diffs:
95
+ ${fileDiffs
96
+ .map(({ filePath, diff }) => `
97
+ FILE: ${filePath}
98
+ DIFF:
99
+ ${diff}
100
+ END_DIFF
101
+ `.trim())
102
+ .join("\n\n")}
103
+
104
+ Result:
105
+ `.trim();
106
+ export const buildDiffFileNamePrompt = (diff) => `
107
+ Classify these staged changes and produce a concise structured filename basis.
108
+
109
+ Rules:
110
+ - Return exactly two lines and nothing else
111
+ - First line format: type: <value>
112
+ - Second line format: topic: <value>
113
+ - type must be one of: feat, fix, refactor, chore, docs, style, test, perf, bugfix
114
+ - topic must be 1 to 3 lowercase hyphen-separated words
115
+ - topic must describe the actual change area
116
+ - Do not include branch names
117
+ - Do not include dates
118
+ - Do not include the .md extension
119
+ - Do not include quotes, markdown, bullets, or explanations
120
+ - Avoid generic filler words like proposed, report, diff, and changes unless absolutely necessary
121
+
122
+ Git diff:
123
+ ${diff}
124
+
125
+ Result:
126
+ `.trim();
127
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/prompt.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,UAAkB,EAAU,EAAE,CACtE;;;;;;;8DAO4D,UAAU,IAAI,SAAS;;;;;;;;;;;;;;sDAc/B,UAAU,IAAI,SAAS;;;6EAGA,UAAU,IAAI,SAAS;;YAExF,UAAU,IAAI,SAAS;;;;;;;;;;;;;;;;;;;;;EAqBjC,IAAI;;;CAGL,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,CAAC,MAAM,0BAA0B,GAAG,CACxC,QAAgB,EAChB,IAAY,EACZ,UAAkB,EACV,EAAE,CACV;;;;;;;;;;;;2BAYyB,UAAU,IAAI,SAAS;;;EAGhD,QAAQ;;;EAGR,IAAI;;;CAGL,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAC9C,SAAoD,EACpD,UAAkB,EACV,EAAE,CACV;;;;;;;;;;;;;;;;;;2BAkByB,UAAU,IAAI,SAAS;;;EAGhD,SAAS;KACR,GAAG,CACF,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACpB,QAAQ;;EAEd,IAAI;;CAEL,CAAC,IAAI,EAAE,CACL;KACA,IAAI,CAAC,MAAM,CAAC;;;CAGd,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,IAAY,EACJ,EAAE,CACV;;;;;;;;;;;;;;;;;EAiBA,IAAI;;;CAGL,CAAC,IAAI,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@amnesia2k/git-aic",
3
+ "type": "module",
4
+ "version": "2.1.0",
5
+ "description": "Git AIC",
6
+ "main": "dist/bin/cli.js",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "private": false,
11
+ "bin": {
12
+ "git-aic": "dist/bin/cli.js"
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "bin/",
17
+ "src/",
18
+ "README.md",
19
+ "package.json"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.build.json",
23
+ "dev": "tsx bin/cli.ts",
24
+ "commit": "tsx bin/cli.ts"
25
+ },
26
+ "keywords": [
27
+ "git",
28
+ "aic",
29
+ "ai",
30
+ "git-aic"
31
+ ],
32
+ "author": "Olatilewa Olatoye",
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "@types/conf": "^2.1.0",
37
+ "@types/node": "^25.5.0",
38
+ "ts-node": "^10.9.2",
39
+ "tsx": "^4.21.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.1.0",
44
+ "axios": "^1.13.6",
45
+ "chalk": "^5.6.2",
46
+ "cli-loaders": "^3.0.0",
47
+ "commander": "^14.0.3",
48
+ "conf": "^15.1.0",
49
+ "simple-git": "^3.32.3"
50
+ }
51
+ }
package/src/config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import Conf from "conf";
2
+
3
+ // @ts-ignore
4
+ export const config = new Conf({ projectName: "git-aic" });
5
+
6
+ export const getStoredApiKey = () => config.get("apiKey") as string | undefined;
7
+ export const setStoredApiKey = (key: string) => config.set("apiKey", key);
package/src/git.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { simpleGit } from "simple-git";
2
+ import type { SimpleGit } from "simple-git";
3
+ import { readFileSync } from "fs";
4
+
5
+ const git: SimpleGit = simpleGit();
6
+
7
+ export interface FileDiff {
8
+ filePath: string;
9
+ diff: string;
10
+ }
11
+
12
+ export interface HeadCommitInfo {
13
+ short: string;
14
+ full: string;
15
+ }
16
+
17
+ export const getGitDiff = async () => {
18
+ try {
19
+ await git.raw(["config", "core.autocrlf", "true"]);
20
+ const diff = await git.diff(["--cached", "--ignore-space-at-eol"]);
21
+ return diff || "";
22
+ } catch (error) {
23
+ console.error(error);
24
+ return "";
25
+ }
26
+ };
27
+
28
+ export const getBranchName = async () => {
29
+ try {
30
+ const status = await git.status();
31
+ return status.current || "";
32
+ } catch (error) {
33
+ console.error(error);
34
+ return "";
35
+ }
36
+ };
37
+
38
+ export const getStagedFileDiffs = async (): Promise<FileDiff[]> => {
39
+ try {
40
+ const status = await git.status();
41
+ const fileDiffs = await Promise.all(
42
+ status.staged.map(async (filePath) => {
43
+ const diff = await git.diff([
44
+ "--cached",
45
+ "--ignore-space-at-eol",
46
+ "--",
47
+ filePath,
48
+ ]);
49
+
50
+ return {
51
+ filePath,
52
+ diff: diff || "",
53
+ };
54
+ }),
55
+ );
56
+
57
+ return fileDiffs.filter((entry) => entry.diff.trim().length > 0);
58
+ } catch (error) {
59
+ console.error(error);
60
+ return [];
61
+ }
62
+ };
63
+
64
+ const createUntrackedFileDiff = (filePath: string) => {
65
+ try {
66
+ const contents = readFileSync(filePath, "utf8");
67
+ const lines = contents.split(/\r?\n/);
68
+ const body = lines.map((line) => `+${line}`).join("\n");
69
+
70
+ return [
71
+ `diff --git a/${filePath} b/${filePath}`,
72
+ "new file mode 100644",
73
+ "index 0000000..0000000",
74
+ "--- /dev/null",
75
+ `+++ b/${filePath}`,
76
+ `@@ -0,0 +1,${lines.length} @@`,
77
+ body,
78
+ ].join("\n");
79
+ } catch (error) {
80
+ console.error(error);
81
+ return "";
82
+ }
83
+ };
84
+
85
+ export const getSelectedFileDiffs = async (
86
+ filePaths: string[],
87
+ ): Promise<FileDiff[]> => {
88
+ try {
89
+ const status = await git.status();
90
+ const fileDiffs = await Promise.all(
91
+ filePaths.map(async (filePath) => {
92
+ if (status.not_added.includes(filePath)) {
93
+ return {
94
+ filePath,
95
+ diff: createUntrackedFileDiff(filePath),
96
+ };
97
+ }
98
+
99
+ const diff = await git.diff([
100
+ "HEAD",
101
+ "--ignore-space-at-eol",
102
+ "--",
103
+ filePath,
104
+ ]);
105
+
106
+ return {
107
+ filePath,
108
+ diff: diff || "",
109
+ };
110
+ }),
111
+ );
112
+
113
+ return fileDiffs.filter((entry) => entry.diff.trim().length > 0);
114
+ } catch (error) {
115
+ console.error(error);
116
+ return [];
117
+ }
118
+ };
119
+
120
+ export const getSelectedFilesDiff = async (filePaths: string[]) => {
121
+ const fileDiffs = await getSelectedFileDiffs(filePaths);
122
+ return fileDiffs.map(({ diff }) => diff).join("\n\n");
123
+ };
124
+
125
+ export const getHeadCommitInfo = async (): Promise<HeadCommitInfo> => {
126
+ try {
127
+ const full = (await git.revparse(["HEAD"])).trim();
128
+ const short = (await git.revparse(["--short", "HEAD"])).trim();
129
+
130
+ return {
131
+ short,
132
+ full,
133
+ };
134
+ } catch (error) {
135
+ console.error(error);
136
+ return {
137
+ short: "unknown",
138
+ full: "unknown",
139
+ };
140
+ }
141
+ };