lovecode-ai 0.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.
- package/README.md +175 -0
- package/bin/lovecode.js +5 -0
- package/dist/browser-UA4QOMPS.js +37 -0
- package/dist/chunk-FMT77EJQ.js +160 -0
- package/dist/chunk-G7VQGYJW.js +858 -0
- package/dist/chunk-IVAMLKMS.js +194 -0
- package/dist/chunk-LKUWOZUZ.js +0 -0
- package/dist/chunk-MOZHR2QY.js +412 -0
- package/dist/git-TBOGPTY4.js +70 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7783 -0
- package/dist/playwright-N7OAVW2N.js +36 -0
- package/dist/registry-MW5ISDO7.js +32 -0
- package/dist/theme-ZRZYRB2Q.js +18 -0
- package/package.json +75 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
// src/git/commands.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
function exec(cmd, cwd) {
|
|
6
|
+
try {
|
|
7
|
+
const stdout = execSync(cmd, {
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
cwd,
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
timeout: 15e3
|
|
12
|
+
});
|
|
13
|
+
return { stdout: stdout.trim(), stderr: "", failed: false };
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const e = err;
|
|
16
|
+
const toStr = (v) => {
|
|
17
|
+
if (!v) return "";
|
|
18
|
+
if (typeof v === "string") return v;
|
|
19
|
+
return v.toString("utf-8");
|
|
20
|
+
};
|
|
21
|
+
const stdout = toStr(e.stdout).trim();
|
|
22
|
+
const stderr = toStr(e.stderr).trim();
|
|
23
|
+
return { stdout, stderr: stderr || e.message, failed: true };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isGitAvailable() {
|
|
27
|
+
try {
|
|
28
|
+
execSync("git --version", { encoding: "utf-8", stdio: "pipe" });
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getGitRoot(cwd) {
|
|
35
|
+
const result = exec("git rev-parse --show-toplevel", cwd);
|
|
36
|
+
if (result.stderr) return null;
|
|
37
|
+
return result.stdout;
|
|
38
|
+
}
|
|
39
|
+
function isRepo(cwd) {
|
|
40
|
+
return getGitRoot(cwd) !== null;
|
|
41
|
+
}
|
|
42
|
+
function getCurrentBranch(cwd) {
|
|
43
|
+
const result = exec("git rev-parse --abbrev-ref HEAD", cwd);
|
|
44
|
+
return result.stdout || "unknown";
|
|
45
|
+
}
|
|
46
|
+
function getStatus(cwd) {
|
|
47
|
+
const root = getGitRoot(cwd);
|
|
48
|
+
const defaultCwd = root || cwd || process.cwd();
|
|
49
|
+
const branchStr = exec("git status --branch --porcelain=v2", defaultCwd);
|
|
50
|
+
const branch = getCurrentBranch(defaultCwd);
|
|
51
|
+
let behind = 0;
|
|
52
|
+
let ahead = 0;
|
|
53
|
+
const behindMatch = branchStr.stdout.match(/# branch\.ab \+(\d+) \-(\d+)/);
|
|
54
|
+
if (behindMatch) {
|
|
55
|
+
ahead = parseInt(behindMatch[1], 10);
|
|
56
|
+
behind = parseInt(behindMatch[2], 10);
|
|
57
|
+
}
|
|
58
|
+
const staged = [];
|
|
59
|
+
const unstaged = [];
|
|
60
|
+
const untracked = [];
|
|
61
|
+
const conflicted = [];
|
|
62
|
+
for (const line of branchStr.stdout.split("\n")) {
|
|
63
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
64
|
+
const parts = line.split(" ");
|
|
65
|
+
const xy = parts[0].slice(0, 2);
|
|
66
|
+
const isStaged = xy[0] !== "." && xy[0] !== " ";
|
|
67
|
+
const isUnstaged = xy[1] !== "." && xy[1] !== " ";
|
|
68
|
+
const path3 = parts[parts.length - 1];
|
|
69
|
+
if (xy[0] === "u" || xy[1] === "u") {
|
|
70
|
+
conflicted.push(path3);
|
|
71
|
+
} else {
|
|
72
|
+
if (isStaged) staged.push(path3);
|
|
73
|
+
if (isUnstaged) unstaged.push(path3);
|
|
74
|
+
}
|
|
75
|
+
} else if (line.startsWith("? ")) {
|
|
76
|
+
untracked.push(line.slice(2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
branch,
|
|
81
|
+
behind,
|
|
82
|
+
ahead,
|
|
83
|
+
staged,
|
|
84
|
+
unstaged,
|
|
85
|
+
untracked,
|
|
86
|
+
conflicted,
|
|
87
|
+
clean: staged.length === 0 && unstaged.length === 0 && untracked.length === 0 && conflicted.length === 0
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function stageAll(cwd) {
|
|
91
|
+
const root = getGitRoot(cwd);
|
|
92
|
+
const result = exec("git add -A", root || cwd);
|
|
93
|
+
return !result.failed;
|
|
94
|
+
}
|
|
95
|
+
function stageFiles(files, cwd) {
|
|
96
|
+
const root = getGitRoot(cwd);
|
|
97
|
+
if (!root) return false;
|
|
98
|
+
const result = exec(`git add -- ${files.map((f) => `"${f}"`).join(" ")}`, root);
|
|
99
|
+
return !result.failed;
|
|
100
|
+
}
|
|
101
|
+
function commit(message, cwd) {
|
|
102
|
+
const root = getGitRoot(cwd);
|
|
103
|
+
if (!root) return { success: false, output: "Not a git repository" };
|
|
104
|
+
const result = exec(`git commit -m "${message.replace(/"/g, '\\"')}"`, root);
|
|
105
|
+
const output = (result.stdout + "\n" + result.stderr).toLowerCase();
|
|
106
|
+
if (output.includes("nothing to commit")) {
|
|
107
|
+
return { success: false, output: "Nothing to commit. Working tree clean." };
|
|
108
|
+
}
|
|
109
|
+
if (result.failed) {
|
|
110
|
+
return { success: false, output: result.stderr || result.stdout };
|
|
111
|
+
}
|
|
112
|
+
const hashMatch = result.stdout.match(/\[[\w-]+ ([a-f0-9]+)\]/);
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
hash: hashMatch?.[1],
|
|
116
|
+
output: result.stdout
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function getDiff(cwd) {
|
|
120
|
+
const root = getGitRoot(cwd);
|
|
121
|
+
if (!root) return "";
|
|
122
|
+
const result = exec("git diff --staged", root);
|
|
123
|
+
if (result.stdout) return result.stdout;
|
|
124
|
+
const unstaged = exec("git diff", root);
|
|
125
|
+
return unstaged.stdout;
|
|
126
|
+
}
|
|
127
|
+
function getStagedDiff(cwd) {
|
|
128
|
+
const root = getGitRoot(cwd);
|
|
129
|
+
if (!root) return "";
|
|
130
|
+
const result = exec("git diff --cached", root);
|
|
131
|
+
return result.stdout;
|
|
132
|
+
}
|
|
133
|
+
function getUnstagedDiff(cwd) {
|
|
134
|
+
const root = getGitRoot(cwd);
|
|
135
|
+
if (!root) return "";
|
|
136
|
+
const result = exec("git diff", root);
|
|
137
|
+
return result.stdout;
|
|
138
|
+
}
|
|
139
|
+
function getFullDiff(cwd) {
|
|
140
|
+
const root = getGitRoot(cwd);
|
|
141
|
+
if (!root) return "";
|
|
142
|
+
const result = exec("git diff HEAD", root);
|
|
143
|
+
return result.stdout;
|
|
144
|
+
}
|
|
145
|
+
function getLog(count = 10, cwd) {
|
|
146
|
+
const root = getGitRoot(cwd);
|
|
147
|
+
if (!root) return [];
|
|
148
|
+
const result = exec(`git log --oneline --format="%H|%ai|%an|%s" --max-count=${count}`, root);
|
|
149
|
+
if (!result.stdout) return [];
|
|
150
|
+
return result.stdout.split("\n").map((line) => {
|
|
151
|
+
const [hash, date, author, ...msgParts] = line.split("|");
|
|
152
|
+
return { hash, date, author, message: msgParts.join("|") };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function getBranches(cwd) {
|
|
156
|
+
const root = getGitRoot(cwd);
|
|
157
|
+
if (!root) return [];
|
|
158
|
+
const result = exec("git branch -v", root);
|
|
159
|
+
if (!result.stdout) return [];
|
|
160
|
+
const branches = [];
|
|
161
|
+
for (const line of result.stdout.split("\n")) {
|
|
162
|
+
const trimmed = line.trim();
|
|
163
|
+
if (!trimmed) continue;
|
|
164
|
+
const current = line.startsWith("* ");
|
|
165
|
+
const parts = trimmed.replace(/^\*\s*/, "").split(/\s+/);
|
|
166
|
+
const name = parts[0];
|
|
167
|
+
branches.push({ current, name, behind: 0, ahead: 0 });
|
|
168
|
+
}
|
|
169
|
+
return branches;
|
|
170
|
+
}
|
|
171
|
+
function createBranch(name, cwd) {
|
|
172
|
+
const root = getGitRoot(cwd);
|
|
173
|
+
if (!root) return false;
|
|
174
|
+
const result = exec(`git checkout -b "${name}"`, root);
|
|
175
|
+
return !result.failed;
|
|
176
|
+
}
|
|
177
|
+
function switchBranch(name, cwd) {
|
|
178
|
+
const root = getGitRoot(cwd);
|
|
179
|
+
if (!root) return { success: false, output: "Not a git repository" };
|
|
180
|
+
const result = exec(`git checkout "${name}"`, root);
|
|
181
|
+
if (result.failed) return { success: false, output: result.stderr || result.stdout };
|
|
182
|
+
return { success: true, output: result.stdout || result.stderr };
|
|
183
|
+
}
|
|
184
|
+
function deleteBranch(name, force = false, cwd) {
|
|
185
|
+
const root = getGitRoot(cwd);
|
|
186
|
+
if (!root) return { success: false, output: "Not a git repository" };
|
|
187
|
+
const flag = force ? "-D" : "-d";
|
|
188
|
+
const result = exec(`git branch ${flag} "${name}"`, root);
|
|
189
|
+
if (result.failed) return { success: false, output: result.stderr || result.stdout };
|
|
190
|
+
return { success: true, output: result.stdout || result.stderr };
|
|
191
|
+
}
|
|
192
|
+
function cleanupMergedBranches(cwd) {
|
|
193
|
+
const root = getGitRoot(cwd);
|
|
194
|
+
if (!root) return [];
|
|
195
|
+
const current = getCurrentBranch(root);
|
|
196
|
+
const result = exec("git branch --merged", root);
|
|
197
|
+
const deleted = [];
|
|
198
|
+
for (const line of result.stdout.split("\n")) {
|
|
199
|
+
const name = line.trim().replace(/^\*\s*/, "");
|
|
200
|
+
if (name && name !== current && name !== "main" && name !== "master") {
|
|
201
|
+
const del = deleteBranch(name, false, root);
|
|
202
|
+
if (del.success) deleted.push(name);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return deleted;
|
|
206
|
+
}
|
|
207
|
+
function getPRDiff(baseBranch = "main", cwd) {
|
|
208
|
+
const root = getGitRoot(cwd);
|
|
209
|
+
if (!root) return "";
|
|
210
|
+
const result = exec(`git diff ${baseBranch}...HEAD`, root);
|
|
211
|
+
return result.stdout;
|
|
212
|
+
}
|
|
213
|
+
function getPRLog(baseBranch = "main", cwd) {
|
|
214
|
+
const root = getGitRoot(cwd);
|
|
215
|
+
if (!root) return [];
|
|
216
|
+
const result = exec(`git log ${baseBranch}..HEAD --oneline --format="%H|%ai|%an|%s"`, root);
|
|
217
|
+
if (!result.stdout) return [];
|
|
218
|
+
return result.stdout.split("\n").map((line) => {
|
|
219
|
+
const [hash, date, author, ...msgParts] = line.split("|");
|
|
220
|
+
return { hash, date, author, message: msgParts.join("|") };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function hasConflicts(cwd) {
|
|
224
|
+
const root = getGitRoot(cwd);
|
|
225
|
+
if (!root) return false;
|
|
226
|
+
const result = exec("git diff --name-only --diff-filter=U", root);
|
|
227
|
+
return result.stdout.trim().length > 0;
|
|
228
|
+
}
|
|
229
|
+
function getConflictFiles(cwd) {
|
|
230
|
+
const root = getGitRoot(cwd);
|
|
231
|
+
if (!root) return [];
|
|
232
|
+
const result = exec("git diff --name-only --diff-filter=U", root);
|
|
233
|
+
return result.stdout ? result.stdout.split("\n").filter(Boolean) : [];
|
|
234
|
+
}
|
|
235
|
+
function getConflictMarkers(filePath, cwd) {
|
|
236
|
+
const root = getGitRoot(cwd);
|
|
237
|
+
const fullPath = root ? path.join(root, filePath) : filePath;
|
|
238
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
239
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
240
|
+
const lines = content.split("\n");
|
|
241
|
+
const markers = [];
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const l = lines[i];
|
|
244
|
+
if (l.startsWith("<<<<<<<")) markers.push({ line: i + 1, type: "start" });
|
|
245
|
+
else if (l.startsWith("=======")) markers.push({ line: i + 1, type: "separator" });
|
|
246
|
+
else if (l.startsWith(">>>>>>>")) markers.push({ line: i + 1, type: "end" });
|
|
247
|
+
}
|
|
248
|
+
return markers;
|
|
249
|
+
}
|
|
250
|
+
function formatStatus(status) {
|
|
251
|
+
const lines = [
|
|
252
|
+
`Branch: ${status.branch}`,
|
|
253
|
+
status.clean ? "(clean)" : ""
|
|
254
|
+
].filter(Boolean);
|
|
255
|
+
if (status.behind || status.ahead) {
|
|
256
|
+
lines.push(` ${status.behind > 0 ? `${status.behind} behind` : ""}${status.behind && status.ahead ? ", " : ""}${status.ahead > 0 ? `${status.ahead} ahead` : ""}`);
|
|
257
|
+
}
|
|
258
|
+
for (const [label, items] of [["Staged", status.staged], ["Unstaged", status.unstaged], ["Untracked", status.untracked], ["Conflicted", status.conflicted]]) {
|
|
259
|
+
if (items.length > 0) {
|
|
260
|
+
lines.push(` ${label}:`);
|
|
261
|
+
for (const item of items.slice(0, 20)) {
|
|
262
|
+
lines.push(` ${item}`);
|
|
263
|
+
}
|
|
264
|
+
if (items.length > 20) lines.push(` ... and ${items.length - 20} more`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
function formatBranches(branches) {
|
|
270
|
+
return branches.map((b) => `${b.current ? "* " : " "}${b.name}`).join("\n");
|
|
271
|
+
}
|
|
272
|
+
function formatLog(log) {
|
|
273
|
+
return log.map((e) => `${e.hash.slice(0, 8)} ${e.date.slice(0, 10)} ${e.author} ${e.message}`).join("\n");
|
|
274
|
+
}
|
|
275
|
+
function abbreviateDiff(diff, maxLines = 100) {
|
|
276
|
+
const lines = diff.split("\n");
|
|
277
|
+
if (lines.length <= maxLines) return diff;
|
|
278
|
+
return lines.slice(0, maxLines).join("\n") + `
|
|
279
|
+
... (${lines.length - maxLines} more lines)`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/ai/registry.ts
|
|
283
|
+
import chalk2 from "chalk";
|
|
284
|
+
|
|
285
|
+
// src/utils/logger.ts
|
|
286
|
+
import chalk from "chalk";
|
|
287
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
288
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
289
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
290
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
291
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
292
|
+
return LogLevel2;
|
|
293
|
+
})(LogLevel || {});
|
|
294
|
+
var LOG_LEVEL = (process.env.LOVECODE_LOG_LEVEL || "info").toUpperCase();
|
|
295
|
+
var currentLevel = LogLevel[LOG_LEVEL] ?? 1 /* INFO */;
|
|
296
|
+
var Logger = class {
|
|
297
|
+
static debug(...args) {
|
|
298
|
+
if (currentLevel <= 0 /* DEBUG */) {
|
|
299
|
+
console.error(chalk.dim("[debug]"), ...args);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
static info(...args) {
|
|
303
|
+
if (currentLevel <= 1 /* INFO */) {
|
|
304
|
+
console.error(chalk.blue("[info]"), ...args);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
static warn(...args) {
|
|
308
|
+
if (currentLevel <= 2 /* WARN */) {
|
|
309
|
+
console.error(chalk.yellow("[warn]"), ...args);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
static error(...args) {
|
|
313
|
+
if (currentLevel <= 3 /* ERROR */) {
|
|
314
|
+
console.error(chalk.red("[error]"), ...args);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/ai/ollama.ts
|
|
320
|
+
var OllamaProvider = class {
|
|
321
|
+
name = "ollama";
|
|
322
|
+
async chat(messages, config) {
|
|
323
|
+
const baseUrl = config.baseUrl || "http://localhost:11434";
|
|
324
|
+
Logger.debug(`Ollama chat: ${config.model} (${messages.length} messages)`);
|
|
325
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json" },
|
|
328
|
+
body: JSON.stringify({
|
|
329
|
+
model: config.model,
|
|
330
|
+
messages,
|
|
331
|
+
stream: false,
|
|
332
|
+
options: {
|
|
333
|
+
temperature: config.temperature ?? 0.2,
|
|
334
|
+
num_predict: config.maxTokens ?? 4096
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
});
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
|
340
|
+
}
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
return data.message?.content ?? "";
|
|
343
|
+
}
|
|
344
|
+
async *stream(messages, config) {
|
|
345
|
+
const baseUrl = config.baseUrl || "http://localhost:11434";
|
|
346
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
model: config.model,
|
|
351
|
+
messages,
|
|
352
|
+
stream: true,
|
|
353
|
+
options: {
|
|
354
|
+
temperature: config.temperature ?? 0.2,
|
|
355
|
+
num_predict: config.maxTokens ?? 4096
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
|
361
|
+
}
|
|
362
|
+
const reader = response.body?.getReader();
|
|
363
|
+
if (!reader) return;
|
|
364
|
+
const decoder = new TextDecoder();
|
|
365
|
+
let buffer = "";
|
|
366
|
+
while (true) {
|
|
367
|
+
const { done, value } = await reader.read();
|
|
368
|
+
if (done) break;
|
|
369
|
+
buffer += decoder.decode(value, { stream: true });
|
|
370
|
+
const lines = buffer.split("\n");
|
|
371
|
+
buffer = lines.pop() ?? "";
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
if (!line.trim()) continue;
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(line);
|
|
376
|
+
if (parsed.done) return;
|
|
377
|
+
if (parsed.message?.content) {
|
|
378
|
+
yield parsed.message.content;
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async isAvailable(baseUrl) {
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch(`${baseUrl || "http://localhost:11434"}/api/tags`);
|
|
388
|
+
return res.ok;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/ai/openai-like.ts
|
|
396
|
+
var OpenAILikeProvider = class {
|
|
397
|
+
name;
|
|
398
|
+
baseUrl;
|
|
399
|
+
constructor(name, baseUrl) {
|
|
400
|
+
this.name = name;
|
|
401
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
402
|
+
}
|
|
403
|
+
async chat(messages, config) {
|
|
404
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
405
|
+
Logger.debug(`[${this.name}] chat: ${config.model} (${messages.length} messages)`);
|
|
406
|
+
const apiKey = this.getApiKey();
|
|
407
|
+
const response = await fetch(url, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"Content-Type": "application/json",
|
|
411
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
412
|
+
},
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
model: config.model,
|
|
415
|
+
messages,
|
|
416
|
+
temperature: config.temperature ?? 0.2,
|
|
417
|
+
max_tokens: config.maxTokens ?? 4096,
|
|
418
|
+
stream: false
|
|
419
|
+
})
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
const body = await response.text().catch(() => "");
|
|
423
|
+
throw new Error(`[${this.name}] API error ${response.status}: ${body.slice(0, 200)}`);
|
|
424
|
+
}
|
|
425
|
+
const data = await response.json();
|
|
426
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
427
|
+
}
|
|
428
|
+
async *stream(messages, config) {
|
|
429
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
430
|
+
const apiKey = this.getApiKey();
|
|
431
|
+
const response = await fetch(url, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: {
|
|
434
|
+
"Content-Type": "application/json",
|
|
435
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
436
|
+
},
|
|
437
|
+
body: JSON.stringify({
|
|
438
|
+
model: config.model,
|
|
439
|
+
messages,
|
|
440
|
+
temperature: config.temperature ?? 0.2,
|
|
441
|
+
max_tokens: config.maxTokens ?? 4096,
|
|
442
|
+
stream: true
|
|
443
|
+
})
|
|
444
|
+
});
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
const body = await response.text().catch(() => "");
|
|
447
|
+
throw new Error(`[${this.name}] API error ${response.status}: ${body.slice(0, 200)}`);
|
|
448
|
+
}
|
|
449
|
+
const reader = response.body?.getReader();
|
|
450
|
+
if (!reader) return;
|
|
451
|
+
const decoder = new TextDecoder();
|
|
452
|
+
let buffer = "";
|
|
453
|
+
while (true) {
|
|
454
|
+
const { done, value } = await reader.read();
|
|
455
|
+
if (done) break;
|
|
456
|
+
buffer += decoder.decode(value, { stream: true });
|
|
457
|
+
const lines = buffer.split("\n");
|
|
458
|
+
buffer = lines.pop() ?? "";
|
|
459
|
+
for (const line of lines) {
|
|
460
|
+
const trimmed = line.trim();
|
|
461
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
462
|
+
const jsonStr = trimmed.slice(6).trim();
|
|
463
|
+
if (jsonStr === "[DONE]") return;
|
|
464
|
+
try {
|
|
465
|
+
const parsed = JSON.parse(jsonStr);
|
|
466
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
467
|
+
if (content) yield content;
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
getApiKey() {
|
|
474
|
+
const envVar = this.name.toUpperCase();
|
|
475
|
+
return process.env[`${envVar}_API_KEY`] || process.env.OPENAI_API_KEY;
|
|
476
|
+
}
|
|
477
|
+
async isAvailable() {
|
|
478
|
+
try {
|
|
479
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
480
|
+
headers: this.getApiKey() ? { Authorization: `Bearer ${this.getApiKey()}` } : {}
|
|
481
|
+
});
|
|
482
|
+
return res.ok;
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// src/ai/registry.ts
|
|
490
|
+
var registry = [
|
|
491
|
+
{
|
|
492
|
+
name: "ollama",
|
|
493
|
+
provider: new OllamaProvider(),
|
|
494
|
+
models: ["codellama", "deepseek-coder", "llama3.2", "llama3.1", "mistral", "mixtral", "phi3", "qwen2.5-coder"],
|
|
495
|
+
local: true,
|
|
496
|
+
priority: 10,
|
|
497
|
+
defaultModel: "codellama",
|
|
498
|
+
getConfig: (model) => ({
|
|
499
|
+
model,
|
|
500
|
+
baseUrl: "http://localhost:11434",
|
|
501
|
+
temperature: 0.2,
|
|
502
|
+
maxTokens: 8192
|
|
503
|
+
})
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "groq",
|
|
507
|
+
provider: new OpenAILikeProvider("groq", "https://api.groq.com/openai/v1"),
|
|
508
|
+
models: ["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma2-9b-it", "deepseek-r1-distill-llama-70b"],
|
|
509
|
+
local: false,
|
|
510
|
+
priority: 30,
|
|
511
|
+
defaultModel: "llama3-70b-8192",
|
|
512
|
+
getConfig: (model) => ({
|
|
513
|
+
model,
|
|
514
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
515
|
+
temperature: 0.2,
|
|
516
|
+
maxTokens: 8192
|
|
517
|
+
})
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "openrouter",
|
|
521
|
+
provider: new OpenAILikeProvider("openrouter", "https://openrouter.ai/api/v1"),
|
|
522
|
+
models: [
|
|
523
|
+
"google/gemini-2.0-flash-001",
|
|
524
|
+
"google/gemini-2.0-flash-lite-preview",
|
|
525
|
+
"mistralai/mistral-7b-instruct",
|
|
526
|
+
"meta-llama/llama-3.2-3b-instruct",
|
|
527
|
+
"deepseek/deepseek-chat",
|
|
528
|
+
"qwen/qwen-2.5-7b-instruct"
|
|
529
|
+
],
|
|
530
|
+
local: false,
|
|
531
|
+
priority: 40,
|
|
532
|
+
defaultModel: "google/gemini-2.0-flash-001",
|
|
533
|
+
getConfig: (model) => ({
|
|
534
|
+
model,
|
|
535
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
536
|
+
temperature: 0.2,
|
|
537
|
+
maxTokens: 8192
|
|
538
|
+
})
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "together",
|
|
542
|
+
provider: new OpenAILikeProvider("together", "https://api.together.xyz/v1"),
|
|
543
|
+
models: [
|
|
544
|
+
"mistralai/Mixtral-8x22B-Instruct-v0.1",
|
|
545
|
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
|
546
|
+
"meta-llama/Llama-3.2-3B-Instruct-Turbo",
|
|
547
|
+
"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo",
|
|
548
|
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
|
549
|
+
"Qwen/Qwen2.5-7B-Instruct-Turbo"
|
|
550
|
+
],
|
|
551
|
+
local: false,
|
|
552
|
+
priority: 50,
|
|
553
|
+
defaultModel: "mistralai/Mixtral-8x22B-Instruct-v0.1",
|
|
554
|
+
getConfig: (model) => ({
|
|
555
|
+
model,
|
|
556
|
+
baseUrl: "https://api.together.xyz/v1",
|
|
557
|
+
temperature: 0.2,
|
|
558
|
+
maxTokens: 8192
|
|
559
|
+
})
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "huggingface",
|
|
563
|
+
provider: new OpenAILikeProvider("huggingface", "https://api-inference.huggingface.co/v1"),
|
|
564
|
+
models: [
|
|
565
|
+
"HuggingFaceH4/zephyr-7b-beta",
|
|
566
|
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
|
567
|
+
"meta-llama/Meta-Llama-3-8B-Instruct",
|
|
568
|
+
"google/gemma-2-9b-it"
|
|
569
|
+
],
|
|
570
|
+
local: false,
|
|
571
|
+
priority: 60,
|
|
572
|
+
defaultModel: "HuggingFaceH4/zephyr-7b-beta",
|
|
573
|
+
getConfig: (model) => ({
|
|
574
|
+
model,
|
|
575
|
+
baseUrl: "https://api-inference.huggingface.co/v1",
|
|
576
|
+
temperature: 0.2,
|
|
577
|
+
maxTokens: 4096
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
];
|
|
581
|
+
function getProvider(name) {
|
|
582
|
+
return registry.find((p) => p.name === name);
|
|
583
|
+
}
|
|
584
|
+
function getProviderForModel(model) {
|
|
585
|
+
return registry.find((p) => p.models.includes(model) || p.defaultModel === model);
|
|
586
|
+
}
|
|
587
|
+
function getLocalProviders() {
|
|
588
|
+
return registry.filter((p) => p.local);
|
|
589
|
+
}
|
|
590
|
+
function resolveModel(modelOrProvider) {
|
|
591
|
+
const byName = getProvider(modelOrProvider);
|
|
592
|
+
if (byName) {
|
|
593
|
+
return { entry: byName, model: byName.defaultModel };
|
|
594
|
+
}
|
|
595
|
+
const byModel = getProviderForModel(modelOrProvider);
|
|
596
|
+
if (byModel) {
|
|
597
|
+
return { entry: byModel, model: modelOrProvider };
|
|
598
|
+
}
|
|
599
|
+
const local = getLocalProviders()[0];
|
|
600
|
+
return { entry: local, model: modelOrProvider };
|
|
601
|
+
}
|
|
602
|
+
function printProviders() {
|
|
603
|
+
const lines = [chalk2.bold("\n Available Providers")];
|
|
604
|
+
for (const entry of registry) {
|
|
605
|
+
const tag = entry.local ? chalk2.green(" LOCAL ") : chalk2.blue(" CLOUD ");
|
|
606
|
+
const defaultModel = chalk2.dim(`(default: ${entry.defaultModel})`);
|
|
607
|
+
const models = entry.models.slice(0, 4).join(", ");
|
|
608
|
+
const more = entry.models.length > 4 ? chalk2.dim(` +${entry.models.length - 4} more`) : "";
|
|
609
|
+
lines.push(`
|
|
610
|
+
${tag} ${chalk2.cyan(entry.name.padEnd(12))} ${defaultModel}`);
|
|
611
|
+
lines.push(` ${chalk2.dim(models)}${more}`);
|
|
612
|
+
}
|
|
613
|
+
lines.push("");
|
|
614
|
+
return lines.join("\n");
|
|
615
|
+
}
|
|
616
|
+
function setDefaultModel(model) {
|
|
617
|
+
const resolved = resolveModel(model);
|
|
618
|
+
resolved.entry.defaultModel = resolved.model;
|
|
619
|
+
return { provider: resolved.entry.name, model: resolved.model };
|
|
620
|
+
}
|
|
621
|
+
function getDefaultModel() {
|
|
622
|
+
const highest = [...registry].sort((a, b) => {
|
|
623
|
+
if (a.local && !b.local) return -1;
|
|
624
|
+
if (!a.local && b.local) return 1;
|
|
625
|
+
return a.priority - b.priority;
|
|
626
|
+
})[0];
|
|
627
|
+
return { provider: highest.name, model: highest.defaultModel };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/git/message.ts
|
|
631
|
+
function getDefaultProvider() {
|
|
632
|
+
const defaultModel = getDefaultModel();
|
|
633
|
+
const resolved = resolveModel(defaultModel.model);
|
|
634
|
+
const provider = resolved.entry.provider;
|
|
635
|
+
const config = resolved.entry.getConfig ? resolved.entry.getConfig(resolved.model) : { model: resolved.model, temperature: 0.3, maxTokens: 512 };
|
|
636
|
+
return { provider, config };
|
|
637
|
+
}
|
|
638
|
+
var COMMIT_PROMPT = `You are a commit message generator. Analyze the provided git diff and generate a concise, conventional commit message.
|
|
639
|
+
|
|
640
|
+
Format: <type>(<scope>): <description>
|
|
641
|
+
|
|
642
|
+
Types: feat, fix, refactor, test, docs, style, chore, perf, ci
|
|
643
|
+
Scope is optional.
|
|
644
|
+
|
|
645
|
+
Rules:
|
|
646
|
+
- First line: max 72 characters
|
|
647
|
+
- If needed, follow with a blank line and bullet points for details
|
|
648
|
+
- Focus on WHAT and WHY, not HOW
|
|
649
|
+
- Use imperative mood
|
|
650
|
+
|
|
651
|
+
Respond with ONLY the commit message, no explanations.`;
|
|
652
|
+
var PR_SUMMARY_PROMPT = `You are a PR summary generator. Analyze the provided branch diff and commit log, then generate a comprehensive pull request summary.
|
|
653
|
+
|
|
654
|
+
Format:
|
|
655
|
+
## Summary
|
|
656
|
+
<brief overview of changes>
|
|
657
|
+
|
|
658
|
+
## Changes
|
|
659
|
+
- <bullet point per logical change>
|
|
660
|
+
|
|
661
|
+
## Motivation
|
|
662
|
+
<why these changes were made>
|
|
663
|
+
|
|
664
|
+
## Testing
|
|
665
|
+
<suggested testing approach>
|
|
666
|
+
|
|
667
|
+
Keep it concise but informative. Focus on the impact of changes.`;
|
|
668
|
+
async function generateCommitMessage(options) {
|
|
669
|
+
if (!isGitAvailable()) return "git not available";
|
|
670
|
+
let diff = getStagedDiff();
|
|
671
|
+
if (!diff) diff = getFullDiff();
|
|
672
|
+
if (!diff) return "No changes detected";
|
|
673
|
+
const { provider, config } = options?.provider ? { provider: options.provider, config: options.providerConfig || { model: "default", temperature: 0.3, maxTokens: 512 } } : getDefaultProvider();
|
|
674
|
+
const truncatedDiff = abbreviateDiff(diff, 150);
|
|
675
|
+
const messages = [
|
|
676
|
+
{ role: "system", content: COMMIT_PROMPT },
|
|
677
|
+
{ role: "user", content: `Git diff:
|
|
678
|
+
|
|
679
|
+
${truncatedDiff}` }
|
|
680
|
+
];
|
|
681
|
+
try {
|
|
682
|
+
return await provider.chat(messages, { ...config, temperature: 0.3, maxTokens: 512 });
|
|
683
|
+
} catch {
|
|
684
|
+
return fallbackCommitMessage(diff);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function generatePRSummary(baseBranch, options) {
|
|
688
|
+
if (!isGitAvailable()) return "git not available";
|
|
689
|
+
const branch = getCurrentBranch();
|
|
690
|
+
const base = baseBranch || "main";
|
|
691
|
+
const diff = getPRDiff(base);
|
|
692
|
+
const log = getPRLog(base);
|
|
693
|
+
if (!diff && log.length === 0) return `No changes between ${base} and ${branch}`;
|
|
694
|
+
const { provider, config } = options?.provider ? { provider: options.provider, config: options.providerConfig || { model: "default", temperature: 0.3, maxTokens: 1024 } } : getDefaultProvider();
|
|
695
|
+
const truncatedDiff = abbreviateDiff(diff, 200);
|
|
696
|
+
const commitLog = log.map((e) => ` - ${e.message}`).join("\n");
|
|
697
|
+
const messages = [
|
|
698
|
+
{ role: "system", content: PR_SUMMARY_PROMPT },
|
|
699
|
+
{
|
|
700
|
+
role: "user",
|
|
701
|
+
content: `Branch: ${branch} \u2192 ${base}
|
|
702
|
+
|
|
703
|
+
Commits:
|
|
704
|
+
${commitLog}
|
|
705
|
+
|
|
706
|
+
Diff:
|
|
707
|
+
${truncatedDiff}`
|
|
708
|
+
}
|
|
709
|
+
];
|
|
710
|
+
try {
|
|
711
|
+
return await provider.chat(messages, { ...config, temperature: 0.3, maxTokens: 1024 });
|
|
712
|
+
} catch {
|
|
713
|
+
return fallbackPRSummary(branch, base, log);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function fallbackCommitMessage(diff) {
|
|
717
|
+
const lines = diff.split("\n");
|
|
718
|
+
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
719
|
+
const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
720
|
+
const filesChanged = new Set(
|
|
721
|
+
lines.filter((l) => l.startsWith("diff --git")).map((l) => l.split(" b/")[1])
|
|
722
|
+
).size;
|
|
723
|
+
const type = removed > added ? "fix" : "refactor";
|
|
724
|
+
return `${type}: update ${filesChanged} file${filesChanged > 1 ? "s" : ""} (+${added} -${removed})`;
|
|
725
|
+
}
|
|
726
|
+
function fallbackPRSummary(branch, base, log) {
|
|
727
|
+
return `## Summary
|
|
728
|
+
Changes on branch \`${branch}\` relative to \`${base}\`.
|
|
729
|
+
|
|
730
|
+
## Changes
|
|
731
|
+
${log.map((e) => `- ${e.message}`).join("\n")}
|
|
732
|
+
|
|
733
|
+
## Motivation
|
|
734
|
+
Automated summary generated by LoveCode AI.`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/git/conflict.ts
|
|
738
|
+
import * as fs2 from "fs";
|
|
739
|
+
import * as path2 from "path";
|
|
740
|
+
function detectConflicts(cwd) {
|
|
741
|
+
if (!isGitAvailable() || !isRepo(cwd)) return [];
|
|
742
|
+
if (!hasConflicts(cwd)) return [];
|
|
743
|
+
const files = getConflictFiles(cwd);
|
|
744
|
+
const root = getGitRoot(cwd);
|
|
745
|
+
return files.map((file) => {
|
|
746
|
+
const markers = getConflictMarkers(file, cwd);
|
|
747
|
+
const regions = [];
|
|
748
|
+
for (let i = 0; i < markers.length; i++) {
|
|
749
|
+
if (markers[i].type === "start") {
|
|
750
|
+
const end = markers.find((m, j) => j > i && m.type === "end");
|
|
751
|
+
if (end) {
|
|
752
|
+
regions.push({ startLine: markers[i].line, endLine: end.line });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const fullPath = root ? path2.join(root, file) : file;
|
|
757
|
+
let lineCount = 0;
|
|
758
|
+
try {
|
|
759
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
760
|
+
lineCount = content.split("\n").length;
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
file,
|
|
765
|
+
markers: markers.length / 3,
|
|
766
|
+
lines: lineCount,
|
|
767
|
+
regions
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function suggestResolutions(cwd) {
|
|
772
|
+
const conflicts = detectConflicts(cwd);
|
|
773
|
+
if (conflicts.length === 0) return [];
|
|
774
|
+
return conflicts.map((conflict) => {
|
|
775
|
+
const strategy = pickStrategy(conflict);
|
|
776
|
+
return {
|
|
777
|
+
file: conflict.file,
|
|
778
|
+
strategy: strategy.label,
|
|
779
|
+
explanation: strategy.explanation
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function pickStrategy(conflict) {
|
|
784
|
+
if (conflict.regions.length <= 2 && conflict.markers <= 2) {
|
|
785
|
+
return {
|
|
786
|
+
label: "manual-edit",
|
|
787
|
+
explanation: `Small conflict in ${conflict.file} (${conflict.markers} conflict regions). Open the file, resolve each conflict region by keeping the appropriate changes, remove <<<<<<<, =======, >>>>>>> markers, then run 'git add ${conflict.file}' and 'git commit'.`
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
label: "review-carefully",
|
|
792
|
+
explanation: `Multiple conflict regions in ${conflict.file}. Review each region carefully, keeping the correct combination of changes. After resolving all conflicts, use 'git add ${conflict.file}' and continue the merge with 'git commit'.`
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function formatConflictInfo(conflicts) {
|
|
796
|
+
if (conflicts.length === 0) return "No merge conflicts detected.";
|
|
797
|
+
const lines = [`Merge Conflicts Detected (${conflicts.length} files):`];
|
|
798
|
+
for (const c of conflicts) {
|
|
799
|
+
lines.push(` ${c.file}`);
|
|
800
|
+
lines.push(` Conflict regions: ${c.markers}`);
|
|
801
|
+
lines.push(` Total lines: ${c.lines}`);
|
|
802
|
+
for (const region of c.regions) {
|
|
803
|
+
lines.push(` Lines ${region.startLine}-${region.endLine}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return lines.join("\n");
|
|
807
|
+
}
|
|
808
|
+
function formatResolutionSuggestions(suggestions) {
|
|
809
|
+
if (suggestions.length === 0) return "No resolution suggestions needed.";
|
|
810
|
+
const lines = ["Resolution Suggestions:"];
|
|
811
|
+
for (const s of suggestions) {
|
|
812
|
+
lines.push(` File: ${s.file}`);
|
|
813
|
+
lines.push(` Strategy: ${s.strategy}`);
|
|
814
|
+
lines.push(` ${s.explanation}`);
|
|
815
|
+
lines.push("");
|
|
816
|
+
}
|
|
817
|
+
return lines.join("\n");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export {
|
|
821
|
+
OllamaProvider,
|
|
822
|
+
isGitAvailable,
|
|
823
|
+
getGitRoot,
|
|
824
|
+
isRepo,
|
|
825
|
+
getCurrentBranch,
|
|
826
|
+
getStatus,
|
|
827
|
+
stageAll,
|
|
828
|
+
stageFiles,
|
|
829
|
+
commit,
|
|
830
|
+
getDiff,
|
|
831
|
+
getStagedDiff,
|
|
832
|
+
getUnstagedDiff,
|
|
833
|
+
getFullDiff,
|
|
834
|
+
getLog,
|
|
835
|
+
getBranches,
|
|
836
|
+
createBranch,
|
|
837
|
+
switchBranch,
|
|
838
|
+
deleteBranch,
|
|
839
|
+
cleanupMergedBranches,
|
|
840
|
+
getPRDiff,
|
|
841
|
+
getPRLog,
|
|
842
|
+
hasConflicts,
|
|
843
|
+
getConflictFiles,
|
|
844
|
+
getConflictMarkers,
|
|
845
|
+
formatStatus,
|
|
846
|
+
formatBranches,
|
|
847
|
+
formatLog,
|
|
848
|
+
abbreviateDiff,
|
|
849
|
+
getProvider,
|
|
850
|
+
printProviders,
|
|
851
|
+
setDefaultModel,
|
|
852
|
+
generateCommitMessage,
|
|
853
|
+
generatePRSummary,
|
|
854
|
+
detectConflicts,
|
|
855
|
+
suggestResolutions,
|
|
856
|
+
formatConflictInfo,
|
|
857
|
+
formatResolutionSuggestions
|
|
858
|
+
};
|