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.
Files changed (60) hide show
  1. package/README.md +7 -0
  2. package/dist/commands/commit/context/buildPrompt.d.ts +4 -0
  3. package/dist/commands/commit/context/buildPrompt.js +13 -0
  4. package/dist/commands/commit/context/summaryPrompt.d.ts +2 -0
  5. package/dist/commands/commit/context/summaryPrompt.js +17 -0
  6. package/dist/commands/commit/generateCommitMessage.js +8 -26
  7. package/dist/commands/commit/index.js +4 -2
  8. package/dist/commands/commit/summarizeDiff.d.ts +1 -0
  9. package/dist/commands/commit/summarizeDiff.js +172 -0
  10. package/dist/commands/middleware/setupMiddleware/defaultModels.d.ts +8 -0
  11. package/dist/commands/middleware/setupMiddleware/defaultModels.js +11 -0
  12. package/dist/commands/middleware/setupMiddleware/index.js +58 -24
  13. package/dist/commands/pr/generatePRDetails.js +7 -14
  14. package/dist/commands/reset.d.ts +3 -0
  15. package/dist/commands/reset.js +26 -0
  16. package/dist/config.d.ts +6 -10
  17. package/dist/config.js +25 -20
  18. package/dist/index.js +6 -0
  19. package/dist/llm/client.d.ts +24 -0
  20. package/dist/llm/index.d.ts +3 -2
  21. package/dist/llm/index.js +4 -9
  22. package/dist/llm/providers/anthropic/index.d.ts +9 -0
  23. package/dist/llm/providers/anthropic/index.js +31 -0
  24. package/dist/llm/providers/apiKey.d.ts +3 -0
  25. package/dist/llm/providers/apiKey.js +40 -0
  26. package/dist/llm/providers/apple/client.d.ts +3 -0
  27. package/dist/llm/providers/apple/client.js +87 -0
  28. package/dist/llm/providers/apple/index.d.ts +13 -0
  29. package/dist/llm/providers/apple/index.js +77 -0
  30. package/dist/llm/providers/apple/models.d.ts +14 -0
  31. package/dist/llm/providers/apple/models.js +21 -0
  32. package/dist/llm/providers/base.d.ts +30 -0
  33. package/dist/llm/providers/base.js +36 -0
  34. package/dist/llm/providers/local/index.d.ts +11 -0
  35. package/dist/llm/providers/local/index.js +96 -0
  36. package/dist/llm/providers/openai/index.d.ts +10 -0
  37. package/dist/llm/providers/openai/index.js +16 -0
  38. package/dist/llm/providers/openaiCompatible.d.ts +15 -0
  39. package/dist/llm/providers/openaiCompatible.js +69 -0
  40. package/dist/llm/providers/openrouter/index.d.ts +9 -0
  41. package/dist/llm/providers/openrouter/index.js +16 -0
  42. package/dist/llm/registry.d.ts +8 -0
  43. package/dist/llm/registry.js +43 -0
  44. package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.d.ts +1 -0
  45. package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.js +3 -3
  46. package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.d.ts +1 -1
  47. package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.js +13 -3
  48. package/dist/llm/setup/types.js +1 -0
  49. package/dist/llm/tokenCount.d.ts +3 -0
  50. package/dist/llm/tokenCount.js +4 -0
  51. package/dist/services/git/getStagedChanges.js +1 -1
  52. package/package.json +6 -2
  53. package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.d.ts +0 -1
  54. package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.js +0 -39
  55. package/dist/commands/middleware/setupMiddleware/setupLocalLLM.d.ts +0 -5
  56. package/dist/commands/middleware/setupMiddleware/setupLocalLLM.js +0 -60
  57. package/dist/commands/middleware/setupMiddleware/setupOpenRouter.d.ts +0 -2
  58. package/dist/commands/middleware/setupMiddleware/setupOpenRouter.js +0 -66
  59. /package/dist/{commands/middleware/setupMiddleware/types.js → llm/client.js} +0 -0
  60. /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,4 @@
1
+ export declare const buildCommitPrompt: (diff: string, validationErrors?: string) => {
2
+ system: string;
3
+ user: string;
4
+ };
@@ -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 { getLLMClient } from "../../llm/index.js";
3
- import { getCommitlintRules, hasCommitlintConfig, } from "../../utils/commitlint.js";
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 { model } = config;
15
- const baseRules = hasCommitlint ? getCommitlintRules() : "";
16
- const errorInfo = validationErrors
17
- ? `\n\nYOUR PREVIOUS MESSAGE FAILED VALIDATION WITH THESE ERRORS:\n${validationErrors}\n\nFIX THESE ISSUES IN YOUR NEW MESSAGE.`
18
- : "";
19
- const llmClient = getLLMClient();
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(diff);
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(diff, validationErrors);
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,8 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ export interface DefaultModel {
3
+ id: string;
4
+ label: string;
5
+ isAvailable: () => boolean;
6
+ config: GitPTConfig;
7
+ }
8
+ export declare const resolveDefaultModel: () => DefaultModel | undefined;
@@ -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, validateConfig } from "../../../config.js";
3
- import { setupLocalLLM } from "./setupLocalLLM.js";
4
- import { setupOpenRouter } from "./setupOpenRouter.js";
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
- // If we're running a command, and the config is valid, continue with the command
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
- const isValidConfig = validateConfig();
12
- if (isValidConfig.isValid) {
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
- // For initial setup or model command, start by selecting the provider
17
- const useLocalLLMAnswer = await inquirer.prompt([
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: "useLocalLLM",
62
+ name: "provider",
21
63
  message: "Select LLM provider:",
22
- choices: [
23
- { name: "OpenRouter (remote)", value: false },
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
- // Update config based on selected provider
30
- existingConfig.provider = useLocalLLMAnswer.useLocalLLM
31
- ? "local"
32
- : "openrouter";
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 { getConfig } from "../../config.js";
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 response = await llmClient.chat.completions.create({
14
- model: model,
15
- messages: [
16
- { role: "system", content: systemPrompt },
17
- { role: "user", content: userPromptWithContext },
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,3 @@
1
+ export declare const resetCommand: (options?: {
2
+ yes?: boolean;
3
+ }) => Promise<void>;
@@ -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
- export var ConfigErrors;
37
- (function (ConfigErrors) {
38
- ConfigErrors["CustomLLMEndpointRequired"] = "Custom LLM endpoint is required for local LLM provider.";
39
- ConfigErrors["APIKeyRequired"] = "API key is required for OpenRouter provider.";
40
- ConfigErrors["ModelRequired"] = "Model is required.";
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
+ }
@@ -1,5 +1,6 @@
1
- import openai from "openai";
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
- }) => openai;
5
+ apiKey?: string;
6
+ }) => LLMClient;