@kud/ai-conventional-commit-cli 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <a href="https://www.conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" /></a>
14
14
  </p>
15
15
 
16
- > TL;DR: Stage your changes, run `ai-conventional-commit` (or `split` for multiple commits), accept, done. Add `--gitmoji` if you like emoji. Refine later with `refine`.
16
+ > TL;DR: Stage your changes, run `ai-conventional-commit` (or `split` for multiple commits), accept, done. Add `--style gitmoji` if you like emoji. Refine later with `refine`.
17
17
 
18
18
  ---
19
19
 
@@ -101,11 +101,11 @@ ai-conventional-commit
101
101
  # Propose multiple commits (interactive confirm + real selective staging)
102
102
  ai-conventional-commit split
103
103
 
104
- # Add emoji decorations
105
- ai-conventional-commit --gitmoji
104
+ # Add emoji decorations (gitmoji)
105
+ ai-conventional-commit --style gitmoji
106
106
 
107
107
  # Pure emoji style (emoji: subject)
108
- ai-conventional-commit --gitmoji-pure
108
+ ai-conventional-commit --style gitmoji-pure
109
109
 
110
110
  # Refine previous session's first commit (shorter wording)
111
111
  ai-conventional-commit refine --shorter
@@ -113,20 +113,23 @@ ai-conventional-commit refine --shorter
113
113
 
114
114
  ## Command Reference
115
115
 
116
- | Command | Purpose |
117
- | --------------------------------- | ------------------------------------------- |
118
- | `ai-conventional-commit` | Generate single commit suggestion (default) |
119
- | `ai-conventional-commit generate` | Explicit alias of root |
120
- | `ai-conventional-commit split` | Propose & execute multiple commits |
121
- | `ai-conventional-commit refine` | Refine last session (or indexed) commit |
116
+ | Command | Purpose |
117
+ | ------------------------------------ | ------------------------------------------- |
118
+ | `ai-conventional-commit` | Generate single commit suggestion (default) |
119
+ | `ai-conventional-commit generate` | Explicit alias of root |
120
+ | `ai-conventional-commit split` | Propose & execute multiple commits |
121
+ | `ai-conventional-commit refine` | Refine last session (or indexed) commit |
122
+ | `ai-conventional-commit models` | List / pick models, save default |
123
+ | `ai-conventional-commit config show` | Show merged config + sources |
124
+ | `ai-conventional-commit config get` | Get a single config value |
125
+ | `ai-conventional-commit config set` | Persist a global config value |
122
126
 
123
127
  Helpful flags:
124
128
 
125
- - `--gitmoji` / `--gitmoji-pure`
129
+ - `--style <standard|gitmoji|gitmoji-pure>`
126
130
  - `--model <provider/name>` (override)
127
131
  - `--scope <scope>` (refine)
128
132
  - `--shorter` / `--longer`
129
- - `--emoji` (add appropriate emoji in refine)
130
133
 
131
134
  ## Examples
132
135
 
@@ -163,7 +166,7 @@ $ ai-conventional-commit refine --scope cli --shorter
163
166
  | gitmoji | `✨ feat: add search box` | Emoji + type retained |
164
167
  | gitmoji-pure | `✨: add search box` | Type removed; emoji acts as category |
165
168
 
166
- Enable via CLI flags or config (`gitmoji: true`, `gitmojiMode`).
169
+ Enable via CLI flag `--style gitmoji|gitmoji-pure` or config `"style": "gitmoji"` / `"style": "gitmoji-pure"`.
167
170
 
168
171
  ## Privacy Modes
169
172
 
@@ -183,8 +186,7 @@ Resolves via cosmiconfig (JSON/YAML/etc). Example:
183
186
  {
184
187
  "model": "github-copilot/gpt-4.1",
185
188
  "privacy": "low",
186
- "gitmoji": true,
187
- "gitmojiMode": "gitmoji",
189
+ "style": "gitmoji",
188
190
  "styleSamples": 120,
189
191
  "plugins": ["./src/sample-plugin/example-plugin.ts"],
190
192
  "maxTokens": 512
@@ -192,12 +194,39 @@ Resolves via cosmiconfig (JSON/YAML/etc). Example:
192
194
  ```
193
195
 
194
196
  Environment overrides (prefix `AICC_`):
195
- `MODEL`, `PRIVACY`, `STYLE_SAMPLES`, `GITMOJI`, `MAX_TOKENS`, `VERBOSE`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
197
+
198
+ ### Configuration Precedence
199
+
200
+ Lowest to highest (later wins):
201
+
202
+ 1. Built-in defaults
203
+ 2. Global config file: `~/.config/ai-conventional-commit-cli/aicc.json` (or `$XDG_CONFIG_HOME`)
204
+ 3. Project config (.aiccrc via cosmiconfig)
205
+ 4. Environment variables (`AICC_*`)
206
+ 5. CLI flags (e.g. `--model`, `--style`)
207
+
208
+ View the resolved configuration:
209
+
210
+ ```bash
211
+ ai-conventional-commit config show
212
+ ai-conventional-commit config show --json | jq
213
+ ```
214
+
215
+ Manage models:
216
+
217
+ ```bash
218
+ ai-conventional-commit models # list (opencode pass-through)
219
+ ai-conventional-commit models --interactive # interactive picker
220
+ ai-conventional-commit models --interactive --save # pick + persist globally
221
+ ai-conventional-commit models --current # show active model + source
222
+ ```
223
+
224
+ `MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `VERBOSE`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
196
225
 
197
226
  ## Refinement Workflow
198
227
 
199
228
  1. Generate (`ai-conventional-commit` or `split`) – session cached under `.git/.aicc-cache/last-session.json`.
200
- 2. Run `refine` with flags (`--shorter`, `--longer`, `--scope=ui`, `--emoji`).
229
+ 2. Run `refine` with flags (`--shorter`, `--longer`, `--scope=ui`).
201
230
  3. Accept or reject; refined output does _not_ auto‑amend history until you use it.
202
231
 
203
232
  ## Plugin API
@@ -0,0 +1,95 @@
1
+ // src/config.ts
2
+ import { cosmiconfig } from "cosmiconfig";
3
+ import { resolve, dirname, join } from "path";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ var DEFAULTS = {
7
+ model: process.env.AICC_MODEL || "github-copilot/gpt-4.1",
8
+ privacy: process.env.AICC_PRIVACY || "low",
9
+ style: process.env.AICC_STYLE || "standard",
10
+ styleSamples: parseInt(process.env.AICC_STYLE_SAMPLES || "120", 10),
11
+ maxTokens: parseInt(process.env.AICC_MAX_TOKENS || "512", 10),
12
+ cacheDir: ".git/.aicc-cache",
13
+ plugins: [],
14
+ verbose: process.env.AICC_VERBOSE === "true"
15
+ };
16
+ function getGlobalConfigPath() {
17
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
18
+ return resolve(base, "ai-conventional-commit-cli", "aicc.json");
19
+ }
20
+ function saveGlobalConfig(partial) {
21
+ const filePath = getGlobalConfigPath();
22
+ const dir = dirname(filePath);
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
24
+ let existing = {};
25
+ if (existsSync(filePath)) {
26
+ try {
27
+ existing = JSON.parse(readFileSync(filePath, "utf8")) || {};
28
+ } catch (e) {
29
+ if (process.env.AICC_VERBOSE === "true") {
30
+ console.error("[ai-cc] Failed to parse existing global config, overwriting.");
31
+ }
32
+ }
33
+ }
34
+ const merged = { ...existing, ...partial };
35
+ writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
36
+ return filePath;
37
+ }
38
+ async function loadConfig(cwd = process.cwd()) {
39
+ return (await loadConfigDetailed(cwd)).config;
40
+ }
41
+ async function loadConfigDetailed(cwd = process.cwd()) {
42
+ let globalCfg = {};
43
+ const globalPath = getGlobalConfigPath();
44
+ if (existsSync(globalPath)) {
45
+ try {
46
+ globalCfg = JSON.parse(readFileSync(globalPath, "utf8")) || {};
47
+ } catch (e) {
48
+ if (process.env.AICC_VERBOSE === "true") {
49
+ console.error("[ai-cc] Failed to parse global config, ignoring.");
50
+ }
51
+ }
52
+ }
53
+ const explorer = cosmiconfig("aicc");
54
+ const result = await explorer.search(cwd);
55
+ const projectCfg = result?.config || {};
56
+ const envCfg = {};
57
+ if (process.env.AICC_MODEL) envCfg.model = process.env.AICC_MODEL;
58
+ if (process.env.AICC_PRIVACY) envCfg.privacy = process.env.AICC_PRIVACY;
59
+ if (process.env.AICC_STYLE) envCfg.style = process.env.AICC_STYLE;
60
+ if (process.env.AICC_STYLE_SAMPLES)
61
+ envCfg.styleSamples = parseInt(process.env.AICC_STYLE_SAMPLES, 10);
62
+ if (process.env.AICC_MAX_TOKENS) envCfg.maxTokens = parseInt(process.env.AICC_MAX_TOKENS, 10);
63
+ if (process.env.AICC_VERBOSE) envCfg.verbose = process.env.AICC_VERBOSE === "true";
64
+ const merged = {
65
+ ...DEFAULTS,
66
+ ...globalCfg,
67
+ ...projectCfg,
68
+ ...envCfg
69
+ };
70
+ merged.plugins = (merged.plugins || []).filter((p) => {
71
+ const abs = resolve(cwd, p);
72
+ return existsSync(abs);
73
+ });
74
+ const sources = Object.keys(merged).reduce((acc, key) => {
75
+ const k = key;
76
+ let src = "default";
77
+ if (k in globalCfg) src = "global";
78
+ if (k in projectCfg) src = "project";
79
+ if (k in envCfg) src = "env";
80
+ acc[k] = src;
81
+ return acc;
82
+ }, {});
83
+ const withMeta = Object.assign(merged, { _sources: sources });
84
+ return {
85
+ config: withMeta,
86
+ raw: { defaults: DEFAULTS, global: globalCfg, project: projectCfg, env: envCfg }
87
+ };
88
+ }
89
+
90
+ export {
91
+ getGlobalConfigPath,
92
+ saveGlobalConfig,
93
+ loadConfig,
94
+ loadConfigDetailed
95
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ getGlobalConfigPath,
3
+ loadConfig,
4
+ loadConfigDetailed,
5
+ saveGlobalConfig
6
+ } from "./chunk-DCGUX6KW.js";
7
+ export {
8
+ getGlobalConfigPath,
9
+ loadConfig,
10
+ loadConfigDetailed,
11
+ saveGlobalConfig
12
+ };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ loadConfig
4
+ } from "./chunk-DCGUX6KW.js";
2
5
 
3
6
  // src/index.ts
4
7
  import { Cli, Command, Option } from "clipanion";
@@ -195,7 +198,7 @@ var buildGenerationMessages = (opts) => {
195
198
  );
196
199
  specLines.push("Length Rule: Entire title line (including type/scope) must be <= 72 chars.");
197
200
  specLines.push(
198
- "Emoji Rule: " + (config.gitmoji ? "OPTIONAL single leading gitmoji BEFORE the type only if confidently adds clarity; do not invent or stack; omit if unsure." : "Disallow all emojis and gitmoji codes; output must start directly with the type.")
201
+ "Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE the type only if confidently adds clarity; do not invent or stack; omit if unsure." : "Disallow all emojis and gitmoji codes; output must start directly with the type.")
199
202
  );
200
203
  specLines.push(
201
204
  "Forbidden: breaking changes notation, exclamation mark after type unless truly semver-major (avoid unless diff clearly indicates)."
@@ -244,7 +247,7 @@ var buildRefineMessages = (opts) => {
244
247
  spec.push("Title Format: <type>(<optional-scope>): <subject> (<=72 chars)");
245
248
  spec.push("Subject: imperative, present tense, no trailing period.");
246
249
  spec.push(
247
- "Emoji Rule: " + (config.gitmoji ? "OPTIONAL single leading gitmoji BEFORE type if it adds clarity; omit if unsure." : "Disallow all emojis; start directly with the type.")
250
+ "Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE type if it adds clarity; omit if unsure." : "Disallow all emojis; start directly with the type.")
248
251
  );
249
252
  spec.push("Preserve semantic meaning; only improve clarity, scope, brevity, conformity.");
250
253
  spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
@@ -298,7 +301,7 @@ ${userAggregate}`;
298
301
  });
299
302
  }
300
303
  const start = Date.now();
301
- return await new Promise((resolve3, reject) => {
304
+ return await new Promise((resolve2, reject) => {
302
305
  let resolved = false;
303
306
  let acc = "";
304
307
  const includeLogs = process.env.AICC_PRINT_LOGS === "true";
@@ -318,7 +321,7 @@ ${userAggregate}`;
318
321
  `[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
319
322
  );
320
323
  }
321
- resolve3(value);
324
+ resolve2(value);
322
325
  };
323
326
  const tryEager = () => {
324
327
  if (!eager) return;
@@ -699,8 +702,10 @@ function renderCommitBlock(opts) {
699
702
  const lines = opts.body.split("\n");
700
703
  lines.forEach((line, i) => {
701
704
  if (line.trim().length === 0) borderLine();
702
- else if (i === 0) borderLine(descColor("Description:") + " " + bodyFirst(line));
703
- else borderLine(bodyRest(line));
705
+ else if (i === 0) {
706
+ borderLine(descColor("Description:"));
707
+ borderLine(bodyFirst(line));
708
+ } else borderLine(bodyRest(line));
704
709
  });
705
710
  }
706
711
  }
@@ -760,8 +765,8 @@ async function runGenerate(config) {
760
765
  candidates = candidates.map((c) => ({
761
766
  ...c,
762
767
  title: formatCommitTitle(c.title, {
763
- allowGitmoji: !!config.gitmoji,
764
- mode: config.gitmojiMode || "standard"
768
+ allowGitmoji: config.style === "gitmoji" || config.style === "gitmoji-pure",
769
+ mode: config.style
765
770
  })
766
771
  }));
767
772
  const chosen = candidates[0];
@@ -911,8 +916,8 @@ async function runSplit(config, desired) {
911
916
  candidates = candidates.map((c) => ({
912
917
  ...c,
913
918
  title: formatCommitTitle(c.title, {
914
- allowGitmoji: !!config.gitmoji,
915
- mode: config.gitmojiMode || "standard"
919
+ allowGitmoji: config.style === "gitmoji" || config.style === "gitmoji-pure",
920
+ mode: config.style
916
921
  })
917
922
  }));
918
923
  const fancy = candidates.length > 1;
@@ -1051,7 +1056,6 @@ async function runRefine(config, options) {
1051
1056
  if (options.shorter) instructions.push("Make the title shorter but keep meaning.");
1052
1057
  if (options.longer) instructions.push("Add more specificity to the title.");
1053
1058
  if (options.scope) instructions.push(`Add or adjust scope to: ${options.scope}`);
1054
- if (options.emoji) instructions.push("Add a relevant emoji prefix.");
1055
1059
  if (!instructions.length) {
1056
1060
  const add = await prompt("No refinement flags given. Enter custom instruction: ");
1057
1061
  if (add.trim()) instructions.push(add.trim());
@@ -1073,8 +1077,8 @@ async function runRefine(config, options) {
1073
1077
  );
1074
1078
  const refinedPlan = await runStep("Parsing response", async () => extractJSON(raw));
1075
1079
  refinedPlan.commits[0].title = formatCommitTitle(refinedPlan.commits[0].title, {
1076
- allowGitmoji: !!config.gitmoji || !!options.emoji,
1077
- mode: config.gitmojiMode || "standard"
1080
+ allowGitmoji: config.style === "gitmoji" || config.style === "gitmoji-pure",
1081
+ mode: config.style
1078
1082
  });
1079
1083
  phased.phase("Suggested commit");
1080
1084
  phased.stop();
@@ -1108,39 +1112,10 @@ async function runRefine(config, options) {
1108
1112
  finalSuccess({ count: 1, startedAt });
1109
1113
  }
1110
1114
 
1111
- // src/config.ts
1112
- import { cosmiconfig } from "cosmiconfig";
1113
- import { resolve as resolve2 } from "path";
1114
- import { existsSync as existsSync4 } from "fs";
1115
- var DEFAULTS = {
1116
- model: process.env.AICC_MODEL || "github-copilot/gpt-4.1",
1117
- privacy: process.env.AICC_PRIVACY || "low",
1118
- gitmoji: process.env.AICC_GITMOJI === "true",
1119
- gitmojiMode: "standard",
1120
- styleSamples: parseInt(process.env.AICC_STYLE_SAMPLES || "120", 10),
1121
- maxTokens: parseInt(process.env.AICC_MAX_TOKENS || "512", 10),
1122
- cacheDir: ".git/.aicc-cache",
1123
- plugins: [],
1124
- verbose: process.env.AICC_VERBOSE === "true"
1125
- };
1126
- async function loadConfig(cwd = process.cwd()) {
1127
- const explorer = cosmiconfig("aicc");
1128
- const result = await explorer.search(cwd);
1129
- const cfg = {
1130
- ...DEFAULTS,
1131
- ...result?.config || {}
1132
- };
1133
- cfg.plugins = (cfg.plugins || []).filter((p) => {
1134
- const abs = resolve2(cwd, p);
1135
- return existsSync4(abs);
1136
- });
1137
- return cfg;
1138
- }
1139
-
1140
1115
  // package.json
1141
1116
  var package_default = {
1142
1117
  name: "@kud/ai-conventional-commit-cli",
1143
- version: "0.6.0",
1118
+ version: "0.7.1",
1144
1119
  type: "module",
1145
1120
  description: "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
1146
1121
  bin: {
@@ -1210,22 +1185,78 @@ var package_default = {
1210
1185
  };
1211
1186
 
1212
1187
  // src/index.ts
1188
+ import { execa as execa2 } from "execa";
1189
+ import inquirer4 from "inquirer";
1213
1190
  var pkgVersion = package_default.version || "0.0.0";
1191
+ var RootCommand = class extends Command {
1192
+ static paths = [[]];
1193
+ static usage = Command.Usage({
1194
+ description: "Generate a conventional commit message (default command).",
1195
+ details: `Commands:
1196
+
1197
+ \u22C5 generate: Generate a single commit (default)
1198
+
1199
+ \u22C5 split: Plan & apply multiple smaller commits
1200
+
1201
+ \u22C5 refine: Refine last (or indexed) commit
1202
+
1203
+ \u22C5 models: List or pick AI models (opencode)
1204
+
1205
+ \u22C5 config show: Show merged config + sources
1206
+
1207
+ \u22C5 config get <key>: Get a single config value
1208
+
1209
+ \u22C5 config set <k> <v>: Persist a global config value
1210
+
1211
+ Refine Options:
1212
+
1213
+ \u22C5 --shorter: Make message more concise
1214
+
1215
+ \u22C5 --longer: Expand message with detail
1216
+
1217
+ \u22C5 --scope <scope>: Add or replace scope
1218
+
1219
+ \u22C5 --index <n>: Target commit from previous split`,
1220
+ examples: [
1221
+ ["Generate with gitmoji style", "ai-conventional-commit --style gitmoji"],
1222
+ ["Split staged changes", "ai-conventional-commit split --max 3"],
1223
+ ["Pick & save default model", "ai-conventional-commit models -i --save"],
1224
+ ["Set style globally", "ai-conventional-commit config set style gitmoji"],
1225
+ ["Show config JSON", "ai-conventional-commit config show --json"]
1226
+ ]
1227
+ });
1228
+ style = Option.String("--style", {
1229
+ required: false,
1230
+ description: "Title style: standard | gitmoji | gitmoji-pure"
1231
+ });
1232
+ model = Option.String("-m,--model", {
1233
+ required: false,
1234
+ description: "Model provider/name (e.g. github-copilot/gpt-4.1)"
1235
+ });
1236
+ async execute() {
1237
+ const config = await loadConfig();
1238
+ if (this.style) config.style = this.style;
1239
+ if (this.model) config.model = this.model;
1240
+ await runGenerate(config);
1241
+ }
1242
+ };
1214
1243
  var GenerateCommand = class extends Command {
1215
- static paths = [[`generate`], []];
1244
+ static paths = [[`generate`]];
1216
1245
  static usage = Command.Usage({
1217
1246
  description: "Generate a conventional commit message for staged changes.",
1218
- details: `Generates a single commit message using AI with style + guardrails.
1219
- Add --gitmoji[-pure] to enable emoji styles.`,
1247
+ details: `Generate a single conventional commit using AI with style rules, guardrails, and optional model override. Uses staged changes. Add --style gitmoji or --style gitmoji-pure for emoji modes.`,
1220
1248
  examples: [
1221
- ["Generate a commit with gitmoji style", "ai-conventional-commit generate --gitmoji"]
1249
+ ["Basic usage (standard style)", "ai-conventional-commit generate"],
1250
+ ["Force gitmoji style", "ai-conventional-commit generate --style gitmoji"],
1251
+ [
1252
+ "Override model for this run",
1253
+ "ai-conventional-commit generate --model github-copilot/gpt-4.1"
1254
+ ]
1222
1255
  ]
1223
1256
  });
1224
- gitmoji = Option.Boolean("--gitmoji", false, {
1225
- description: "Gitmoji mode (vs --gitmoji-pure): emoji + type (emoji: subject)"
1226
- });
1227
- gitmojiPure = Option.Boolean("--gitmoji-pure", false, {
1228
- description: "Pure gitmoji mode (vs --gitmoji): emoji only (emoji: subject)"
1257
+ style = Option.String("--style", {
1258
+ required: false,
1259
+ description: "Title style: standard | gitmoji | gitmoji-pure"
1229
1260
  });
1230
1261
  model = Option.String("-m,--model", {
1231
1262
  required: false,
@@ -1233,9 +1264,8 @@ Add --gitmoji[-pure] to enable emoji styles.`,
1233
1264
  });
1234
1265
  async execute() {
1235
1266
  const config = await loadConfig();
1236
- if (this.gitmoji || this.gitmojiPure) {
1237
- config.gitmoji = true;
1238
- config.gitmojiMode = this.gitmojiPure ? "gitmoji-pure" : "gitmoji";
1267
+ if (this.style) {
1268
+ config.style = this.style;
1239
1269
  }
1240
1270
  if (this.model) config.model = this.model;
1241
1271
  await runGenerate(config);
@@ -1245,21 +1275,19 @@ var SplitCommand = class extends Command {
1245
1275
  static paths = [[`split`]];
1246
1276
  static usage = Command.Usage({
1247
1277
  description: "Propose multiple smaller conventional commits for current staged diff.",
1248
- details: `Analyzes staged changes, groups them logically and suggests multiple commit messages.
1249
- Use --max to limit the number of proposals.`,
1278
+ details: `Analyze staged changes, group them logically, and propose multiple conventional commit titles + bodies. Use --max to limit proposals. Each proposal obeys style + guardrails.`,
1250
1279
  examples: [
1280
+ ["Default split (standard style)", "ai-conventional-commit split"],
1251
1281
  [
1252
- "Split into at most 3 commits with gitmoji",
1253
- "ai-conventional-commit split --max 3 --gitmoji"
1282
+ "Limit to 3 proposals with gitmoji style",
1283
+ "ai-conventional-commit split --max 3 --style gitmoji"
1254
1284
  ]
1255
1285
  ]
1256
1286
  });
1257
1287
  max = Option.String("--max", { description: "Max proposed commits", required: false });
1258
- gitmoji = Option.Boolean("--gitmoji", false, {
1259
- description: "Gitmoji mode (vs --gitmoji-pure): emoji + type"
1260
- });
1261
- gitmojiPure = Option.Boolean("--gitmoji-pure", false, {
1262
- description: "Pure gitmoji mode (vs --gitmoji): emoji only"
1288
+ style = Option.String("--style", {
1289
+ required: false,
1290
+ description: "Title style: standard | gitmoji | gitmoji-pure"
1263
1291
  });
1264
1292
  model = Option.String("-m,--model", {
1265
1293
  required: false,
@@ -1267,10 +1295,7 @@ Use --max to limit the number of proposals.`,
1267
1295
  });
1268
1296
  async execute() {
1269
1297
  const config = await loadConfig();
1270
- if (this.gitmoji || this.gitmojiPure) {
1271
- config.gitmoji = true;
1272
- config.gitmojiMode = this.gitmojiPure ? "gitmoji-pure" : "gitmoji";
1273
- }
1298
+ if (this.style) config.style = this.style;
1274
1299
  if (this.model) config.model = this.model;
1275
1300
  await runSplit(config, this.max ? parseInt(this.max, 10) : void 0);
1276
1301
  }
@@ -1279,7 +1304,7 @@ var RefineCommand = class extends Command {
1279
1304
  static paths = [[`refine`]];
1280
1305
  static usage = Command.Usage({
1281
1306
  description: "Refine the last (or chosen) commit message with style rules.",
1282
- details: `Allows targeted improvements: shorter/longer length, inject scope, add emoji, or select a specific index when multiple commits were generated earlier.`,
1307
+ details: `Targeted improvements to an existing commit: shorter/longer length, inject or replace scope, or refine a specific index from a previous split.`,
1283
1308
  examples: [
1284
1309
  ["Shorten the last commit message", "ai-conventional-commit refine --shorter"],
1285
1310
  ["Add a scope to the last commit", "ai-conventional-commit refine --scope ui"]
@@ -1288,7 +1313,10 @@ var RefineCommand = class extends Command {
1288
1313
  shorter = Option.Boolean("--shorter", false, { description: "Make message more concise" });
1289
1314
  longer = Option.Boolean("--longer", false, { description: "Expand message with detail" });
1290
1315
  scope = Option.String("--scope", { description: "Override/add scope (e.g. ui, api)" });
1291
- emoji = Option.Boolean("--emoji", false, { description: "Add appropriate gitmoji (non-pure)" });
1316
+ style = Option.String("--style", {
1317
+ required: false,
1318
+ description: "Title style: standard | gitmoji | gitmoji-pure"
1319
+ });
1292
1320
  index = Option.String("--index", {
1293
1321
  description: "Select commit index if multiple were generated"
1294
1322
  });
@@ -1298,50 +1326,196 @@ var RefineCommand = class extends Command {
1298
1326
  });
1299
1327
  async execute() {
1300
1328
  const config = await loadConfig();
1329
+ if (this.style) config.style = this.style;
1301
1330
  if (this.model) config.model = this.model;
1302
1331
  await runRefine(config, {
1303
1332
  shorter: this.shorter,
1304
1333
  longer: this.longer,
1305
1334
  scope: this.scope,
1306
- emoji: this.emoji,
1307
1335
  index: this.index ? parseInt(this.index, 10) : void 0
1308
1336
  });
1309
1337
  }
1310
1338
  };
1311
- var HelpCommand = class extends Command {
1312
- static paths = [[`--help`], [`-h`]];
1313
- // capture explicit help
1339
+ var ModelsCommand = class extends Command {
1340
+ static paths = [[`models`]];
1341
+ static usage = Command.Usage({
1342
+ description: "List available models via opencode CLI.",
1343
+ details: `Wrapper around "opencode models". Use --interactive (or -i) for a picker; prints the selected model id for piping or quick copy. Add --save with --interactive to persist globally in aicc.json (XDG config).`,
1344
+ examples: [
1345
+ ["List models", "ai-conventional-commit models"],
1346
+ ["Interactively pick a model", "ai-conventional-commit models --interactive"],
1347
+ ["Pick & save globally", "ai-conventional-commit models --interactive --save"]
1348
+ ]
1349
+ });
1350
+ interactive = Option.Boolean("-i,--interactive", false, {
1351
+ description: "Interactive model selection"
1352
+ });
1353
+ save = Option.Boolean("--save", false, {
1354
+ description: "Persist selected model globally (requires --interactive)"
1355
+ });
1356
+ current = Option.Boolean("--current", false, {
1357
+ description: "Print current default model and its source"
1358
+ });
1314
1359
  async execute() {
1315
- this.context.stdout.write(globalHelp() + "\n");
1360
+ if (this.current) {
1361
+ const { loadConfigDetailed } = await import("./config-Q7AKJSO4.js");
1362
+ const { config } = await loadConfigDetailed();
1363
+ this.context.stdout.write(`${config.model} (source: ${config._sources.model})
1364
+ `);
1365
+ return;
1366
+ }
1367
+ try {
1368
+ const { stdout } = await execa2("opencode", ["models"]).catch(async (err) => {
1369
+ if (err.shortMessage && /ENOENT/.test(err.shortMessage)) {
1370
+ this.context.stderr.write(
1371
+ "opencode CLI not found in PATH. Install it from https://github.com/opencodejs/opencode or ensure the binary is available.\n"
1372
+ );
1373
+ }
1374
+ throw err;
1375
+ });
1376
+ const useInteractive = this.interactive;
1377
+ if (!useInteractive) {
1378
+ this.context.stdout.write(stdout.trim() + "\n");
1379
+ return;
1380
+ }
1381
+ if (!process.stdout.isTTY) {
1382
+ this.context.stdout.write(stdout.trim() + "\n");
1383
+ return;
1384
+ }
1385
+ const candidates = Array.from(
1386
+ new Set(
1387
+ stdout.split("\n").map((l) => l.trim()).filter((l) => /^[a-z0-9_.-]+\/[A-Za-z0-9_.:-]+$/.test(l))
1388
+ )
1389
+ );
1390
+ if (candidates.length === 0) {
1391
+ this.context.stdout.write(stdout.trim() + "\n");
1392
+ return;
1393
+ }
1394
+ const { model } = await inquirer4.prompt([
1395
+ {
1396
+ name: "model",
1397
+ type: "list",
1398
+ message: "Select a model",
1399
+ choices: candidates
1400
+ }
1401
+ ]);
1402
+ this.context.stdout.write(model + "\n");
1403
+ if (this.save) {
1404
+ try {
1405
+ const { saveGlobalConfig } = await import("./config-Q7AKJSO4.js");
1406
+ const path = saveGlobalConfig({ model });
1407
+ this.context.stdout.write(`Saved as default model in ${path}
1408
+ `);
1409
+ } catch (e) {
1410
+ this.context.stderr.write(`Failed to save global config: ${e?.message || e}
1411
+ `);
1412
+ }
1413
+ }
1414
+ this.context.stdout.write(
1415
+ `
1416
+ Use it now:
1417
+ ai-conventional-commit generate --model ${model}
1418
+ `
1419
+ );
1420
+ } catch (e) {
1421
+ this.context.stderr.write(
1422
+ `Failed to list models via "opencode models": ${e?.message || e}
1423
+ `
1424
+ );
1425
+ }
1426
+ }
1427
+ };
1428
+ var ConfigShowCommand = class extends Command {
1429
+ static paths = [[`config`, `show`]];
1430
+ static usage = Command.Usage({
1431
+ description: "Show effective configuration with source metadata.",
1432
+ details: "Outputs merged config fields, their values, and source precedence info. Use --json for raw JSON including _sources.",
1433
+ examples: [
1434
+ ["Human readable", "ai-conventional-commit config show"],
1435
+ ["JSON with sources", "ai-conventional-commit config show --json"]
1436
+ ]
1437
+ });
1438
+ json = Option.Boolean("--json", false, { description: "Output JSON including _sources" });
1439
+ async execute() {
1440
+ const { loadConfigDetailed } = await import("./config-Q7AKJSO4.js");
1441
+ const { config, raw } = await loadConfigDetailed();
1442
+ if (this.json) {
1443
+ this.context.stdout.write(JSON.stringify({ config, raw }, null, 2) + "\n");
1444
+ return;
1445
+ }
1446
+ const sources = config._sources;
1447
+ const lines = Object.entries(config).filter(([k]) => k !== "_sources").map(([k, v]) => `${k} = ${JSON.stringify(v)} (${sources[k]})`);
1448
+ this.context.stdout.write(lines.join("\n") + "\n");
1449
+ }
1450
+ };
1451
+ var ConfigGetCommand = class extends Command {
1452
+ static paths = [[`config`, `get`]];
1453
+ static usage = Command.Usage({
1454
+ description: "Get a single configuration value (effective).",
1455
+ details: "Returns the effective value after merging sources. Optionally show its source.",
1456
+ examples: [
1457
+ ["Get model", "ai-conventional-commit config get model"],
1458
+ ["Get style", "ai-conventional-commit config get style"],
1459
+ ["Get model with source", "ai-conventional-commit config get model --with-source"]
1460
+ ]
1461
+ });
1462
+ key = Option.String();
1463
+ withSource = Option.Boolean("--with-source", false, { description: "Append source label" });
1464
+ async execute() {
1465
+ const { loadConfigDetailed } = await import("./config-Q7AKJSO4.js");
1466
+ const { config } = await loadConfigDetailed();
1467
+ const key = this.key;
1468
+ if (!(key in config)) {
1469
+ this.context.stderr.write(`Unknown config key: ${this.key}
1470
+ `);
1471
+ process.exitCode = 1;
1472
+ return;
1473
+ }
1474
+ if (this.withSource) {
1475
+ const src = config._sources?.[key];
1476
+ this.context.stdout.write(`${JSON.stringify(config[key])} (${src})
1477
+ `);
1478
+ } else {
1479
+ this.context.stdout.write(JSON.stringify(config[key]) + "\n");
1480
+ }
1481
+ }
1482
+ };
1483
+ var ConfigSetCommand = class extends Command {
1484
+ static paths = [[`config`, `set`]];
1485
+ static usage = Command.Usage({
1486
+ description: "Set and persist a global configuration key.",
1487
+ details: "Writes to the global aicc.json (XDG config). Accepts JSON for complex values. Only allowed keys: model, style, privacy, styleSamples, maxTokens, verbose.",
1488
+ examples: [
1489
+ ["Set default model", "ai-conventional-commit config set model github-copilot/gpt-4.1"],
1490
+ ["Set style to gitmoji", "ai-conventional-commit config set style gitmoji"],
1491
+ ["Enable verbose mode", "ai-conventional-commit config set verbose true"]
1492
+ ]
1493
+ });
1494
+ key = Option.String();
1495
+ value = Option.String();
1496
+ async execute() {
1497
+ const allowed = /* @__PURE__ */ new Set(["model", "style", "privacy", "styleSamples", "maxTokens", "verbose"]);
1498
+ if (!allowed.has(this.key)) {
1499
+ this.context.stderr.write(`Cannot set key: ${this.key}
1500
+ `);
1501
+ process.exitCode = 1;
1502
+ return;
1503
+ }
1504
+ let parsed = this.value;
1505
+ if (/^(true|false)$/i.test(this.value)) parsed = this.value.toLowerCase() === "true";
1506
+ else if (/^[0-9]+$/.test(this.value)) parsed = parseInt(this.value, 10);
1507
+ else if (/^[\[{]/.test(this.value)) {
1508
+ try {
1509
+ parsed = JSON.parse(this.value);
1510
+ } catch {
1511
+ }
1512
+ }
1513
+ const { saveGlobalConfig } = await import("./config-Q7AKJSO4.js");
1514
+ const path = saveGlobalConfig({ [this.key]: parsed });
1515
+ this.context.stdout.write(`Saved ${this.key} to ${path}
1516
+ `);
1316
1517
  }
1317
1518
  };
1318
- function globalHelp() {
1319
- return `ai-conventional-commit v${pkgVersion}
1320
-
1321
- Usage:
1322
- ai-conventional-commit [generate] [options] Generate a commit (default)
1323
- ai-conventional-commit split [options] Propose multiple commits
1324
- ai-conventional-commit refine [options] Refine last or indexed commit
1325
-
1326
- Global Options:
1327
- -m, --model <provider/name> Override model provider/name
1328
- --gitmoji[-pure] Gitmoji modes: emoji + type (default) or pure emoji only
1329
- -h, --help Show this help
1330
- -V, --version Show version
1331
-
1332
- Refine Options:
1333
- --shorter / --longer Adjust message length
1334
- --scope <scope> Add or replace scope
1335
- --emoji Add suitable gitmoji
1336
- --index <n> Select commit index
1337
-
1338
- Examples:
1339
- ai-conventional-commit --gitmoji
1340
- ai-conventional-commit --gitmoji-pure
1341
- ai-conventional-commit split --max 3 --gitmoji
1342
- ai-conventional-commit refine --scope api --emoji
1343
- `;
1344
- }
1345
1519
  var VersionCommand = class extends Command {
1346
1520
  static paths = [[`--version`], [`-V`]];
1347
1521
  async execute() {
@@ -1354,10 +1528,14 @@ var cli = new Cli({
1354
1528
  binaryName: "ai-conventional-commit",
1355
1529
  binaryVersion: pkgVersion
1356
1530
  });
1531
+ cli.register(RootCommand);
1357
1532
  cli.register(GenerateCommand);
1358
1533
  cli.register(SplitCommand);
1359
1534
  cli.register(RefineCommand);
1360
- cli.register(HelpCommand);
1535
+ cli.register(ModelsCommand);
1536
+ cli.register(ConfigShowCommand);
1537
+ cli.register(ConfigGetCommand);
1538
+ cli.register(ConfigSetCommand);
1361
1539
  cli.register(VersionCommand);
1362
1540
  cli.runExit(process.argv.slice(2), {
1363
1541
  stdin: process.stdin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/ai-conventional-commit-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
6
6
  "bin": {