@kyubiware/commit-mint 0.12.0 → 0.13.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 +24 -7
- package/dist/bin.mjs +157 -15
- package/dist/bin.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -201,11 +201,15 @@ Stored in `~/.commit-mint` (INI format). Run `cmint config` to edit.
|
|
|
201
201
|
|
|
202
202
|
| Key | Default | Description |
|
|
203
203
|
|---|---|---|
|
|
204
|
-
| `provider` | `groq` | `groq`, `cerebras`, or `
|
|
204
|
+
| `provider` | `groq` | `groq`, `cerebras`, `mistral`, `commandcode`, `omniroute`, `openrouter`, or `gemini` |
|
|
205
205
|
| `model` | `openai/gpt-oss-20b` | Default model; overridable per provider |
|
|
206
206
|
| `model_groq` | — | Override default when provider is `groq` |
|
|
207
207
|
| `model_cerebras` | — | Override default when provider is `cerebras` |
|
|
208
208
|
| `model_mistral` | — | Override default when provider is `mistral` |
|
|
209
|
+
| `model_commandcode` | — | Override default when provider is `commandcode` |
|
|
210
|
+
| `model_omniroute` | — | Override default when provider is `omniroute` |
|
|
211
|
+
| `model_openrouter` | — | Override default when provider is `openrouter` |
|
|
212
|
+
| `model_gemini` | — | Override default when provider is `gemini` |
|
|
209
213
|
| `locale` | `en` | Locale for generated messages |
|
|
210
214
|
| `max-length` | `100` | Max commit message length |
|
|
211
215
|
| `type` | — | Force commit type prefix |
|
|
@@ -223,6 +227,10 @@ API key lookup checks the env var first, then the INI file.
|
|
|
223
227
|
| `GROQ_API_KEY` | `cmint` (when `provider=groq`) | Yes, unless set in `~/.commit-mint` |
|
|
224
228
|
| `CEREBRAS_API_KEY` | `cmint` (when `provider=cerebras`) | Yes, unless set in `~/.commit-mint` |
|
|
225
229
|
| `MISTRAL_API_KEY` | `cmint` (when `provider=mistral`) | Yes, unless set in `~/.commit-mint` |
|
|
230
|
+
| `COMMANDCODE_API_KEY` | `cmint` (when `provider=commandcode`) | Yes, unless set in `~/.commit-mint` |
|
|
231
|
+
| `OMNIROUTE_API_KEY` | `cmint` (when `provider=omniroute`) | Optional — local gateway, no key needed |
|
|
232
|
+
| `OPENROUTER_API_KEY` | `cmint` (when `provider=openrouter`) | Yes, unless set in `~/.commit-mint` |
|
|
233
|
+
| `GEMINI_API_KEY` | `cmint` (when `provider=gemini`) | Yes, unless set in `~/.commit-mint` |
|
|
226
234
|
| `https_proxy` / `HTTPS_PROXY` | All providers | Optional — overrides `proxy` config key |
|
|
227
235
|
| `NO_COLOR` | All UI | Optional — disables `kolorist` color output |
|
|
228
236
|
|
|
@@ -233,11 +241,19 @@ API key lookup checks the env var first, then the INI file.
|
|
|
233
241
|
| `groq` | `GROQ_API_KEY` | `openai/gpt-oss-20b` | `groq-sdk` |
|
|
234
242
|
| `cerebras` | `CEREBRAS_API_KEY` | `gpt-oss-120b` | Built-in fetch |
|
|
235
243
|
| `mistral` | `MISTRAL_API_KEY` | `mistral-small` | Built-in fetch |
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
| `commandcode` | `COMMANDCODE_API_KEY` | `deepseek/deepseek-v4-flash` | Built-in fetch |
|
|
245
|
+
| `omniroute` | `OMNIROUTE_API_KEY` | `auto/fast` | Built-in fetch (none — local gateway) |
|
|
246
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `openrouter/free` | Built-in fetch |
|
|
247
|
+
| `gemini` | `GEMINI_API_KEY` | `gemini-3.8-flash` | Built-in fetch |
|
|
248
|
+
|
|
249
|
+
All seven use OpenAI-compatible APIs and most have a generous free tier. Per-
|
|
250
|
+
provider model overrides: set `model_groq`, `model_cerebras`, `model_mistral`,
|
|
251
|
+
`model_commandcode`, `model_omniroute`, `model_openrouter`, or `model_gemini`
|
|
239
252
|
in `~/.commit-mint`. Resolution order is `model_<provider>` → `model` →
|
|
240
|
-
provider default.
|
|
253
|
+
provider default. OpenRouter and Gemini expose live model lists in `cmint
|
|
254
|
+
config` — ids tagged free (`:free`/`-free`, plus OpenRouter's `openrouter/free`
|
|
255
|
+
router) sort first and are labeled `(free)`. Gemini ids carry no free marker,
|
|
256
|
+
so its list keeps the provider's server order.
|
|
241
257
|
|
|
242
258
|
### Pre-flight Check Config
|
|
243
259
|
|
|
@@ -387,8 +403,9 @@ cmint logs -n 50 # last 50 lines
|
|
|
387
403
|
|
|
388
404
|
- [x] Node.js 18+
|
|
389
405
|
- [x] git
|
|
390
|
-
- [x] One of: `GROQ_API_KEY`, `CEREBRAS_API_KEY`,
|
|
391
|
-
(or run `cmint config` to set one
|
|
406
|
+
- [x] One of: `GROQ_API_KEY`, `CEREBRAS_API_KEY`, `MISTRAL_API_KEY`, or
|
|
407
|
+
`COMMANDCODE_API_KEY` (or run `cmint config` to set one; `omniroute`
|
|
408
|
+
needs no key)
|
|
392
409
|
- [x] Optional, for clipboard copy: `wl-copy`, `xclip`, `xsel`, or `pbcopy`
|
|
393
410
|
|
|
394
411
|
## Support
|
package/dist/bin.mjs
CHANGED
|
@@ -33,7 +33,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
33
33
|
//#region package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "@kyubiware/commit-mint",
|
|
36
|
-
version: "0.
|
|
36
|
+
version: "0.13.0",
|
|
37
37
|
description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
|
|
38
38
|
type: "module",
|
|
39
39
|
bin: { "cmint": "./dist/bin.mjs" },
|
|
@@ -43,6 +43,7 @@ var package_default = {
|
|
|
43
43
|
"dev": "tsx src/bin.ts",
|
|
44
44
|
"dev:auto": "tsx src/bin.ts -a",
|
|
45
45
|
"dev:debug": "tsx src/bin.ts --debug",
|
|
46
|
+
"dev:config": "tsx src/bin.ts config",
|
|
46
47
|
"lint": "biome check .",
|
|
47
48
|
"lint:fix": "biome check --write --unsafe .",
|
|
48
49
|
"typecheck": "tsc --noEmit",
|
|
@@ -136,10 +137,30 @@ const PROVIDER_CONFIGS = {
|
|
|
136
137
|
baseURL: "https://api.mistral.ai/v1",
|
|
137
138
|
defaultModel: "mistral-small"
|
|
138
139
|
},
|
|
140
|
+
commandcode: {
|
|
141
|
+
baseURL: "https://api.commandcode.ai/provider/v1",
|
|
142
|
+
defaultModel: "deepseek/deepseek-v4-flash",
|
|
143
|
+
supportsModelList: true,
|
|
144
|
+
displayName: "Command Code",
|
|
145
|
+
modelListExclude: /^claude/
|
|
146
|
+
},
|
|
139
147
|
omniroute: {
|
|
140
148
|
baseURL: "http://localhost:20128/v1",
|
|
141
|
-
defaultModel: "auto",
|
|
149
|
+
defaultModel: "auto/fast",
|
|
142
150
|
requiresApiKey: false
|
|
151
|
+
},
|
|
152
|
+
openrouter: {
|
|
153
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
154
|
+
defaultModel: "openrouter/free",
|
|
155
|
+
supportsModelList: true,
|
|
156
|
+
displayName: "OpenRouter"
|
|
157
|
+
},
|
|
158
|
+
gemini: {
|
|
159
|
+
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
160
|
+
defaultModel: "gemini-3.8-flash",
|
|
161
|
+
supportsModelList: true,
|
|
162
|
+
displayName: "Gemini",
|
|
163
|
+
modelListExclude: /embedding|image|tts|veo|imagen|nano-banana|audio/i
|
|
143
164
|
}
|
|
144
165
|
};
|
|
145
166
|
const ALLOWED_PROVIDERS = Object.keys(PROVIDER_CONFIGS);
|
|
@@ -147,19 +168,33 @@ const PROVIDER_ENV_KEYS = {
|
|
|
147
168
|
groq: "GROQ_API_KEY",
|
|
148
169
|
cerebras: "CEREBRAS_API_KEY",
|
|
149
170
|
mistral: "MISTRAL_API_KEY",
|
|
150
|
-
|
|
171
|
+
commandcode: "COMMANDCODE_API_KEY",
|
|
172
|
+
omniroute: "OMNIROUTE_API_KEY",
|
|
173
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
174
|
+
gemini: "GEMINI_API_KEY"
|
|
151
175
|
};
|
|
152
176
|
function formatProviderName(provider) {
|
|
177
|
+
const displayName = PROVIDER_CONFIGS[provider]?.displayName;
|
|
178
|
+
if (displayName) return displayName;
|
|
153
179
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
154
180
|
}
|
|
155
181
|
function isValidProvider(name) {
|
|
156
182
|
return ALLOWED_PROVIDERS.includes(name);
|
|
157
183
|
}
|
|
158
184
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
185
|
+
* Parse a chat-completions JSON response and normalize gateway quirks:
|
|
186
|
+
* non-2xx → thrown Error, DeepSeek-style `reasoning_content` → OpenAI-style
|
|
187
|
+
* `reasoning` so downstream fallbacks see one field name.
|
|
162
188
|
*/
|
|
189
|
+
async function readCompletion(response) {
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
const text = await response.text().catch(() => "");
|
|
192
|
+
throw new Error(`${response.status} ${text}`);
|
|
193
|
+
}
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
for (const choice of data.choices ?? []) if (!choice.message?.reasoning && choice.message?.reasoning_content) choice.message.reasoning = choice.message.reasoning_content;
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
163
198
|
function createFetchClient(baseURL, apiKey, timeout) {
|
|
164
199
|
return { chat: { completions: { async create(params) {
|
|
165
200
|
const url = `${baseURL}/chat/completions`;
|
|
@@ -167,20 +202,21 @@ function createFetchClient(baseURL, apiKey, timeout) {
|
|
|
167
202
|
const controller = new AbortController();
|
|
168
203
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
169
204
|
try {
|
|
170
|
-
|
|
205
|
+
return await readCompletion(await fetch(url, {
|
|
171
206
|
method: "POST",
|
|
172
207
|
headers: {
|
|
173
208
|
"Content-Type": "application/json",
|
|
174
209
|
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
175
210
|
},
|
|
176
|
-
body: JSON.stringify(
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
...params,
|
|
213
|
+
stream: false
|
|
214
|
+
}),
|
|
177
215
|
signal: controller.signal
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
return await response.json();
|
|
216
|
+
}));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timed out after ${timeout}ms`);
|
|
219
|
+
throw error;
|
|
184
220
|
} finally {
|
|
185
221
|
clearTimeout(timer);
|
|
186
222
|
}
|
|
@@ -204,6 +240,42 @@ function createProvider(options) {
|
|
|
204
240
|
model
|
|
205
241
|
};
|
|
206
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Fetch a provider's live model list via GET {baseURL}/models (standard OpenAI
|
|
245
|
+
* list shape). Sends `Authorization: Bearer <key>` only when a key is
|
|
246
|
+
* provided; throws on non-2xx responses and malformed payloads. Server order
|
|
247
|
+
* is preserved (curated, flagship models first).
|
|
248
|
+
*/
|
|
249
|
+
async function fetchProviderModels(provider, apiKey = "", timeoutMs = 15e3) {
|
|
250
|
+
const url = `${PROVIDER_CONFIGS[provider].baseURL}/models`;
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
headers: { ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {} },
|
|
253
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) throw new Error(`Failed to fetch models from ${provider} (HTTP ${response.status})`);
|
|
256
|
+
const json = await response.json();
|
|
257
|
+
if (!Array.isArray(json.data)) throw new Error(`Invalid model list response from ${provider}: expected { data: [...] }`);
|
|
258
|
+
const models = [];
|
|
259
|
+
for (const entry of json.data) {
|
|
260
|
+
if (entry.id === void 0 || entry.id === null) continue;
|
|
261
|
+
models.push({
|
|
262
|
+
id: String(entry.id),
|
|
263
|
+
name: typeof entry.name === "string" ? entry.name : void 0,
|
|
264
|
+
contextLength: typeof entry.context_length === "number" ? entry.context_length : void 0
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return models;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Free models are flagged in the id by the provider's own convention — a
|
|
271
|
+
* `-free` / `:free` suffix (e.g. `poolside/laguna-s-2.1-free`,
|
|
272
|
+
* `meituan/LongCat-2.0:free`), matching the models CommandCode's pricing docs
|
|
273
|
+
* list as free. OpenRouter's `openrouter/free` router id counts as free by
|
|
274
|
+
* exact match.
|
|
275
|
+
*/
|
|
276
|
+
function isFreeModelId(id) {
|
|
277
|
+
return id === "openrouter/free" || /[:-]free$/i.test(id);
|
|
278
|
+
}
|
|
207
279
|
//#endregion
|
|
208
280
|
//#region src/services/config.ts
|
|
209
281
|
const CONFIG_PATH = join(os.homedir(), ".commit-mint");
|
|
@@ -1268,6 +1340,7 @@ function mapGroqError(error, providerLabel) {
|
|
|
1268
1340
|
if (error instanceof Groq.APIConnectionTimeoutError) return /* @__PURE__ */ new Error("Request timed out. Check your network or try a smaller diff.");
|
|
1269
1341
|
if (error instanceof Groq.APIError) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
|
|
1270
1342
|
if (error instanceof Error && /^4\d{2}\s/.test(error.message)) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
|
|
1343
|
+
if (error instanceof Error && (error.name === "AbortError" || /timed out/i.test(error.message))) return /* @__PURE__ */ new Error(`Request to ${label} timed out. Check your network, try a smaller diff, or increase 'timeout' in ~/.commit-mint.`);
|
|
1271
1344
|
return /* @__PURE__ */ new Error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1272
1345
|
}
|
|
1273
1346
|
const CONVENTIONAL_COMMIT_REGEX = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+$/;
|
|
@@ -4572,6 +4645,21 @@ async function promptProvider() {
|
|
|
4572
4645
|
value: "mistral",
|
|
4573
4646
|
hint: PROVIDER_CONFIGS.mistral.defaultModel
|
|
4574
4647
|
},
|
|
4648
|
+
{
|
|
4649
|
+
label: "Command Code",
|
|
4650
|
+
value: "commandcode",
|
|
4651
|
+
hint: PROVIDER_CONFIGS.commandcode.defaultModel
|
|
4652
|
+
},
|
|
4653
|
+
{
|
|
4654
|
+
label: "OpenRouter",
|
|
4655
|
+
value: "openrouter",
|
|
4656
|
+
hint: PROVIDER_CONFIGS.openrouter.defaultModel
|
|
4657
|
+
},
|
|
4658
|
+
{
|
|
4659
|
+
label: "Gemini",
|
|
4660
|
+
value: "gemini",
|
|
4661
|
+
hint: PROVIDER_CONFIGS.gemini.defaultModel
|
|
4662
|
+
},
|
|
4575
4663
|
{
|
|
4576
4664
|
label: "OmniRoute",
|
|
4577
4665
|
value: "omniroute",
|
|
@@ -4612,6 +4700,55 @@ const requireNumber = (v) => {
|
|
|
4612
4700
|
if (!v?.trim()) return "Value cannot be empty";
|
|
4613
4701
|
return Number.isNaN(Number(v)) ? "Must be a number" : void 0;
|
|
4614
4702
|
};
|
|
4703
|
+
/**
|
|
4704
|
+
* Build select options for the live model list: free models first (provider
|
|
4705
|
+
* id convention: `-free` / `:free` suffix), then the rest in the server's
|
|
4706
|
+
* curated order. `exclude` hides ids the provider's /chat/completions
|
|
4707
|
+
* endpoint can't serve (registry `modelListExclude` — e.g. CommandCode's
|
|
4708
|
+
* Anthropic-format claude-* models, Gemini's embeddings/TTS/image models).
|
|
4709
|
+
*/
|
|
4710
|
+
function buildModelOptions(models, exclude) {
|
|
4711
|
+
const usable = exclude ? models.filter((m) => !exclude.test(m.id)) : models;
|
|
4712
|
+
return [...usable.filter((m) => isFreeModelId(m.id)), ...usable.filter((m) => !isFreeModelId(m.id))].map((m) => {
|
|
4713
|
+
const label = m.name ?? m.id;
|
|
4714
|
+
return {
|
|
4715
|
+
label: isFreeModelId(m.id) ? `${label} (free)` : label,
|
|
4716
|
+
value: m.id,
|
|
4717
|
+
hint: m.contextLength ? `${Math.round(m.contextLength / 1e3)}k ctx` : void 0
|
|
4718
|
+
};
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
async function pickModelFromList(provider, currentModel) {
|
|
4722
|
+
let apiKey;
|
|
4723
|
+
try {
|
|
4724
|
+
apiKey = await getProviderApiKey(provider);
|
|
4725
|
+
} catch {
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4728
|
+
let models;
|
|
4729
|
+
try {
|
|
4730
|
+
models = await fetchProviderModels(provider, apiKey);
|
|
4731
|
+
} catch {
|
|
4732
|
+
p$1.log.warn("Could not fetch model list — enter the model ID manually");
|
|
4733
|
+
return;
|
|
4734
|
+
}
|
|
4735
|
+
const options = buildModelOptions(models, PROVIDER_CONFIGS[provider].modelListExclude);
|
|
4736
|
+
if (options.length === 0) {
|
|
4737
|
+
p$1.log.warn("Could not fetch model list — enter the model ID manually");
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
4740
|
+
const initial = options.find((o) => o.value === currentModel);
|
|
4741
|
+
const selected = await p$1.select({
|
|
4742
|
+
message: "Select model:",
|
|
4743
|
+
options,
|
|
4744
|
+
...initial ? { initialValue: initial.value } : {},
|
|
4745
|
+
maxItems: 12
|
|
4746
|
+
});
|
|
4747
|
+
if (p$1.isCancel(selected)) return selected;
|
|
4748
|
+
await writeConfig({ model: selected });
|
|
4749
|
+
debug("config: model set to %s", selected);
|
|
4750
|
+
return selected;
|
|
4751
|
+
}
|
|
4615
4752
|
function getSettingHandlers(config) {
|
|
4616
4753
|
const provider = getProvider(config);
|
|
4617
4754
|
return {
|
|
@@ -4633,7 +4770,12 @@ function getSettingHandlers(config) {
|
|
|
4633
4770
|
},
|
|
4634
4771
|
apikey: async () => promptApiKey(provider),
|
|
4635
4772
|
model: async () => {
|
|
4636
|
-
|
|
4773
|
+
const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
|
|
4774
|
+
if (PROVIDER_CONFIGS[provider].supportsModelList) {
|
|
4775
|
+
const picked = await pickModelFromList(provider, effectiveModel);
|
|
4776
|
+
if (picked !== void 0) return picked;
|
|
4777
|
+
}
|
|
4778
|
+
return promptTextSetting("Model ID:", "model", effectiveModel);
|
|
4637
4779
|
},
|
|
4638
4780
|
locale: async () => promptTextSetting("Locale (e.g. en, ja, ko):", "locale", config.locale),
|
|
4639
4781
|
maxlen: async () => promptTextSetting("Max commit message length:", "max-length", config["max-length"], requireNumber),
|