@kyubiware/commit-mint 0.12.1 → 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 +153 -16
- 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,7 +202,7 @@ 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",
|
|
@@ -178,14 +213,10 @@ function createFetchClient(baseURL, apiKey, timeout) {
|
|
|
178
213
|
stream: false
|
|
179
214
|
}),
|
|
180
215
|
signal: controller.signal
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
const data = await response.json();
|
|
187
|
-
for (const choice of data.choices ?? []) if (!choice.message?.reasoning && choice.message?.reasoning_content) choice.message.reasoning = choice.message.reasoning_content;
|
|
188
|
-
return data;
|
|
216
|
+
}));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timed out after ${timeout}ms`);
|
|
219
|
+
throw error;
|
|
189
220
|
} finally {
|
|
190
221
|
clearTimeout(timer);
|
|
191
222
|
}
|
|
@@ -209,6 +240,42 @@ function createProvider(options) {
|
|
|
209
240
|
model
|
|
210
241
|
};
|
|
211
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
|
+
}
|
|
212
279
|
//#endregion
|
|
213
280
|
//#region src/services/config.ts
|
|
214
281
|
const CONFIG_PATH = join(os.homedir(), ".commit-mint");
|
|
@@ -1273,6 +1340,7 @@ function mapGroqError(error, providerLabel) {
|
|
|
1273
1340
|
if (error instanceof Groq.APIConnectionTimeoutError) return /* @__PURE__ */ new Error("Request timed out. Check your network or try a smaller diff.");
|
|
1274
1341
|
if (error instanceof Groq.APIError) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
|
|
1275
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.`);
|
|
1276
1344
|
return /* @__PURE__ */ new Error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1277
1345
|
}
|
|
1278
1346
|
const CONVENTIONAL_COMMIT_REGEX = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+$/;
|
|
@@ -4577,6 +4645,21 @@ async function promptProvider() {
|
|
|
4577
4645
|
value: "mistral",
|
|
4578
4646
|
hint: PROVIDER_CONFIGS.mistral.defaultModel
|
|
4579
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
|
+
},
|
|
4580
4663
|
{
|
|
4581
4664
|
label: "OmniRoute",
|
|
4582
4665
|
value: "omniroute",
|
|
@@ -4617,6 +4700,55 @@ const requireNumber = (v) => {
|
|
|
4617
4700
|
if (!v?.trim()) return "Value cannot be empty";
|
|
4618
4701
|
return Number.isNaN(Number(v)) ? "Must be a number" : void 0;
|
|
4619
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
|
+
}
|
|
4620
4752
|
function getSettingHandlers(config) {
|
|
4621
4753
|
const provider = getProvider(config);
|
|
4622
4754
|
return {
|
|
@@ -4638,7 +4770,12 @@ function getSettingHandlers(config) {
|
|
|
4638
4770
|
},
|
|
4639
4771
|
apikey: async () => promptApiKey(provider),
|
|
4640
4772
|
model: async () => {
|
|
4641
|
-
|
|
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);
|
|
4642
4779
|
},
|
|
4643
4780
|
locale: async () => promptTextSetting("Locale (e.g. en, ja, ko):", "locale", config.locale),
|
|
4644
4781
|
maxlen: async () => promptTextSetting("Max commit message length:", "max-length", config["max-length"], requireNumber),
|