@kyubiware/commit-mint 0.5.1 → 0.5.3

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/dist/cli.mjs CHANGED
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
28
28
  //#region package.json
29
29
  var package_default = {
30
30
  name: "@kyubiware/commit-mint",
31
- version: "0.5.1",
31
+ version: "0.5.3",
32
32
  description: "🌿 A commit tool that actually handles hook failures",
33
33
  type: "module",
34
34
  bin: { "cmint": "./dist/cli.mjs" },
@@ -104,11 +104,11 @@ const PROVIDER_CONFIGS = {
104
104
  defaultModel: "openai/gpt-oss-20b"
105
105
  },
106
106
  cerebras: {
107
- baseURL: "https://api.cerebras.ai",
107
+ baseURL: "https://api.cerebras.ai/v1",
108
108
  defaultModel: "gpt-oss-120b"
109
109
  },
110
110
  mistral: {
111
- baseURL: "https://api.mistral.ai",
111
+ baseURL: "https://api.mistral.ai/v1",
112
112
  defaultModel: "mistral-small"
113
113
  }
114
114
  };
@@ -124,17 +124,52 @@ function formatProviderName(provider) {
124
124
  function isValidProvider(name) {
125
125
  return ALLOWED_PROVIDERS.includes(name);
126
126
  }
127
+ /**
128
+ * Generic OpenAI-compatible chat completions client using fetch.
129
+ * Used for non-Groq providers where the Groq SDK's hardcoded `/openai/v1/` path
130
+ * prefix doesn't match the provider's actual API path.
131
+ */
132
+ function createFetchClient(baseURL, apiKey, timeout) {
133
+ return { chat: { completions: { async create(params) {
134
+ const url = `${baseURL}/chat/completions`;
135
+ debug("fetchClient: POST %s, model=%s", url, params.model);
136
+ const controller = new AbortController();
137
+ const timer = setTimeout(() => controller.abort(), timeout);
138
+ try {
139
+ const response = await fetch(url, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ Authorization: `Bearer ${apiKey}`
144
+ },
145
+ body: JSON.stringify(params),
146
+ signal: controller.signal
147
+ });
148
+ if (!response.ok) {
149
+ const text = await response.text().catch(() => "");
150
+ throw new Error(`${response.status} ${text}`);
151
+ }
152
+ return await response.json();
153
+ } finally {
154
+ clearTimeout(timer);
155
+ }
156
+ } } } };
157
+ }
127
158
  function createProvider(options) {
128
159
  if (!isValidProvider(options.provider)) throw new Error(`Invalid provider "${options.provider}". Allowed values: ${ALLOWED_PROVIDERS.join(", ")}`);
129
160
  const providerConfig = PROVIDER_CONFIGS[options.provider];
130
161
  const model = options.modelOverride ?? providerConfig.defaultModel;
131
162
  const baseURL = options.baseURLOverride ?? providerConfig.baseURL;
163
+ const timeout = options.timeout ?? 6e4;
164
+ let client;
165
+ if (options.provider === "groq") client = new Groq({
166
+ apiKey: options.apiKey,
167
+ baseURL,
168
+ timeout
169
+ });
170
+ else client = createFetchClient(baseURL, options.apiKey, timeout);
132
171
  return {
133
- client: new Groq({
134
- apiKey: options.apiKey,
135
- baseURL,
136
- timeout: options.timeout
137
- }),
172
+ client,
138
173
  model
139
174
  };
140
175
  }
@@ -191,8 +226,17 @@ async function getProviderApiKey(provider) {
191
226
  debug("getProviderApiKey(%s): not found", provider);
192
227
  throw new Error(`Please set your ${formatProviderName(provider)} API key via \`cmint config set ${envVar}=<your token>\``);
193
228
  }
229
+ /** Check if a model name is the default for a provider OTHER than the given one. */
230
+ function isOtherProviderDefault(model, provider) {
231
+ for (const [name, config] of Object.entries(PROVIDER_CONFIGS)) if (name !== provider && config.defaultModel === model) return true;
232
+ return false;
233
+ }
194
234
  function getModelForProvider(config, provider, defaultModel) {
195
- return config[`model_${provider}`] ?? config.model ?? defaultModel;
235
+ const providerModel = config[`model_${provider}`];
236
+ if (providerModel) return providerModel;
237
+ const globalModel = config.model;
238
+ if (globalModel && !isOtherProviderDefault(globalModel, provider)) return globalModel;
239
+ return defaultModel;
196
240
  }
197
241
  //#endregion
198
242
  //#region src/services/hooks.ts
@@ -293,6 +337,50 @@ function parseEslintErrors(output) {
293
337
  return errors;
294
338
  }
295
339
  /**
340
+ * Parse cmint check output into structured errors.
341
+ * Check output uses [tool] prefix format (built by callers in auto-group.ts / staging.ts).
342
+ * This is distinct from parseHookErrors which handles raw git hook stderr.
343
+ */
344
+ function parseCheckErrors(output) {
345
+ if (!output.trim()) return [];
346
+ const errors = [];
347
+ const lines = output.split("\n");
348
+ let currentTool = "";
349
+ let currentLines = [];
350
+ const flush = () => {
351
+ if (currentTool && currentLines.length > 0) {
352
+ const message = currentLines.join("\n").trim();
353
+ if (message) errors.push({
354
+ tool: currentTool,
355
+ message,
356
+ raw: currentLines.join("\n")
357
+ });
358
+ }
359
+ currentTool = "";
360
+ currentLines = [];
361
+ };
362
+ for (const line of lines) {
363
+ const match = line.match(/^\[(\S+)\]\s*(.*)/);
364
+ if (match) {
365
+ flush();
366
+ currentTool = match[1];
367
+ const rest = match[2].trim();
368
+ if (rest) currentLines.push(rest);
369
+ } else if (currentTool) currentLines.push(line);
370
+ else currentLines.push(line);
371
+ }
372
+ flush();
373
+ if (errors.length === 0) {
374
+ const trimmed = output.trim();
375
+ if (trimmed) errors.push({
376
+ tool: "checks",
377
+ message: trimmed,
378
+ raw: output
379
+ });
380
+ }
381
+ return errors;
382
+ }
383
+ /**
296
384
  * Parse lint-staged/hook stderr output to discover which tools ran
297
385
  * and whether they succeeded. Used for clean post-commit summary.
298
386
  */
@@ -651,6 +739,7 @@ function mapGroqError(error, providerLabel) {
651
739
  if (error instanceof Groq.RateLimitError) return /* @__PURE__ */ new Error(`Rate limited by ${label}. Please wait and try again.`);
652
740
  if (error instanceof Groq.APIConnectionTimeoutError) return /* @__PURE__ */ new Error("Request timed out. Check your network or try a smaller diff.");
653
741
  if (error instanceof Groq.APIError) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
742
+ if (error instanceof Error && /^4\d{2}\s/.test(error.message)) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
654
743
  return /* @__PURE__ */ new Error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
655
744
  }
656
745
  const CONVENTIONAL_COMMIT_REGEX = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+$/;
@@ -757,6 +846,7 @@ async function generateCommitMessage(diff, options) {
757
846
  debug("callAI: %s — model=%s, promptLen=%d, systemLen=%d", !!strictSystemPrompt ? "RETRY (strict)" : "INITIAL", model, userPrompt.length, (strictSystemPrompt ?? systemPrompt).length);
758
847
  try {
759
848
  const isReasoningModel = /^(o[1-9]|.*gpt-oss.*|.*gpt-5.*)/i.test(model);
849
+ const isGroq = (options.provider ?? "groq") === "groq";
760
850
  const completion = await client.chat.completions.create({
761
851
  messages: [{
762
852
  role: "system",
@@ -768,7 +858,7 @@ async function generateCommitMessage(diff, options) {
768
858
  model,
769
859
  temperature: .3,
770
860
  ...isReasoningModel ? { max_completion_tokens: 1024 } : { max_tokens: 1024 },
771
- reasoning_format: "parsed"
861
+ ...isGroq && isReasoningModel ? { reasoning_format: "parsed" } : {}
772
862
  });
773
863
  const elapsed = Date.now() - callStart;
774
864
  const rawContent = completion.choices[0]?.message?.content;
@@ -1523,35 +1613,44 @@ async function runAutoGroupFlow(changedFiles, flags) {
1523
1613
  if (excluded.length > 0) {
1524
1614
  debug("Committing %d excluded files upfront:", excluded.length, excluded);
1525
1615
  const message = buildExcludedFilesMessage(excluded);
1616
+ log.info(excluded.map((f) => ` ${f}`).join("\n"));
1526
1617
  await resetStaging();
1527
1618
  await stageFiles(excluded);
1528
1619
  const headBefore = await getHead();
1529
1620
  const commitResult = await attemptCommit(message);
1530
1621
  const headAfter = await getHead();
1531
- if (commitResult.ok || headBefore !== headAfter) debug("Excluded files committed:", message);
1532
- else debug("Excluded files commit failed, continuing without them");
1622
+ if (commitResult.ok || headBefore !== headAfter) {
1623
+ debug("Excluded files committed:", message);
1624
+ log.success(dim(message));
1625
+ } else {
1626
+ debug("Excluded files commit failed, continuing without them");
1627
+ log.warn(red("Failed to commit excluded files."));
1628
+ }
1533
1629
  }
1534
1630
  if (included.length === 0) {
1535
1631
  debug("No included files to group, done");
1536
- outro(green("Done."));
1632
+ if (excluded.length > 0) outro(green("Committed excluded files. No other changes to group."));
1633
+ else outro(dim("Nothing to commit."));
1537
1634
  return "committed";
1538
1635
  }
1539
1636
  if (!flags.noCheck) {
1540
1637
  const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
1541
1638
  const repoRoot = await getRepoRoot();
1542
1639
  const allFiles = included.filter((f) => f.status !== "D").map((f) => f.path);
1543
- debug("Running user checks on %d files...", allFiles.length);
1544
- const ck = spinner();
1545
- ck.start("Running checks...");
1546
- const checkResults = await runAllChecks(repoRoot, allFiles, 6e4);
1547
- debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
1548
- if (!checkResults.ok) {
1549
- ck.stop(`${checkResults.results.filter((r) => !r.ok).length} check(s) failed`);
1550
- const rawStderr = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}] ${r.stderr}`).join("\n");
1551
- if (await showCheckFailureMenu(parseHookErrors(rawStderr), rawStderr) === "cancelled") return "cancelled";
1552
- } else {
1553
- ck.stop("All checks passed");
1554
- if (checkResults.results.length > 0) log.info(checkResults.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
1640
+ if (await detectConfig(repoRoot)) {
1641
+ debug("Running user checks on %d files...", allFiles.length);
1642
+ const ck = spinner();
1643
+ ck.start("Running checks...");
1644
+ const checkResults = await runAllChecks(repoRoot, allFiles, 6e4);
1645
+ debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
1646
+ if (!checkResults.ok) {
1647
+ ck.stop(`${checkResults.results.filter((r) => !r.ok).length} check(s) failed`);
1648
+ const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
1649
+ if (await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput) === "cancelled") return "cancelled";
1650
+ } else {
1651
+ ck.stop("All checks passed");
1652
+ if (checkResults.results.length > 0) log.info(checkResults.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
1653
+ }
1555
1654
  }
1556
1655
  }
1557
1656
  const config = await readConfig();
@@ -1750,16 +1849,19 @@ async function handleStaging(changedFiles, flags) {
1750
1849
  }
1751
1850
  if (stagingResult === "checks") {
1752
1851
  await stageAll();
1753
- const ckSpinner = spinner();
1754
- ckSpinner.start("Running checks...");
1755
- const ckResult = await runAllChecks(repoRoot, currentFiles.filter((f) => f.status !== "D").map((f) => f.path), 6e4);
1756
- if (ckResult.ok) {
1757
- ckSpinner.stop("All checks passed");
1758
- for (const r of ckResult.results) if (r.stdout.trim()) log.info(dim(r.stdout.trim()));
1759
- } else {
1760
- const failed = ckResult.results.filter((r) => !r.ok);
1761
- ckSpinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
1762
- for (const r of failed) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
1852
+ const allFiles = currentFiles.filter((f) => f.status !== "D").map((f) => f.path);
1853
+ if (await detectConfig(repoRoot)) {
1854
+ const ckSpinner = spinner();
1855
+ ckSpinner.start("Running checks...");
1856
+ const ckResult = await runAllChecks(repoRoot, allFiles, 6e4);
1857
+ if (ckResult.ok) {
1858
+ ckSpinner.stop("All checks passed");
1859
+ for (const r of ckResult.results) if (r.stdout.trim()) log.info(dim(r.stdout.trim()));
1860
+ } else {
1861
+ const failed = ckResult.results.filter((r) => !r.ok);
1862
+ ckSpinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
1863
+ for (const r of failed) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
1864
+ }
1763
1865
  }
1764
1866
  currentFiles = await getChangedFiles();
1765
1867
  continue;
@@ -1798,8 +1900,8 @@ async function runPreCommitChecks(changedFiles, noCheck) {
1798
1900
  const checkResults = await runAllChecks(checkRoot, stagedFileList, 6e4);
1799
1901
  debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
1800
1902
  if (!checkResults.ok) {
1801
- const rawStderr = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}] ${r.stderr}`).join("\n");
1802
- if (await showCheckFailureMenu(parseHookErrors(rawStderr), rawStderr) === "cancelled") process.exit(1);
1903
+ const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
1904
+ if (await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput) === "cancelled") process.exit(1);
1803
1905
  }
1804
1906
  }
1805
1907
  //#endregion
@@ -1937,10 +2039,11 @@ function maskKey(key) {
1937
2039
  function buildConfigDisplay(config) {
1938
2040
  const provider = isValidProvider(config.provider ?? "groq") ? config.provider : "groq";
1939
2041
  const apiKey = config[PROVIDER_ENV_KEYS[provider]];
2042
+ const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
1940
2043
  return [
1941
2044
  `Provider: ${bold(formatProviderName(provider))}`,
1942
2045
  `API Key: ${maskKey(apiKey)}`,
1943
- `Model: ${config.model ?? "(none)"}`,
2046
+ `Model: ${effectiveModel}`,
1944
2047
  `Locale: ${config.locale ?? "en"}`,
1945
2048
  `Max Length: ${config["max-length"] ?? "100"}`,
1946
2049
  `Commit Type: ${config.type || dim("(none)")}`,
@@ -2007,11 +2110,23 @@ function getSettingHandlers(config) {
2007
2110
  provider: async () => {
2008
2111
  const result = await promptProvider();
2009
2112
  if (p.isCancel(result)) return result;
2010
- await writeConfig({ provider: result });
2011
- debug("config: provider set to %s", result);
2113
+ const newProvider = result;
2114
+ const newDefaultModel = PROVIDER_CONFIGS[newProvider].defaultModel;
2115
+ await writeConfig({
2116
+ provider: newProvider,
2117
+ model: newDefaultModel
2118
+ });
2119
+ debug("config: provider set to %s, model set to %s", newProvider, newDefaultModel);
2120
+ const keyName = PROVIDER_ENV_KEYS[newProvider];
2121
+ if (!(await readConfig())[keyName]) {
2122
+ const keyResult = await promptApiKey(newProvider);
2123
+ if (p.isCancel(keyResult)) return keyResult;
2124
+ }
2012
2125
  },
2013
2126
  apikey: async () => promptApiKey(provider),
2014
- model: async () => promptTextSetting("Model ID:", "model", config.model),
2127
+ model: async () => {
2128
+ return promptTextSetting("Model ID:", "model", getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel));
2129
+ },
2015
2130
  locale: async () => promptTextSetting("Locale (e.g. en, ja, ko):", "locale", config.locale),
2016
2131
  maxlen: async () => promptTextSetting("Max commit message length:", "max-length", config["max-length"], requireNumber),
2017
2132
  type: async () => promptTextSetting("Commit type prefix (e.g. conventional):", "type", config.type),
@@ -2030,6 +2145,7 @@ async function editSettingsLoop(initialConfig) {
2030
2145
  while (true) {
2031
2146
  config = await readConfig();
2032
2147
  const provider = getProvider(config);
2148
+ const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
2033
2149
  const setting = await p.select({
2034
2150
  message: "Select a setting to edit:",
2035
2151
  options: [
@@ -2042,7 +2158,7 @@ async function editSettingsLoop(initialConfig) {
2042
2158
  value: "apikey"
2043
2159
  },
2044
2160
  {
2045
- label: `Model ${dim(`(${config.model ?? "(none)"})`)}`,
2161
+ label: `Model ${dim(`(${effectiveModel})`)}`,
2046
2162
  value: "model"
2047
2163
  },
2048
2164
  {