neuro-commit 0.1.3 → 0.2.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 CHANGED
@@ -8,11 +8,12 @@
8
8
  <a href="https://github.com/cr1ma/neuro-commit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/cr1ma/neuro-commit" alt="license"></a>
9
9
  <a href="https://github.com/cr1ma/neuro-commit/actions/workflows/publish.yml"><img src="https://github.com/cr1ma/neuro-commit/actions/workflows/publish.yml/badge.svg" alt="publish status"></a>
10
10
  <a href="https://github.com/cr1ma/neuro-commit/issues"><img src="https://img.shields.io/github/issues/cr1ma/neuro-commit" alt="open issues"></a>
11
+ <a href="https://github.com/cr1ma/neuro-commit"><img src="https://img.shields.io/github/stars/cr1ma/neuro-commit" alt="stars"></a>
11
12
  </p>
12
13
 
13
14
  ---
14
15
 
15
- **NeuroCommit** is a zero-config CLI tool that analyzes your staged Git changes and generates a clean, structured Markdown summary ready to be fed into any AI/LLM for high-quality commit message generation.
16
+ **NeuroCommit** is a CLI tool that analyzes your staged Git changes and generates commit messages either automatically via OpenAI API or as a structured prompt you can paste into any LLM.
16
17
 
17
18
  <p align="center">
18
19
  <img src="docs/assets/neuro-commit-screenshot.png" alt="NeuroCommit CLI screenshot" width="700">
@@ -23,6 +24,7 @@
23
24
  - [Features](#-features)
24
25
  - [Quick Start](#-quick-start)
25
26
  - [How It Works](#-how-it-works)
27
+ - [Configuration](#-configuration)
26
28
  - [Development](#-development)
27
29
  - [Contributing](#-contributing)
28
30
  - [Security](#-security)
@@ -31,12 +33,14 @@
31
33
 
32
34
  ## ✨ Features
33
35
 
34
- - **Zero configuration** — works out of the box with any Git repository
35
- - **Interactive TUI** — beautiful terminal menu with keyboard navigation
36
- - **Smart lock file handling** — detects lock files (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Cargo.lock`, etc.) and omits their noisy diffs
37
- - **Token estimation** — reports estimated token count (using `o200k_base` tokenizer) so you know the prompt size before pasting into an LLM
38
- - **Structured Markdown output** — generates a `neuro-commit.md` with file list, per-file stats, and full diff
39
- - **Update notifications** — automatically notifies you when a new version is available
36
+ - **AI Commit mode** — generates and commits messages automatically using OpenAI API (`gpt-5-nano`) with Structured Outputs
37
+ - **Manual mode** — saves a prompt to `neuro-commit.md` for pasting into any LLM (ChatGPT, Claude, etc.)
38
+ - **Conventional Commits** — always uses `feat:`, `fix:`, `docs:`, `refactor:`, etc.
39
+ - **Smart lock file handling** — detects lock files and omits their noisy diffs
40
+ - **Minimal UI** — clean terminal interface, no visual clutter
41
+ - **Multi-language** — commit message body in English, Ukrainian, Russian, German, French, or Spanish
42
+ - **Configurable** — auto-commit, auto-push, commit history context, dev mode
43
+ - **Secure** — API key via environment variable only, no shell injection vectors
40
44
 
41
45
  ## 🚀 Quick Start
42
46
 
@@ -58,20 +62,52 @@ Then run:
58
62
  neuro-commit
59
63
  ```
60
64
 
65
+ ### Setting up OpenAI API Key
66
+
67
+ For AI Commit mode, set your API key:
68
+
69
+ ```bash
70
+ # Linux / macOS
71
+ export OPENAI_API_KEY="sk-..."
72
+
73
+ # Windows PowerShell
74
+ $env:OPENAI_API_KEY = "sk-..."
75
+
76
+ # Windows CMD
77
+ set OPENAI_API_KEY=sk-...
78
+ ```
79
+
61
80
  ## 📖 How It Works
62
81
 
82
+ ### AI Commit Mode
83
+
63
84
  1. Stage your changes with `git add`
64
- 2. Run `neuro-commit`
65
- 3. Select **Commit** mode from the interactive menu
66
- 4. The tool collects your staged diff, file list, and per-file stats
67
- 5. A `neuro-commit.md` file is generated in the current directory containing:
68
- - File list with statuses (`Added`, `Modified`, `Deleted`, etc.) and per-file insertions/deletions
69
- - Lock file entries listed without their diffs
70
- - Summary line with total files changed, insertions, and deletions
71
- - Full diff output in a fenced `diff` code block
72
- 6. Copy the contents of `neuro-commit.md` into your preferred AI assistant and ask it to write a commit message
73
-
74
- > **Tip:** Lock files (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `uv.lock`, `Cargo.lock`, and others) are listed as changed but their diffs are omitted to keep the output clean and token-efficient.
85
+ 2. Run `neuro-commit` → select **AI Commit**
86
+ 3. Review the file summary and confirm generation
87
+ 4. The tool sends your diff to OpenAI API and generates a commit message
88
+ 5. Choose: **Commit**, **Edit**, **Regenerate**, or **Cancel**
89
+
90
+ ### Manual Mode
91
+
92
+ 1. Stage your changes with `git add`
93
+ 2. Run `neuro-commit` select **Manual Mode**
94
+ 3. A `neuro-commit.md` file is generated with the full prompt
95
+ 4. Paste into your preferred LLM and get a commit message
96
+
97
+ > Both modes use the same prompt — the only difference is delivery method.
98
+
99
+ ## ⚙️ Configuration
100
+
101
+ Settings are stored in `~/.neurocommit/config.json`. Access via the **Settings** menu.
102
+
103
+ | Setting | Default | Description |
104
+ | -------------- | ------- | -------------------------------------- |
105
+ | Language | `en` | Commit message body language |
106
+ | Max length | `72` | Title character limit |
107
+ | Auto-commit | `OFF` | Commit immediately after generation |
108
+ | Auto-push | `OFF` | Push after committing |
109
+ | Commit history | `5` | Recent commits included as AI context |
110
+ | Dev mode | `OFF` | Store API responses (OpenAI dashboard) |
75
111
 
76
112
  ## 🔧 Development
77
113
 
@@ -1,199 +1,99 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const readline = require("readline");
4
3
  const { runCommitMode } = require("../src/commit");
4
+ const { runAiCommitMode } = require("../src/aiCommit");
5
+ const { runSettingsMenu } = require("../src/settings");
6
+ const { isAiAvailable } = require("../src/config");
7
+ const {
8
+ RESET,
9
+ BOLD,
10
+ DIM,
11
+ RED,
12
+ CYAN,
13
+ SHOW_CURSOR,
14
+ showSelectMenu,
15
+ } = require("../src/ui");
5
16
  const pkg = require("../package.json");
6
17
 
7
- // --- ANSI helpers ---
8
- const RESET = "\x1b[0m";
9
- const BOLD = "\x1b[1m";
10
- const DIM = "\x1b[2m";
11
- const GREEN = "\x1b[32m";
12
- const YELLOW = "\x1b[33m";
13
- const CYAN = "\x1b[36m";
14
- const HIDE_CURSOR = "\x1b[?25l";
15
- const SHOW_CURSOR = "\x1b[?25h";
16
-
17
- const banner = `
18
- ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗██╗████████╗
19
- ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██║╚══██╔══╝
20
- ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║█████╗██║ ██║ ██║██╔████╔██║██╔████╔██║██║ ██║
21
- ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════╝██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██║ ██║
22
- ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║
23
- ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
24
- `;
25
-
26
- // --- CLI flags ---
27
18
  const args = process.argv.slice(2);
28
19
 
29
20
  if (args.includes("--help") || args.includes("-h")) {
30
- console.log(`
31
- ${BOLD}neuro-commit${RESET} v${pkg.version} — AI-powered commit message generator
32
-
33
- ${BOLD}USAGE${RESET}
34
- neuro-commit Launch interactive mode
35
- neuro-commit [options]
36
-
37
- ${BOLD}OPTIONS${RESET}
38
- -h, --help Show this help message
39
- -v, --version Show installed version
40
-
41
- ${BOLD}WORKFLOW${RESET}
42
- 1. Stage your changes ${DIM}git add <files>${RESET}
43
- 2. Run neuro-commit ${DIM}neuro-commit${RESET}
44
- 3. Copy generated file ${DIM}neuro-commit.md${RESET}
45
- 4. Paste into your LLM ${DIM}(ChatGPT, Claude, etc.)${RESET}
46
- 5. Get your commit message!
47
-
48
- ${BOLD}LINKS${RESET}
49
- Repository ${CYAN}${pkg.homepage}${RESET}
50
- Issues ${CYAN}${pkg.bugs.url}${RESET}
51
- `);
21
+ console.log(`\n${BOLD}neuro-commit${RESET} v${pkg.version}\n`);
22
+ console.log(`Usage: neuro-commit`);
23
+ console.log(`Flags: -h, --help | -v, --version\n`);
52
24
  process.exit(0);
53
25
  }
54
26
 
55
27
  if (args.includes("--version") || args.includes("-v")) {
56
- console.log(`neuro-commit v${pkg.version}`);
28
+ console.log(`v${pkg.version}`);
57
29
  process.exit(0);
58
30
  }
59
31
 
60
- // --- Menu options ---
61
- const menuItems = [
62
- { label: "Commit", icon: "📝", description: "Generate a commit message" },
63
- ];
64
-
65
- // Ensure cursor is restored if the process exits unexpectedly
66
- process.on("exit", () => {
67
- process.stdout.write(SHOW_CURSOR);
68
- });
32
+ process.on("exit", () => process.stdout.write(SHOW_CURSOR));
33
+ process.on("SIGINT", () => process.exit(0));
69
34
 
70
- process.on("SIGINT", () => {
71
- process.exit(0);
72
- });
73
-
74
- function renderMenu(selectedIndex) {
75
- const lines = menuItems.length + 2;
76
- process.stdout.write(`\x1b[${lines}A`);
77
-
78
- process.stdout.write(
79
- `${CYAN}?${RESET} ${BOLD}Select mode:${RESET} ${DIM}(use arrow keys)${RESET}\n\n`,
80
- );
81
-
82
- for (let i = 0; i < menuItems.length; i++) {
83
- const { label, icon, description } = menuItems[i];
84
- if (i === selectedIndex) {
85
- process.stdout.write(
86
- ` ${GREEN}❯ ${icon} ${BOLD}${label}${RESET} ${DIM}— ${description}${RESET}\n`,
87
- );
88
- } else {
89
- process.stdout.write(
90
- ` ${icon} ${label} ${DIM}— ${description}${RESET}\n`,
35
+ /**
36
+ * Non-blocking version check — runs once, prints hint if outdated.
37
+ */
38
+ async function checkForUpdate() {
39
+ try {
40
+ const { default: latestVersion } = await import("latest-version");
41
+ const latest = await latestVersion(pkg.name);
42
+ if (latest && latest !== pkg.version) {
43
+ console.log(
44
+ `${DIM}Update available: ${pkg.version} ${CYAN}${latest}${RESET}${DIM} (npm i -g ${pkg.name})${RESET}`,
91
45
  );
92
46
  }
47
+ } catch {
48
+ // silently ignore network errors
93
49
  }
94
50
  }
95
51
 
96
- function showMenu() {
97
- return new Promise((resolve) => {
98
- let selected = 0;
99
-
100
- if (!process.stdin.isTTY) {
101
- console.error("Error: interactive mode requires a TTY terminal.");
102
- process.exit(1);
103
- }
104
-
105
- process.stdout.write(HIDE_CURSOR);
106
-
107
- process.stdout.write("\n".repeat(menuItems.length + 2));
108
- renderMenu(selected);
109
-
110
- readline.emitKeypressEvents(process.stdin);
111
- process.stdin.setRawMode(true);
112
- process.stdin.resume();
113
-
114
- function onKeyPress(str, key) {
115
- if (!key) return;
116
-
117
- if ((key.ctrl && key.name === "c") || key.name === "q") {
118
- cleanup();
119
- console.log("\n👋 Goodbye!");
120
- process.exit(0);
121
- }
122
-
123
- if (key.name === "up" || key.name === "k") {
124
- selected = (selected - 1 + menuItems.length) % menuItems.length;
125
- renderMenu(selected);
52
+ async function main() {
53
+ while (true) {
54
+ console.clear();
55
+ console.log(
56
+ `\n${BOLD}neuro-commit${RESET} ${DIM}v${pkg.version}${RESET}\n`,
57
+ );
58
+
59
+ // fire-and-forget update check (only first iteration matters visually)
60
+ checkForUpdate();
61
+
62
+ const choice = await showSelectMenu("Mode:", [
63
+ { label: "AI Commit", description: "generate & commit" },
64
+ { label: "Manual Mode", description: "save prompt to .md" },
65
+ { label: "Settings" },
66
+ ]);
67
+
68
+ switch (choice) {
69
+ case 0: {
70
+ if (!isAiAvailable()) {
71
+ console.log(
72
+ `\n${RED}✖${RESET} Set ${CYAN}OPENAI_API_KEY${RESET} env variable first.`,
73
+ );
74
+ console.log(
75
+ `${DIM}PowerShell: $env:OPENAI_API_KEY = "sk-..."${RESET}`,
76
+ );
77
+ console.log(
78
+ `${DIM}Linux/macOS: export OPENAI_API_KEY="sk-..."${RESET}\n`,
79
+ );
80
+ await new Promise((r) => setTimeout(r, 2000));
81
+ break;
82
+ }
83
+ console.clear();
84
+ await runAiCommitMode();
126
85
  return;
127
86
  }
128
-
129
- if (key.name === "down" || key.name === "j") {
130
- selected = (selected + 1) % menuItems.length;
131
- renderMenu(selected);
87
+ case 1:
88
+ console.clear();
89
+ runCommitMode();
132
90
  return;
133
- }
134
-
135
- if (key.name === "return") {
136
- cleanup();
137
- resolve(selected);
91
+ case 2:
92
+ await runSettingsMenu();
93
+ break;
94
+ default:
138
95
  return;
139
- }
140
- }
141
-
142
- function cleanup() {
143
- process.stdin.removeListener("keypress", onKeyPress);
144
- process.stdin.setRawMode(false);
145
- process.stdin.pause();
146
- process.stdout.write(SHOW_CURSOR);
147
- }
148
-
149
- process.stdin.on("keypress", onKeyPress);
150
- });
151
- }
152
-
153
- // --- Entry point ---
154
- async function main() {
155
- console.clear();
156
- console.log(banner);
157
-
158
- try {
159
- const { default: updateNotifier } = await import("update-notifier");
160
-
161
- const notifier = updateNotifier({
162
- pkg,
163
- updateCheckInterval: 0,
164
- });
165
-
166
- // Cache may be empty on first run — fetch directly as fallback
167
- let updateInfo = notifier.update;
168
- if (!updateInfo) {
169
- try {
170
- updateInfo = await notifier.fetchInfo();
171
- if (updateInfo.type === "latest") {
172
- updateInfo = null;
173
- }
174
- } catch {
175
- // Network error — skip silently
176
- }
177
96
  }
178
-
179
- if (updateInfo) {
180
- console.log(
181
- ` ${YELLOW}Update available!${RESET} ${DIM}${updateInfo.current}${RESET} → ${GREEN}${updateInfo.latest}${RESET}`,
182
- );
183
- console.log(
184
- ` Run ${CYAN}npm install -g neuro-commit${RESET} to update\n`,
185
- );
186
- }
187
- } catch {
188
- // Ignore update check errors
189
- }
190
-
191
- const choice = await showMenu();
192
-
193
- switch (choice) {
194
- case 0:
195
- runCommitMode();
196
- break;
197
97
  }
198
98
  }
199
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuro-commit",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "neuro-commit CLI utility",
5
5
  "author": "cr1ma.dev",
6
6
  "bin": {
@@ -38,7 +38,9 @@
38
38
  "globals": "^17.3.0"
39
39
  },
40
40
  "dependencies": {
41
+ "latest-version": "^9.0.0",
42
+ "openai": "^6.22.0",
41
43
  "tiktoken": "^1.0.22",
42
- "update-notifier": "^7.3.1"
44
+ "zod": "^4.3.6"
43
45
  }
44
46
  }
package/src/ai.js ADDED
@@ -0,0 +1,234 @@
1
+ const OpenAI = require("openai");
2
+ const { z } = require("zod");
3
+ const { zodTextFormat } = require("openai/helpers/zod");
4
+ const { get_encoding } = require("tiktoken");
5
+ const { getApiKey, loadConfig } = require("./config");
6
+ const { isLockFile, statusLabel } = require("./git");
7
+
8
+ // Lazy-initialized tiktoken encoder (o200k_base for GPT-5 / GPT-4o family)
9
+ let _encoder = null;
10
+
11
+ /**
12
+ * Count tokens accurately using tiktoken (o200k_base).
13
+ */
14
+ function countTokens(text) {
15
+ if (!_encoder) _encoder = get_encoding("o200k_base");
16
+ return _encoder.encode(text).length;
17
+ }
18
+
19
+ // Pricing per 1M tokens — gpt-5-nano
20
+ const MODEL_PRICING = {
21
+ input: 0.05,
22
+ cachedInput: 0.005,
23
+ output: 0.4,
24
+ };
25
+
26
+ // Structured output schema
27
+ const CommitMessage = z.object({
28
+ title: z
29
+ .string()
30
+ .describe("Commit title line (max ~72 chars, imperative mood, no period)"),
31
+ body: z
32
+ .array(z.string())
33
+ .describe("Bullet points describing key changes (without leading dash)"),
34
+ });
35
+
36
+ /**
37
+ * Build a file summary string for the prompt.
38
+ */
39
+ function buildFilesInfo(stagedFiles, numstat) {
40
+ const statMap = new Map();
41
+ for (const entry of numstat) {
42
+ statMap.set(entry.file, entry);
43
+ }
44
+
45
+ const lines = [];
46
+ for (const { status, file } of stagedFiles) {
47
+ const label = statusLabel(status).padEnd(10);
48
+ const s = statMap.get(file);
49
+ if (isLockFile(file)) {
50
+ lines.push(` ${label} ${file} | lock file (diff omitted)`);
51
+ } else {
52
+ const stat = s ? `| +${s.added} -${s.deleted}` : "";
53
+ lines.push(` ${label} ${file} ${stat}`);
54
+ }
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+
59
+ /**
60
+ * Build system prompt. Always uses Conventional Commits.
61
+ */
62
+ function buildSystemPrompt(config, context = {}) {
63
+ const { language, maxLength } = config;
64
+
65
+ let langInstruction = "";
66
+ if (language && language !== "en") {
67
+ langInstruction = `\nWrite the commit message body in ${language} language. Keep the type prefix in English.`;
68
+ }
69
+
70
+ let branchContext = "";
71
+ if (context.branch) {
72
+ branchContext = `\nCurrent branch: ${context.branch}`;
73
+ }
74
+
75
+ let historyContext = "";
76
+ if (context.recentCommits && context.recentCommits.length > 0) {
77
+ historyContext = `\nRecent commits for style reference:\n${context.recentCommits.map((c) => ` - ${c}`).join("\n")}`;
78
+ }
79
+
80
+ return `You are an expert at writing clear, concise git commit messages.
81
+
82
+ Rules:
83
+ 1. Use Conventional Commits: feat:, fix:, docs:, style:, refactor:, test:, chore:, perf:, ci:, build:
84
+ 2. If changes affect a specific scope, use parentheses: feat(auth): ...
85
+ 3. Title max ${maxLength} chars, imperative mood, no period at end.
86
+ 4. Body: concise bullet points for key changes.
87
+ 5. Be specific — WHAT changed and WHY.
88
+ 6. Omit file paths unless essential. Omit lock file changes.
89
+ ${langInstruction}
90
+ ${branchContext}
91
+ ${historyContext}`.trim();
92
+ }
93
+
94
+ /**
95
+ * Build user prompt with the diff content.
96
+ */
97
+ function buildUserPrompt(filesInfo, diff) {
98
+ return `Staged changes:
99
+
100
+ Files:
101
+ ${filesInfo}
102
+
103
+ Diff:
104
+ \`\`\`diff
105
+ ${diff}
106
+ \`\`\`
107
+
108
+ Generate a commit message.`;
109
+ }
110
+
111
+ /**
112
+ * Calculate cost from real API usage data.
113
+ */
114
+ function calculateCost(usage) {
115
+ const inputTokens = usage.input_tokens || 0;
116
+ const cachedTokens = usage.input_tokens_details?.cached_tokens || 0;
117
+ const uncachedInput = inputTokens - cachedTokens;
118
+ const outputTokens = usage.output_tokens || 0;
119
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
120
+ const totalTokens = usage.total_tokens || 0;
121
+
122
+ const cost =
123
+ (uncachedInput / 1_000_000) * MODEL_PRICING.input +
124
+ (cachedTokens / 1_000_000) * MODEL_PRICING.cachedInput +
125
+ (outputTokens / 1_000_000) * MODEL_PRICING.output;
126
+
127
+ return {
128
+ cost,
129
+ inputTokens,
130
+ cachedTokens,
131
+ uncachedInput,
132
+ outputTokens,
133
+ reasoningTokens,
134
+ totalTokens,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Rough cost estimate before making a request.
140
+ */
141
+ function estimateCost(estimatedInputTokens, estimatedOutputTokens = 200) {
142
+ return (
143
+ (estimatedInputTokens / 1_000_000) * MODEL_PRICING.input +
144
+ (estimatedOutputTokens / 1_000_000) * MODEL_PRICING.output
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Format structured response into conventional commit message string.
150
+ */
151
+ function formatCommitMessage(parsed) {
152
+ const lines = [parsed.title];
153
+ if (parsed.body && parsed.body.length > 0) {
154
+ lines.push("");
155
+ for (const point of parsed.body) {
156
+ lines.push(`- ${point}`);
157
+ }
158
+ }
159
+ return lines.join("\n");
160
+ }
161
+
162
+ /**
163
+ * Extract parsed content from Structured Outputs response.
164
+ * Handles refusals gracefully.
165
+ */
166
+ function extractParsedContent(response) {
167
+ for (const output of response.output) {
168
+ if (output.type !== "message") continue;
169
+ for (const item of output.content) {
170
+ if (item.type === "refusal") {
171
+ throw new Error(`Model refused: ${item.refusal}`);
172
+ }
173
+ if (item.parsed) {
174
+ return item.parsed;
175
+ }
176
+ }
177
+ }
178
+ throw new Error("Could not parse structured response.");
179
+ }
180
+
181
+ /**
182
+ * Generate a commit message via OpenAI Responses API with Structured Outputs.
183
+ * Returns { message, usage }.
184
+ */
185
+ async function generateCommitMessage(
186
+ diff,
187
+ filesInfo,
188
+ context = {},
189
+ extraInstruction = "",
190
+ ) {
191
+ const apiKey = getApiKey();
192
+ if (!apiKey) {
193
+ throw new Error("OPENAI_API_KEY not set.");
194
+ }
195
+
196
+ const config = loadConfig();
197
+ const client = new OpenAI({ apiKey });
198
+
199
+ const systemPrompt = buildSystemPrompt(config, context) + extraInstruction;
200
+ const userPrompt = buildUserPrompt(filesInfo, diff);
201
+
202
+ const params = {
203
+ model: config.model,
204
+ instructions: systemPrompt,
205
+ input: userPrompt,
206
+ reasoning: { effort: "low" },
207
+ text: {
208
+ format: zodTextFormat(CommitMessage, "commit_message"),
209
+ },
210
+ };
211
+
212
+ if (config.devMode) {
213
+ params.store = true;
214
+ }
215
+
216
+ const response = await client.responses.parse(params);
217
+ const parsed = extractParsedContent(response);
218
+
219
+ return {
220
+ message: formatCommitMessage(parsed),
221
+ usage: response.usage || null,
222
+ };
223
+ }
224
+
225
+ module.exports = {
226
+ buildFilesInfo,
227
+ buildSystemPrompt,
228
+ buildUserPrompt,
229
+ generateCommitMessage,
230
+ calculateCost,
231
+ estimateCost,
232
+ countTokens,
233
+ MODEL_PRICING,
234
+ };