gitpt 1.4.0 → 1.6.1
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 +7 -0
- package/dist/commands/commit/context/buildPrompt.d.ts +4 -0
- package/dist/commands/commit/context/buildPrompt.js +13 -0
- package/dist/commands/commit/context/summaryPrompt.d.ts +2 -0
- package/dist/commands/commit/context/summaryPrompt.js +17 -0
- package/dist/commands/commit/generateCommitMessage.js +8 -26
- package/dist/commands/commit/index.js +4 -2
- package/dist/commands/commit/summarizeDiff.d.ts +1 -0
- package/dist/commands/commit/summarizeDiff.js +172 -0
- package/dist/commands/middleware/setupMiddleware/defaultModels.d.ts +8 -0
- package/dist/commands/middleware/setupMiddleware/defaultModels.js +11 -0
- package/dist/commands/middleware/setupMiddleware/index.js +58 -24
- package/dist/commands/pr/generatePRDetails.js +7 -14
- package/dist/commands/reset.d.ts +3 -0
- package/dist/commands/reset.js +26 -0
- package/dist/config.d.ts +6 -10
- package/dist/config.js +25 -20
- package/dist/index.js +6 -0
- package/dist/llm/client.d.ts +24 -0
- package/dist/llm/index.d.ts +3 -2
- package/dist/llm/index.js +4 -9
- package/dist/llm/providers/anthropic/index.d.ts +9 -0
- package/dist/llm/providers/anthropic/index.js +31 -0
- package/dist/llm/providers/apiKey.d.ts +3 -0
- package/dist/llm/providers/apiKey.js +40 -0
- package/dist/llm/providers/apple/client.d.ts +3 -0
- package/dist/llm/providers/apple/client.js +87 -0
- package/dist/llm/providers/apple/index.d.ts +13 -0
- package/dist/llm/providers/apple/index.js +77 -0
- package/dist/llm/providers/apple/models.d.ts +14 -0
- package/dist/llm/providers/apple/models.js +21 -0
- package/dist/llm/providers/base.d.ts +30 -0
- package/dist/llm/providers/base.js +36 -0
- package/dist/llm/providers/local/index.d.ts +11 -0
- package/dist/llm/providers/local/index.js +96 -0
- package/dist/llm/providers/openai/index.d.ts +10 -0
- package/dist/llm/providers/openai/index.js +16 -0
- package/dist/llm/providers/openaiCompatible.d.ts +15 -0
- package/dist/llm/providers/openaiCompatible.js +69 -0
- package/dist/llm/providers/openrouter/index.d.ts +9 -0
- package/dist/llm/providers/openrouter/index.js +16 -0
- package/dist/llm/registry.d.ts +8 -0
- package/dist/llm/registry.js +43 -0
- package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.d.ts +1 -0
- package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.js +3 -3
- package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.d.ts +1 -1
- package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.js +13 -3
- package/dist/llm/setup/types.js +1 -0
- package/dist/llm/tokenCount.d.ts +3 -0
- package/dist/llm/tokenCount.js +4 -0
- package/dist/services/git/getStagedChanges.js +1 -1
- package/package.json +6 -2
- package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.d.ts +0 -1
- package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.js +0 -39
- package/dist/commands/middleware/setupMiddleware/setupLocalLLM.d.ts +0 -5
- package/dist/commands/middleware/setupMiddleware/setupLocalLLM.js +0 -60
- package/dist/commands/middleware/setupMiddleware/setupOpenRouter.d.ts +0 -2
- package/dist/commands/middleware/setupMiddleware/setupOpenRouter.js +0 -66
- /package/dist/{commands/middleware/setupMiddleware/types.js → llm/client.js} +0 -0
- /package/dist/{commands/middleware/setupMiddleware → llm/setup}/types.d.ts +0 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Git Prompt Tool is a CLI tool that helps you write commit messages using AI thro
|
|
|
15
15
|
- Edit suggested messages before committing
|
|
16
16
|
- Works with various AI models via OpenRouter
|
|
17
17
|
- Support for local LLMs with OpenAI-compatible API
|
|
18
|
+
- Support for Apple Foundation Models on-device (macOS 27+, no API key required)
|
|
18
19
|
- [Commitlint](https://commitlint.js.org/) support - read directly from your repository
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
@@ -114,6 +115,12 @@ GitPT works with any local LLM that provides an OpenAI-compatible API endpoint,
|
|
|
114
115
|
- [LocalAI](https://localai.io/)
|
|
115
116
|
- Custom setups with tools like llama.cpp
|
|
116
117
|
|
|
118
|
+
#### Using Apple Foundation Models
|
|
119
|
+
|
|
120
|
+
On macOS 27 and later, GitPT can use Apple's on-device Foundation Models through the built-in `fm` CLI — no API key or network connection required. Select **Apple Foundation Models** when running `gitpt setup` or `gitpt model`.
|
|
121
|
+
|
|
122
|
+
> Note: the on-device model has a small context window (~4096 tokens), so very large diffs may not fit.
|
|
123
|
+
|
|
117
124
|
## GitHub Usage
|
|
118
125
|
|
|
119
126
|
If you have GitHub CLI (`gh`) installed, you can use GitPT to interact with GitHub (e.g. generate full pull requests).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCommitlintRules, hasCommitlintConfig, } from "../../../utils/commitlint.js";
|
|
2
|
+
import { systemPrompt } from "./systemPrompt.js";
|
|
3
|
+
import { userPrompt } from "./userPrompt.js";
|
|
4
|
+
export const buildCommitPrompt = (diff, validationErrors) => {
|
|
5
|
+
const baseRules = hasCommitlintConfig() ? getCommitlintRules() : "";
|
|
6
|
+
const errorInfo = validationErrors
|
|
7
|
+
? `\n\nYOUR PREVIOUS MESSAGE FAILED VALIDATION WITH THESE ERRORS:\n${validationErrors}\n\nFIX THESE ISSUES IN YOUR NEW MESSAGE.`
|
|
8
|
+
: "";
|
|
9
|
+
return {
|
|
10
|
+
system: [systemPrompt, baseRules, errorInfo].join("\n\n"),
|
|
11
|
+
user: userPrompt(diff),
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const summarySystemPrompt = "\nYou are condensing a fragment of a git diff so a commit message can be written afterwards.\n\nOutput exactly one line per changed file, in this format:\n<path>: <what changed>\n\nRules:\n- One line per file \u2014 no more, no less.\n- Keep each description under ~12 words.\n- Describe the functional change (what was added, removed, refactored, or fixed), not file mechanics.\n- Output ONLY these lines. No preamble, no commentary, no code fences, no headings, no blank lines.\n";
|
|
2
|
+
export declare const summaryUserPrompt: (diffChunk: string) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const summarySystemPrompt = `
|
|
2
|
+
You are condensing a fragment of a git diff so a commit message can be written afterwards.
|
|
3
|
+
|
|
4
|
+
Output exactly one line per changed file, in this format:
|
|
5
|
+
<path>: <what changed>
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- One line per file — no more, no less.
|
|
9
|
+
- Keep each description under ~12 words.
|
|
10
|
+
- Describe the functional change (what was added, removed, refactored, or fixed), not file mechanics.
|
|
11
|
+
- Output ONLY these lines. No preamble, no commentary, no code fences, no headings, no blank lines.
|
|
12
|
+
`;
|
|
13
|
+
export const summaryUserPrompt = (diffChunk) => `
|
|
14
|
+
Summarize the changes in this git diff fragment:
|
|
15
|
+
|
|
16
|
+
${diffChunk}
|
|
17
|
+
`;
|
|
@@ -1,37 +1,19 @@
|
|
|
1
1
|
import { getConfig } from "../../config.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { systemPrompt } from "./context/systemPrompt.js";
|
|
5
|
-
import { userPrompt } from "./context/userPrompt.js";
|
|
2
|
+
import { getProvider } from "../../llm/registry.js";
|
|
3
|
+
import { buildCommitPrompt } from "./context/buildPrompt.js";
|
|
6
4
|
export const generateCommitMessage = async (diff, validationErrors) => {
|
|
7
|
-
// Check if commitlint is configured
|
|
8
|
-
const hasCommitlint = hasCommitlintConfig();
|
|
9
5
|
const config = getConfig();
|
|
10
6
|
// Check if we have a configured model
|
|
11
7
|
if (!config.model) {
|
|
12
8
|
throw new Error('GitPT is not configured properly. Please run "gitpt setup" first.');
|
|
13
9
|
}
|
|
14
|
-
const {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const response = await llmClient.chat.completions.create({
|
|
21
|
-
model: model,
|
|
22
|
-
messages: [
|
|
23
|
-
{
|
|
24
|
-
role: "system",
|
|
25
|
-
content: [systemPrompt, baseRules, errorInfo].join("\n\n"),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
role: "user",
|
|
29
|
-
content: userPrompt(diff),
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
max_tokens: 300,
|
|
10
|
+
const { system, user } = buildCommitPrompt(diff, validationErrors);
|
|
11
|
+
const provider = getProvider();
|
|
12
|
+
const message = await provider.complete({
|
|
13
|
+
system,
|
|
14
|
+
user,
|
|
15
|
+
maxTokens: provider.maxOutputTokens,
|
|
33
16
|
});
|
|
34
|
-
const message = response.choices[0].message.content;
|
|
35
17
|
if (!message) {
|
|
36
18
|
throw new Error("No message returned from LLM");
|
|
37
19
|
}
|
|
@@ -6,6 +6,7 @@ import { git } from "../../services/git/index.js";
|
|
|
6
6
|
import { hasCommitlintConfig, validateCommitMessage, } from "../../utils/commitlint.js";
|
|
7
7
|
import { hasStagedChangesMiddleware } from "../middleware/hasStagedChangesMiddleware.js";
|
|
8
8
|
import { generateCommitMessage } from "./generateCommitMessage.js";
|
|
9
|
+
import { prepareCommitContext } from "./summarizeDiff.js";
|
|
9
10
|
export const commitCommand = async (options) => {
|
|
10
11
|
capabilitiesMiddleware(["git"]);
|
|
11
12
|
await setupMiddleware();
|
|
@@ -19,6 +20,7 @@ export const commitCommand = async (options) => {
|
|
|
19
20
|
try {
|
|
20
21
|
// Get staged changes
|
|
21
22
|
const diff = git.getStagedChanges();
|
|
23
|
+
const context = await prepareCommitContext(diff);
|
|
22
24
|
console.log(chalk.blue("Generating commit message..."));
|
|
23
25
|
// Check if commitlint is configured
|
|
24
26
|
let hasCommitlint = false;
|
|
@@ -32,7 +34,7 @@ export const commitCommand = async (options) => {
|
|
|
32
34
|
console.warn(chalk.yellow("Warning: Error detecting commitlint config, proceeding without commitlint validation."));
|
|
33
35
|
}
|
|
34
36
|
// Generate first commit message
|
|
35
|
-
commitMessage = await generateCommitMessage(
|
|
37
|
+
commitMessage = await generateCommitMessage(context);
|
|
36
38
|
// If commitlint is configured, try to validate and regenerate up to 3 times
|
|
37
39
|
if (hasCommitlint) {
|
|
38
40
|
try {
|
|
@@ -61,7 +63,7 @@ export const commitCommand = async (options) => {
|
|
|
61
63
|
if (attempts < MAX_ATTEMPTS) {
|
|
62
64
|
// Try regenerating with validation errors
|
|
63
65
|
try {
|
|
64
|
-
commitMessage = await generateCommitMessage(
|
|
66
|
+
commitMessage = await generateCommitMessage(context, validationErrors);
|
|
65
67
|
}
|
|
66
68
|
catch (error) {
|
|
67
69
|
console.warn(chalk.yellow("Error regenerating message, breaking validation loop."));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const prepareCommitContext: (diff: string) => Promise<string>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { getProvider } from "../../llm/registry.js";
|
|
3
|
+
import { countTokens } from "../../llm/tokenCount.js";
|
|
4
|
+
import { summarySystemPrompt, summaryUserPrompt } from "./context/summaryPrompt.js";
|
|
5
|
+
import { systemPrompt } from "./context/systemPrompt.js";
|
|
6
|
+
import { userPrompt } from "./context/userPrompt.js";
|
|
7
|
+
const MARGIN = 0.9;
|
|
8
|
+
const MAX_REDUCE_PASSES = 3;
|
|
9
|
+
const LOW_SIGNAL_PATTERNS = [
|
|
10
|
+
{ test: /(^|\/)package-lock\.json$/, note: "dependency lockfile updated" },
|
|
11
|
+
{ test: /(^|\/)npm-shrinkwrap\.json$/, note: "dependency lockfile updated" },
|
|
12
|
+
{ test: /(^|\/)yarn\.lock$/, note: "dependency lockfile updated" },
|
|
13
|
+
{ test: /(^|\/)pnpm-lock\.yaml$/, note: "dependency lockfile updated" },
|
|
14
|
+
{ test: /\.lock$/, note: "dependency lockfile updated" },
|
|
15
|
+
{ test: /(^|\/)(dist|build|out|coverage)\//, note: "generated output updated" },
|
|
16
|
+
{ test: /\.min\.(js|css)$/, note: "minified asset updated" },
|
|
17
|
+
{ test: /\.(snap)$/, note: "test snapshot updated" },
|
|
18
|
+
{ test: /(^|\/)snapshots?\//, note: "test snapshot updated" },
|
|
19
|
+
{ test: /\.(patch|diff)$/, note: "patch file updated" },
|
|
20
|
+
];
|
|
21
|
+
const lowSignalNote = (file) => LOW_SIGNAL_PATTERNS.find((p) => p.test.test(file))?.note ?? null;
|
|
22
|
+
const finalPromptTokens = (context) => countTokens(`${systemPrompt}\n\n${userPrompt(context)}`);
|
|
23
|
+
const splitIntoFileBlocks = (diff) => diff.split(/(?=^diff --git )/m).filter((part) => part.trim().length > 0);
|
|
24
|
+
const fileNameOf = (block) => {
|
|
25
|
+
const match = block.match(/^diff --git a\/(.+?) b\//m);
|
|
26
|
+
return match ? match[1] : "changes";
|
|
27
|
+
};
|
|
28
|
+
const truncateToBudget = (file, content, budget) => {
|
|
29
|
+
if (countTokens(content) <= budget)
|
|
30
|
+
return content;
|
|
31
|
+
const marker = `\n... [truncated ${file} to fit context] ...\n`;
|
|
32
|
+
let keep = content.length;
|
|
33
|
+
let truncated = content;
|
|
34
|
+
while (countTokens(truncated) > budget && keep > 200) {
|
|
35
|
+
keep = Math.floor(keep * 0.7);
|
|
36
|
+
truncated = content.slice(0, keep) + marker;
|
|
37
|
+
}
|
|
38
|
+
return truncated;
|
|
39
|
+
};
|
|
40
|
+
const splitBlockByHunks = (file, content, budget) => {
|
|
41
|
+
const firstHunk = content.search(/^@@ /m);
|
|
42
|
+
if (firstHunk < 0) {
|
|
43
|
+
return [{ files: [file], content: truncateToBudget(file, content, budget) }];
|
|
44
|
+
}
|
|
45
|
+
const header = content.slice(0, firstHunk);
|
|
46
|
+
const hunks = content
|
|
47
|
+
.slice(firstHunk)
|
|
48
|
+
.split(/(?=^@@ )/m)
|
|
49
|
+
.filter((h) => h.length > 0);
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let current = "";
|
|
52
|
+
const flush = () => {
|
|
53
|
+
if (current) {
|
|
54
|
+
chunks.push({ files: [file], content: truncateToBudget(file, header + current, budget) });
|
|
55
|
+
current = "";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
for (const hunk of hunks) {
|
|
59
|
+
if (current && countTokens(header + current + hunk) > budget)
|
|
60
|
+
flush();
|
|
61
|
+
current += hunk;
|
|
62
|
+
}
|
|
63
|
+
flush();
|
|
64
|
+
return chunks;
|
|
65
|
+
};
|
|
66
|
+
const packChunks = (blocks, budget) => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let files = [];
|
|
69
|
+
let content = "";
|
|
70
|
+
let tokens = 0;
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (content) {
|
|
73
|
+
chunks.push({ files, content });
|
|
74
|
+
files = [];
|
|
75
|
+
content = "";
|
|
76
|
+
tokens = 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
for (const block of blocks) {
|
|
80
|
+
if (block.tokens > budget) {
|
|
81
|
+
flush();
|
|
82
|
+
chunks.push(...splitBlockByHunks(block.file, block.content, budget));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (content && tokens + block.tokens > budget)
|
|
86
|
+
flush();
|
|
87
|
+
files.push(block.file);
|
|
88
|
+
content += block.content;
|
|
89
|
+
tokens += block.tokens;
|
|
90
|
+
}
|
|
91
|
+
flush();
|
|
92
|
+
return chunks;
|
|
93
|
+
};
|
|
94
|
+
const summarizeChunk = async (content) => {
|
|
95
|
+
const provider = getProvider();
|
|
96
|
+
const message = await provider.complete({
|
|
97
|
+
system: summarySystemPrompt,
|
|
98
|
+
user: summaryUserPrompt(content),
|
|
99
|
+
maxTokens: provider.maxOutputTokens,
|
|
100
|
+
});
|
|
101
|
+
return message.trim();
|
|
102
|
+
};
|
|
103
|
+
export const prepareCommitContext = async (diff) => {
|
|
104
|
+
const reserved = getProvider().maxOutputTokens;
|
|
105
|
+
const window = await getProvider().getContextWindow();
|
|
106
|
+
if (!Number.isFinite(window))
|
|
107
|
+
return diff;
|
|
108
|
+
const fitBudget = Math.floor((window - reserved) * MARGIN);
|
|
109
|
+
if (finalPromptTokens(diff) <= fitBudget)
|
|
110
|
+
return diff;
|
|
111
|
+
const spinner = ora({ text: "Analyzing diff size..." }).start();
|
|
112
|
+
try {
|
|
113
|
+
const blocks = splitIntoFileBlocks(diff).map((content) => ({
|
|
114
|
+
file: fileNameOf(content),
|
|
115
|
+
content,
|
|
116
|
+
tokens: countTokens(content),
|
|
117
|
+
}));
|
|
118
|
+
const totalTokens = blocks.reduce((sum, b) => sum + b.tokens, 0);
|
|
119
|
+
const sourceBlocks = blocks.filter((b) => !lowSignalNote(b.file));
|
|
120
|
+
const lowSignalLines = blocks
|
|
121
|
+
.map((b) => {
|
|
122
|
+
const note = lowSignalNote(b.file);
|
|
123
|
+
return note ? `${b.file}: ${note}` : null;
|
|
124
|
+
})
|
|
125
|
+
.filter((line) => line !== null);
|
|
126
|
+
const summaryOverhead = countTokens(`${summarySystemPrompt}\n\n${summaryUserPrompt("")}`);
|
|
127
|
+
const chunkBudget = Math.floor((window - reserved - summaryOverhead) * MARGIN);
|
|
128
|
+
const chunks = packChunks(sourceBlocks, chunkBudget);
|
|
129
|
+
const condensedNote = lowSignalLines.length
|
|
130
|
+
? `, ${lowSignalLines.length} generated file(s) condensed`
|
|
131
|
+
: "";
|
|
132
|
+
spinner.text = `Diff is large (~${totalTokens} tokens). Summarizing ${sourceBlocks.length} source file(s) in ${chunks.length} chunk(s)${condensedNote}...`;
|
|
133
|
+
const summaries = [];
|
|
134
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
135
|
+
const chunk = chunks[i];
|
|
136
|
+
spinner.text = `Summarizing chunk ${i + 1}/${chunks.length}: ${chunk.files.join(", ")}`;
|
|
137
|
+
summaries.push(await summarizeChunk(chunk.content));
|
|
138
|
+
}
|
|
139
|
+
const header = `The lines below are per-file summaries of a single commit. Treat them as one related change and write a commit message describing the overall change (not just one file):`;
|
|
140
|
+
let combined = [...summaries.filter(Boolean), ...lowSignalLines].join("\n");
|
|
141
|
+
const framed = () => `${header}\n${combined}`;
|
|
142
|
+
let pass = 0;
|
|
143
|
+
while (finalPromptTokens(framed()) > fitBudget && pass < MAX_REDUCE_PASSES) {
|
|
144
|
+
pass++;
|
|
145
|
+
const lineBlocks = combined
|
|
146
|
+
.split("\n")
|
|
147
|
+
.filter((line) => line.trim().length > 0)
|
|
148
|
+
.map((line) => ({
|
|
149
|
+
file: "summary",
|
|
150
|
+
content: `${line}\n`,
|
|
151
|
+
tokens: countTokens(line),
|
|
152
|
+
}));
|
|
153
|
+
const reduceChunks = packChunks(lineBlocks, chunkBudget);
|
|
154
|
+
const reduced = [];
|
|
155
|
+
for (let i = 0; i < reduceChunks.length; i++) {
|
|
156
|
+
spinner.text = `Condensing summary (pass ${pass}, ${i + 1}/${reduceChunks.length})...`;
|
|
157
|
+
reduced.push(await summarizeChunk(reduceChunks[i].content));
|
|
158
|
+
}
|
|
159
|
+
combined = reduced.filter(Boolean).join("\n");
|
|
160
|
+
}
|
|
161
|
+
if (finalPromptTokens(framed()) > fitBudget) {
|
|
162
|
+
const headerTokens = countTokens(`${systemPrompt}\n\n${userPrompt(`${header}\n`)}`);
|
|
163
|
+
combined = truncateToBudget("summary", combined, fitBudget - headerTokens);
|
|
164
|
+
}
|
|
165
|
+
spinner.succeed(`Summarized ${blocks.length} file(s): ~${totalTokens} → ~${countTokens(framed())} tokens`);
|
|
166
|
+
return framed();
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
spinner.fail("Failed to summarize diff");
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AppleProvider } from "../../../llm/providers/apple/index.js";
|
|
2
|
+
import { isAppleModelAvailable } from "../../../llm/providers/apple/models.js";
|
|
3
|
+
const DEFAULT_MODELS = [
|
|
4
|
+
{
|
|
5
|
+
id: "apple-foundation-models",
|
|
6
|
+
label: "Apple Foundation Models (on-device)",
|
|
7
|
+
isAvailable: () => AppleProvider.isAvailable() && isAppleModelAvailable("system"),
|
|
8
|
+
config: { provider: "apple", model: "system" },
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
export const resolveDefaultModel = () => DEFAULT_MODELS.find((model) => model.isAvailable());
|
|
@@ -1,40 +1,74 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import inquirer from "inquirer";
|
|
2
|
-
import { getConfig,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { clearAcceptedDefault, getAcceptedDefault, getConfig, saveConfig, setAcceptedDefault, } from "../../../config.js";
|
|
4
|
+
import { getProviderClass, PROVIDERS, validateConfig, } from "../../../llm/registry.js";
|
|
5
|
+
import { resolveDefaultModel } from "./defaultModels.js";
|
|
5
6
|
export const setupMiddleware = async (options) => {
|
|
6
7
|
const context = options?.context || "command";
|
|
7
8
|
// Always get the current config, even if it's empty
|
|
8
9
|
const existingConfig = getConfig();
|
|
9
|
-
|
|
10
|
+
const defaultModel = resolveDefaultModel();
|
|
11
|
+
const acceptedDefault = getAcceptedDefault();
|
|
12
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
13
|
+
const applyDefault = (model) => {
|
|
14
|
+
saveConfig(model.config);
|
|
15
|
+
setAcceptedDefault(model.id);
|
|
16
|
+
return getConfig();
|
|
17
|
+
};
|
|
18
|
+
const confirmDefault = async (message) => {
|
|
19
|
+
const { useDefault } = await inquirer.prompt([
|
|
20
|
+
{ type: "confirm", name: "useDefault", message, default: true },
|
|
21
|
+
]);
|
|
22
|
+
return useDefault;
|
|
23
|
+
};
|
|
10
24
|
if (context === "command") {
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
if (validateConfig().isValid) {
|
|
26
|
+
if (!acceptedDefault)
|
|
27
|
+
return getConfig();
|
|
28
|
+
if (!defaultModel || defaultModel.id === acceptedDefault) {
|
|
29
|
+
return getConfig();
|
|
30
|
+
}
|
|
31
|
+
if (!interactive ||
|
|
32
|
+
(await confirmDefault(`The recommended default changed to ${defaultModel.label}. Use it?`))) {
|
|
33
|
+
const result = applyDefault(defaultModel);
|
|
34
|
+
if (interactive) {
|
|
35
|
+
console.log(chalk.green(`✓ Now using ${defaultModel.label}.`));
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
13
39
|
return getConfig();
|
|
14
40
|
}
|
|
41
|
+
if (!existingConfig.provider && defaultModel) {
|
|
42
|
+
if (!interactive)
|
|
43
|
+
return applyDefault(defaultModel);
|
|
44
|
+
if (await confirmDefault(`No model configured. Use ${defaultModel.label} by default?`)) {
|
|
45
|
+
const result = applyDefault(defaultModel);
|
|
46
|
+
console.log(chalk.green(`✓ Using ${defaultModel.label}. Run 'gitpt model' to change.`));
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (!interactive) {
|
|
51
|
+
throw new Error("GitPT is not configured. Run 'gitpt setup' in an interactive terminal.");
|
|
52
|
+
}
|
|
15
53
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
54
|
+
clearAcceptedDefault();
|
|
55
|
+
const providerChoices = PROVIDERS.filter((p) => p.isAvailable()).map((p) => ({
|
|
56
|
+
name: p.label,
|
|
57
|
+
value: p.id,
|
|
58
|
+
}));
|
|
59
|
+
const providerAnswer = await inquirer.prompt([
|
|
18
60
|
{
|
|
19
61
|
type: "list",
|
|
20
|
-
name: "
|
|
62
|
+
name: "provider",
|
|
21
63
|
message: "Select LLM provider:",
|
|
22
|
-
choices:
|
|
23
|
-
|
|
24
|
-
{ name: "Local LLM", value: true },
|
|
25
|
-
],
|
|
26
|
-
default: existingConfig.provider === "local" ? 1 : 0,
|
|
64
|
+
choices: providerChoices,
|
|
65
|
+
default: Math.max(providerChoices.findIndex((c) => c.value === existingConfig.provider), 0),
|
|
27
66
|
},
|
|
28
67
|
]);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: "
|
|
33
|
-
// Proceed based on selected provider
|
|
34
|
-
if (useLocalLLMAnswer.useLocalLLM) {
|
|
35
|
-
return await setupLocalLLM(existingConfig);
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
return await setupOpenRouter(existingConfig);
|
|
68
|
+
existingConfig.provider = providerAnswer.provider;
|
|
69
|
+
const spec = getProviderClass(existingConfig.provider);
|
|
70
|
+
if (!spec) {
|
|
71
|
+
throw new Error(`Unknown provider: ${existingConfig.provider ?? "(none)"}.`);
|
|
39
72
|
}
|
|
73
|
+
return spec.setup(existingConfig);
|
|
40
74
|
};
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
3
|
-
import { getLLMClient } from "../../llm/index.js";
|
|
2
|
+
import { getProvider } from "../../llm/registry.js";
|
|
4
3
|
import { systemPrompt } from "./context/systemPrompt.js";
|
|
5
4
|
import { userPrompt } from "./context/userPrompt.js";
|
|
6
5
|
import { getPRContext } from "./getPRContext.js";
|
|
7
6
|
export const generatePRDetails = async () => {
|
|
8
|
-
const { model } = getConfig();
|
|
9
7
|
const context = getPRContext().join("\n\n");
|
|
10
|
-
const userPromptWithContext = userPrompt(context);
|
|
11
|
-
const llmClient = getLLMClient();
|
|
12
8
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
max_completion_tokens: 1000,
|
|
20
|
-
});
|
|
21
|
-
const result = response.choices[0].message?.content?.trim();
|
|
9
|
+
const provider = getProvider();
|
|
10
|
+
const result = (await provider.complete({
|
|
11
|
+
system: systemPrompt,
|
|
12
|
+
user: userPrompt(context),
|
|
13
|
+
maxTokens: provider.maxOutputTokens,
|
|
14
|
+
})).trim();
|
|
22
15
|
if (!result) {
|
|
23
16
|
throw new Error("No response from LLM");
|
|
24
17
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import { clearConfig, getConfig } from "../config.js";
|
|
4
|
+
export const resetCommand = async (options = {}) => {
|
|
5
|
+
const hasConfig = Object.values(getConfig()).some((value) => value !== undefined);
|
|
6
|
+
if (!hasConfig) {
|
|
7
|
+
console.log(chalk.gray("GitPT has no saved configuration to reset."));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!options.yes) {
|
|
11
|
+
const { confirm } = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: "confirm",
|
|
14
|
+
name: "confirm",
|
|
15
|
+
message: "Reset GitPT? This clears the saved provider, model, and API key.",
|
|
16
|
+
default: false,
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
if (!confirm) {
|
|
20
|
+
console.log(chalk.gray("Reset cancelled."));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
clearConfig();
|
|
25
|
+
console.log(chalk.green("✓ GitPT configuration reset. Run 'gitpt setup' to reconfigure."));
|
|
26
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
export interface GitPTConfig {
|
|
2
|
-
provider?: "openrouter" | "local";
|
|
2
|
+
provider?: "openrouter" | "local" | "apple" | "openai" | "anthropic";
|
|
3
3
|
customLLMEndpoint?: string;
|
|
4
4
|
model?: string;
|
|
5
5
|
apiKey?: string;
|
|
6
|
+
apiKeys?: Record<string, string>;
|
|
7
|
+
contextWindow?: number;
|
|
6
8
|
}
|
|
7
9
|
export declare const getConfig: () => GitPTConfig;
|
|
8
10
|
export declare const saveConfig: (newConfig: GitPTConfig) => void;
|
|
9
|
-
export declare enum ConfigErrors {
|
|
10
|
-
CustomLLMEndpointRequired = "Custom LLM endpoint is required for local LLM provider.",
|
|
11
|
-
APIKeyRequired = "API key is required for OpenRouter provider.",
|
|
12
|
-
ModelRequired = "Model is required."
|
|
13
|
-
}
|
|
14
|
-
export declare const validateConfig: () => {
|
|
15
|
-
isValid: boolean;
|
|
16
|
-
errors?: string[];
|
|
17
|
-
};
|
|
18
11
|
export declare const clearConfig: () => void;
|
|
12
|
+
export declare const getAcceptedDefault: () => string | undefined;
|
|
13
|
+
export declare const setAcceptedDefault: (id: string) => void;
|
|
14
|
+
export declare const clearAcceptedDefault: () => void;
|
package/dist/config.js
CHANGED
|
@@ -7,11 +7,15 @@ export const getConfig = () => {
|
|
|
7
7
|
const customLLMEndpoint = config.get("customLLMEndpoint");
|
|
8
8
|
const model = config.get("model");
|
|
9
9
|
const apiKey = config.get("apiKey");
|
|
10
|
+
const apiKeys = config.get("apiKeys");
|
|
11
|
+
const contextWindow = config.get("contextWindow");
|
|
10
12
|
return {
|
|
11
13
|
provider,
|
|
12
14
|
customLLMEndpoint,
|
|
13
15
|
model,
|
|
14
16
|
apiKey,
|
|
17
|
+
apiKeys,
|
|
18
|
+
contextWindow,
|
|
15
19
|
};
|
|
16
20
|
}
|
|
17
21
|
catch (error) {
|
|
@@ -32,27 +36,28 @@ export const saveConfig = (newConfig) => {
|
|
|
32
36
|
if (newConfig.apiKey !== undefined) {
|
|
33
37
|
config.set("apiKey", newConfig.apiKey);
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
})(ConfigErrors || (ConfigErrors = {}));
|
|
42
|
-
export const validateConfig = () => {
|
|
43
|
-
const { provider, customLLMEndpoint, apiKey, model } = getConfig();
|
|
44
|
-
const errors = [];
|
|
45
|
-
if (provider === "local" && !customLLMEndpoint) {
|
|
46
|
-
errors.push(ConfigErrors.CustomLLMEndpointRequired);
|
|
47
|
-
}
|
|
48
|
-
if (provider === "openrouter" && !apiKey) {
|
|
49
|
-
errors.push(ConfigErrors.APIKeyRequired);
|
|
50
|
-
}
|
|
51
|
-
if (!model) {
|
|
52
|
-
errors.push(ConfigErrors.ModelRequired);
|
|
53
|
-
}
|
|
54
|
-
return { isValid: errors.length === 0, errors };
|
|
39
|
+
if (newConfig.apiKeys !== undefined) {
|
|
40
|
+
config.set("apiKeys", newConfig.apiKeys);
|
|
41
|
+
}
|
|
42
|
+
if (newConfig.contextWindow !== undefined) {
|
|
43
|
+
config.set("contextWindow", newConfig.contextWindow);
|
|
44
|
+
}
|
|
55
45
|
};
|
|
56
46
|
export const clearConfig = () => {
|
|
57
47
|
config.clear();
|
|
58
48
|
};
|
|
49
|
+
const ACCEPTED_DEFAULT_KEY = "acceptedDefault";
|
|
50
|
+
export const getAcceptedDefault = () => {
|
|
51
|
+
try {
|
|
52
|
+
return config.get(ACCEPTED_DEFAULT_KEY);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export const setAcceptedDefault = (id) => {
|
|
59
|
+
config.set(ACCEPTED_DEFAULT_KEY, id);
|
|
60
|
+
};
|
|
61
|
+
export const clearAcceptedDefault = () => {
|
|
62
|
+
config.delete(ACCEPTED_DEFAULT_KEY);
|
|
63
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { commitCommand } from "./commands/commit/index.js";
|
|
|
7
7
|
import { configCommand } from "./commands/config.js";
|
|
8
8
|
import { modelCommand } from "./commands/model.js";
|
|
9
9
|
import { prCreateCommand } from "./commands/pr/index.js";
|
|
10
|
+
import { resetCommand } from "./commands/reset.js";
|
|
10
11
|
import { setupCommand } from "./commands/setup.js";
|
|
11
12
|
const program = new Command();
|
|
12
13
|
program
|
|
@@ -26,6 +27,11 @@ program
|
|
|
26
27
|
.command("model")
|
|
27
28
|
.description("Change the AI model used for generating commit messages")
|
|
28
29
|
.action(modelCommand);
|
|
30
|
+
program
|
|
31
|
+
.command("reset")
|
|
32
|
+
.description("Reset GitPT configuration (clears provider, model, and API key)")
|
|
33
|
+
.option("-y, --yes", "Skip the confirmation prompt")
|
|
34
|
+
.action(resetCommand);
|
|
29
35
|
// Enhanced git commands
|
|
30
36
|
program
|
|
31
37
|
.command("commit")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
export type ChatCompletionCreateParams = OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming;
|
|
3
|
+
export interface LLMChatCompletion {
|
|
4
|
+
choices: Array<{
|
|
5
|
+
message: {
|
|
6
|
+
content: string | null;
|
|
7
|
+
};
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface LLMModelsPage {
|
|
11
|
+
data: OpenAI.Models.Model[];
|
|
12
|
+
hasNextPage(): boolean;
|
|
13
|
+
getNextPage(): Promise<LLMModelsPage>;
|
|
14
|
+
}
|
|
15
|
+
export interface LLMClient {
|
|
16
|
+
chat: {
|
|
17
|
+
completions: {
|
|
18
|
+
create(body: ChatCompletionCreateParams): Promise<LLMChatCompletion>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
models: {
|
|
22
|
+
list(): Promise<LLMModelsPage>;
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/llm/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { LLMClient } from "./client.js";
|
|
2
2
|
export declare const OPENROUTER_API_URL = "https://openrouter.ai/api/v1";
|
|
3
3
|
export declare const getLLMClient: (options?: {
|
|
4
4
|
baseURLOverride?: string;
|
|
5
|
-
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}) => LLMClient;
|