@johnowennixon/diffdash 1.12.0 → 1.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 CHANGED
@@ -11,9 +11,10 @@ A command-line tool to generate Git commit messages using AI.
11
11
 
12
12
  ## Features
13
13
 
14
- * Generate Git commit messages in **natural English**
15
- * Add a prefix or suffix to the summary line
16
- * Add a footer to the generated commit messages
14
+ * Generate Git commit messages in natural language prose
15
+ * Add a prefix or suffix to the summary line to support tickets from project management tools
16
+ * Add a metadata footer to the generated commit messages
17
+ * Support for 42 human languages including English, Chinese, and Hindi
17
18
  * Select from a choice of LLM models
18
19
  * Compare messages generated from all configured models
19
20
  * Disable or auto-approve various stages
@@ -21,7 +22,7 @@ A command-line tool to generate Git commit messages using AI.
21
22
  * Configuration using standard API provider environment variables
22
23
  * Uses the Vercel AI SDK (version 6)
23
24
  * Uses structured JSON with compatible models
24
- * Substantially written using AI coding (Claude Code, Roo Code, and Amp)
25
+ * Substantially written using AI coding (Claude Code, Roo Code, and Amp Code)
25
26
 
26
27
  ## Installation from npmjs.com
27
28
 
@@ -31,13 +32,10 @@ npm install -g @johnowennixon/diffdash
31
32
 
32
33
  ## LLM Models
33
34
 
34
- Currently, for this application, the best LLM model is **gpt-4.1-mini** from OpenAI.
35
+ Currently, for this application, the best LLM model is **gpt-5-mini-minimal** (GPT-5 Mini with reasoning disabled) from OpenAI.
35
36
  It is set as the default model.
36
37
  I can only presume they have done a ton of training on diffs.
37
38
 
38
- I have tested the GPT-5 models and **gpt-5-mini-minimal** (GPT-5 Mini with reasoning disabled) is behaving much the same.
39
- It will become the default model if gpt-4.1-mini is deprecated.
40
-
41
39
  ## API Keys
42
40
 
43
41
  DiffDash requires at least one API key for an LLM provider. These must be provided as environment variables.
@@ -98,6 +96,9 @@ diffdash --add-prefix "[FIX]"
98
96
  # Add a suffix to the commit message summary line
99
97
  diffdash --add-suffix "(closes #DEV-1234)"
100
98
 
99
+ # Generate commit message in a different language
100
+ diffdash --language af # Afrikaans
101
+
101
102
  # Display commit messages generated by all models
102
103
  diffdash --llm-compare
103
104
 
@@ -129,6 +130,7 @@ All command-line arguments are optional.
129
130
  | `--disable-push` | disable pushing changes - exit after making the commit |
130
131
  | `--add-prefix PREFIX` | add a prefix to the commit message summary line |
131
132
  | `--add-suffix SUFFIX` | add a suffix to the commit message summary line |
133
+ | `--language CODE` | choose the language for commit messages (defaults to 'en' for English) |
132
134
  | `--llm-list` | display a list of available Large Language Models and exit |
133
135
  | `--llm-compare` | compare the generated messages from all models - but do not commit |
134
136
  | `--llm-model MODEL` | choose the LLM model by name (the default is normally best) |
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnowennixon/diffdash",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "A command-line tool to generate Git commit messages using AI",
5
5
  "license": "0BSD",
6
6
  "author": "John Owen Nixon",
@@ -42,19 +42,19 @@
42
42
  "test": "run-s -ls lint build"
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/anthropic": "3.0.8",
46
- "@ai-sdk/deepseek": "2.0.4",
47
- "@ai-sdk/google": "3.0.5",
48
- "@ai-sdk/openai": "3.0.21",
49
- "@inquirer/prompts": "8.2.0",
50
- "@openrouter/ai-sdk-provider": "2.1.1",
51
- "ai": "6.0.17",
45
+ "@ai-sdk/anthropic": "3.0.44",
46
+ "@ai-sdk/deepseek": "2.0.20",
47
+ "@ai-sdk/google": "3.0.29",
48
+ "@ai-sdk/openai": "3.0.29",
49
+ "@inquirer/prompts": "8.2.1",
50
+ "@openrouter/ai-sdk-provider": "2.2.3",
51
+ "ai": "6.0.86",
52
52
  "ansis": "4.2.0",
53
53
  "argparse": "2.0.1",
54
54
  "cli-table3": "0.6.5",
55
55
  "json5": "2.2.3",
56
56
  "magic-regexp": "0.10.0",
57
- "simple-git": "3.30.0",
57
+ "simple-git": "3.31.1",
58
58
  "zod": "4.3.6"
59
59
  },
60
60
  "devDependencies": {
@@ -63,12 +63,12 @@
63
63
  "@johnowennixon/add-shebangs": "1.1.0",
64
64
  "@johnowennixon/chmodx": "2.1.0",
65
65
  "@types/argparse": "2.0.17",
66
- "@types/node": "25.0.3",
66
+ "@types/node": "25.2.3",
67
67
  "@typescript/native-preview": "7.0.0-dev.20260103.1",
68
- "knip": "5.82.1",
68
+ "knip": "5.83.1",
69
69
  "markdownlint-cli2": "0.20.0",
70
70
  "npm-run-all2": "8.0.4",
71
- "oxlint": "1.42.0",
71
+ "oxlint": "1.47.0",
72
72
  "rimraf": "6.1.2",
73
73
  "typescript": "5.9.3"
74
74
  }
@@ -16,6 +16,5 @@ export function ansi_boolean(bool) {
16
16
  }
17
17
  export function ansi_strip(text) {
18
18
  const pattern = String.raw `\u001B\[[^m]*m`;
19
- // eslint-disable-next-line sonarjs/no-control-regex
20
19
  return text.replace(new RegExp(pattern, "g"), EMPTY);
21
20
  }
@@ -1,33 +1,70 @@
1
1
  import { ArgumentParser } from "argparse";
2
2
  import { EMPTY } from "./lib_char_empty.js";
3
- import { DASH, PLUS, UNDERSCORE } from "./lib_char_punctuation.js";
3
+ import { DASH, PLUS, QUESTION, UNDERSCORE } from "./lib_char_punctuation.js";
4
4
  import { debug_channels, debug_inspect_when } from "./lib_debug.js";
5
- export function cli_string(options = {}) {
5
+ export function cli_string_optional(params) {
6
+ const { help, metavar } = params;
7
+ const options = { help, metavar };
6
8
  return { kind: "string", options, value: "" };
7
9
  }
8
- export function cli_string_always(options = {}) {
10
+ export function cli_string_required(params) {
11
+ const { help, metavar } = params;
12
+ const options = { help, metavar, required: true };
9
13
  return { kind: "string", options, value: "" };
10
14
  }
11
- export function cli_integer(options = {}) {
15
+ export function cli_string_default(params) {
16
+ const { help, metavar, default: _default } = params;
17
+ const options = { help, metavar, default: _default };
18
+ return { kind: "string", options, value: "" };
19
+ }
20
+ export function cli_string_positional_required(params) {
21
+ const { help, metavar } = params;
22
+ const options = { help, metavar, positional: true };
23
+ return { kind: "string", options, value: "" };
24
+ }
25
+ export function cli_string_positional_optional(params) {
26
+ const { help, metavar } = params;
27
+ const options = { help, metavar, positional: true, nargs: QUESTION };
28
+ return { kind: "string", options, value: "" };
29
+ }
30
+ export function cli_integer_optional(params) {
31
+ const { help, metavar } = params;
32
+ const options = { help, metavar };
12
33
  return { kind: "integer", options, value: 0 };
13
34
  }
14
- export function cli_integer_always(options = {}) {
35
+ export function cli_integer_default(params) {
36
+ const { help, metavar, default: _default } = params;
37
+ const options = { help, metavar, default: _default };
15
38
  return { kind: "integer", options, value: 0 };
16
39
  }
17
- export function cli_boolean(options = {}) {
40
+ export function cli_boolean_always(params) {
41
+ const { help } = params;
42
+ const options = { help };
18
43
  return { kind: "boolean", options, value: false };
19
44
  }
20
- export function cli_choice_optional(options = {}) {
45
+ export function cli_boolean_default(params) {
46
+ const { help, default: _default } = params;
47
+ const options = { help, default: _default };
48
+ return { kind: "boolean", options, value: false };
49
+ }
50
+ export function cli_choice_optional(params) {
51
+ const { help, metavar, choices } = params;
52
+ const options = { help, metavar, choices };
21
53
  return { kind: "choice", options, value: undefined };
22
54
  }
23
- export function cli_choice_default(options = {}) {
55
+ export function cli_choice_required(params) {
56
+ const { help, metavar, choices } = params;
57
+ const options = { help, metavar, choices, required: true };
24
58
  return { kind: "choice", options, value: undefined };
25
59
  }
26
- export function cli_choice_required(options = {}) {
27
- options.required = true;
60
+ export function cli_choice_default(params) {
61
+ const { help, metavar, choices, default: _default } = params;
62
+ const options = { help, metavar, choices, default: _default };
28
63
  return { kind: "choice", options, value: undefined };
29
64
  }
30
- export function cli_list(options = {}) {
65
+ export function cli_list_positional(params) {
66
+ const { help } = params;
67
+ const options = { help, positional: true };
31
68
  return { kind: "list", options, value: [] };
32
69
  }
33
70
  export function cli_meg_optional(meg_schema) {
@@ -36,14 +73,11 @@ export function cli_meg_optional(meg_schema) {
36
73
  export function cli_meg_required(meg_schema) {
37
74
  return { kind: "meg", options: { required: true }, value: meg_schema };
38
75
  }
39
- export function cli_meg_required_predicate(meg_schema, predicate) {
40
- return { kind: "meg", options: { required: true, predicate }, value: meg_schema };
41
- }
42
76
  function cli_omit(obj, key_to_omit) {
43
77
  const { [key_to_omit]: _, ...rest } = obj;
44
78
  return rest;
45
79
  }
46
- function cli_add_keys({ cli_schema, parser_group, predicate, }) {
80
+ function cli_add_keys({ cli_schema, parser_group }) {
47
81
  for (const key in cli_schema) {
48
82
  if (!Object.hasOwn(cli_schema, key)) {
49
83
  continue;
@@ -52,11 +86,6 @@ function cli_add_keys({ cli_schema, parser_group, predicate, }) {
52
86
  if (!cli) {
53
87
  continue;
54
88
  }
55
- if (predicate !== undefined) {
56
- if (!predicate(key)) {
57
- continue;
58
- }
59
- }
60
89
  const key_replaced = key.replaceAll(UNDERSCORE, DASH);
61
90
  const key_amended = `${cli.options.positional === true ? EMPTY : "--"}${key_replaced}`;
62
91
  const options = cli_omit(cli.options, "positional");
@@ -82,7 +111,6 @@ function cli_add_keys({ cli_schema, parser_group, predicate, }) {
82
111
  cli_add_keys({
83
112
  cli_schema: cli.value,
84
113
  parser_group: mutually_exclusive_group,
85
- predicate: cli.options.predicate,
86
114
  });
87
115
  }
88
116
  break;
@@ -91,7 +119,7 @@ function cli_add_keys({ cli_schema, parser_group, predicate, }) {
91
119
  }
92
120
  }
93
121
  }
94
- function cli_recursive_parse({ schema, namespace, predicate, }) {
122
+ function cli_recursive_parse({ schema, namespace, }) {
95
123
  const result = {};
96
124
  for (const key in schema) {
97
125
  if (!Object.hasOwn(schema, key)) {
@@ -101,17 +129,11 @@ function cli_recursive_parse({ schema, namespace, predicate, }) {
101
129
  if (!cli) {
102
130
  continue;
103
131
  }
104
- if (predicate !== undefined) {
105
- if (!predicate(key)) {
106
- continue;
107
- }
108
- }
109
132
  if (cli.kind === "meg") {
110
133
  const nested_schema = cli.value;
111
134
  result[key] = cli_recursive_parse({
112
135
  schema: nested_schema,
113
136
  namespace,
114
- predicate: cli.options.predicate,
115
137
  });
116
138
  }
117
139
  else {
@@ -4,6 +4,7 @@ import { enabled_from_env } from "./lib_enabled.js";
4
4
  import { inspect_obj_to_string } from "./lib_inspect.js";
5
5
  import { stdio_write_stderr_linefeed } from "./lib_stdio_write.js";
6
6
  import { tell_debug } from "./lib_tell.js";
7
+ import { tui_quote_smart_single as qss } from "./lib_tui_quote.js";
7
8
  export const debug_channels = {
8
9
  api: false,
9
10
  backups: false,
@@ -37,7 +38,7 @@ export const debug_channels = {
37
38
  export function debug_enable_if(channel, enabled) {
38
39
  if (enabled && !debug_channels[channel]) {
39
40
  debug_channels[channel] = true;
40
- tell_debug(`Debugging enabled for ‘${channel}’`);
41
+ tell_debug(`Debugging enabled for ${qss(channel)}`);
41
42
  }
42
43
  }
43
44
  function debug_init() {
@@ -1,33 +1,40 @@
1
- import { cli_boolean, cli_choice_default, cli_make_parser, cli_string } from "./lib_cli.js";
1
+ import { cli_boolean_always, cli_choice_default, cli_make_parser, cli_string_optional } from "./lib_cli.js";
2
2
  import { diffdash_llm_model_choices, diffdash_llm_model_default } from "./lib_diffdash_llm.js";
3
+ import { LANGUAGE_CODE_ENGLISH, language_get_code_choices } from "./lib_language.js";
4
+ import { tui_quote_smart_single as qss } from "./lib_tui_quote.js";
3
5
  const diffdash_cli_schema = {
4
- version: cli_boolean({ help: "show program version information and exit" }),
5
- auto_add: cli_boolean({ help: "automatically stage all changes without confirmation" }),
6
- auto_commit: cli_boolean({ help: "automatically commit changes without confirmation" }),
7
- auto_push: cli_boolean({ help: "automatically push changes after commit without confirmation" }),
8
- disable_add: cli_boolean({ help: "disable adding unstaged changes - exit if no changes staged" }),
9
- disable_status: cli_boolean({ help: "disable listing the staged files before generating a message" }),
10
- disable_preview: cli_boolean({ help: "disable previewing the generated message" }),
11
- disable_commit: cli_boolean({ help: "disable committing changes - exit after generating the message" }),
12
- disable_push: cli_boolean({ help: "disable pushing changes - exit after making the commit" }),
13
- add_prefix: cli_string({ help: "add a prefix to the commit message summary line", metavar: "PREFIX" }),
14
- add_suffix: cli_string({ help: "add a suffix to the commit message summary line", metavar: "SUFFIX" }),
15
- llm_list: cli_boolean({ help: "display a list of available Large Language Models and exit" }),
16
- llm_compare: cli_boolean({ help: "compare the generated messages from all models - but do not commit" }),
6
+ version: cli_boolean_always({ help: "show program version information and exit" }),
7
+ auto_add: cli_boolean_always({ help: "automatically stage all changes without confirmation" }),
8
+ auto_commit: cli_boolean_always({ help: "automatically commit changes without confirmation" }),
9
+ auto_push: cli_boolean_always({ help: "automatically push changes after commit without confirmation" }),
10
+ disable_add: cli_boolean_always({ help: "disable adding unstaged changes - exit if no changes staged" }),
11
+ disable_status: cli_boolean_always({ help: "disable listing the staged files before generating a message" }),
12
+ disable_preview: cli_boolean_always({ help: "disable previewing the generated message" }),
13
+ disable_commit: cli_boolean_always({ help: "disable committing changes - exit after generating the message" }),
14
+ disable_push: cli_boolean_always({ help: "disable pushing changes - exit after making the commit" }),
15
+ add_prefix: cli_string_optional({ help: "add a prefix to the commit message summary line", metavar: "PREFIX" }),
16
+ add_suffix: cli_string_optional({ help: "add a suffix to the commit message summary line", metavar: "SUFFIX" }),
17
+ language: cli_choice_default({
18
+ help: `choose the language for commit messages (defaults to ${qss(LANGUAGE_CODE_ENGLISH)})`,
19
+ choices: language_get_code_choices(),
20
+ default: LANGUAGE_CODE_ENGLISH,
21
+ }),
22
+ llm_list: cli_boolean_always({ help: "display a list of available Large Language Models and exit" }),
23
+ llm_compare: cli_boolean_always({ help: "compare the generated messages from all models - but do not commit" }),
17
24
  llm_model: cli_choice_default({
18
- help: `choose the Large Language Model by name (defaults to ${diffdash_llm_model_default})`,
25
+ help: `choose the Large Language Model by name (defaults to ${qss(diffdash_llm_model_default)})`,
19
26
  choices: diffdash_llm_model_choices,
20
27
  default: diffdash_llm_model_default,
21
28
  }),
22
- llm_excludes: cli_string({ help: "models to exclude from comparison (comma separated)", metavar: "MODELS" }),
23
- no_secret_check: cli_boolean({ help: "bypass checking for secrets in diffs" }),
24
- no_verify: cli_boolean({ help: "bypass git hooks when committing or pushing to Git" }),
25
- force: cli_boolean({ help: "apply force when pushing to Git" }),
26
- just_output: cli_boolean({ help: "just output the commit message for use in scripts" }),
27
- silent: cli_boolean({ help: "suppress all normal output - errors and aborts still display" }),
28
- debug_llm_prompts: cli_boolean({ help: "debug prompts sent to the LLM" }),
29
- debug_llm_inputs: cli_boolean({ help: "debug inputs object sent to the LLM" }),
30
- debug_llm_outputs: cli_boolean({ help: "debug outputs object received from the LLM" }),
29
+ llm_excludes: cli_string_optional({ help: "models to exclude from comparison (comma separated)", metavar: "MODELS" }),
30
+ no_secret_check: cli_boolean_always({ help: "bypass checking for secrets in diffs" }),
31
+ no_verify: cli_boolean_always({ help: "bypass git hooks when committing or pushing to Git" }),
32
+ force: cli_boolean_always({ help: "apply force when pushing to Git" }),
33
+ just_output: cli_boolean_always({ help: "just output the commit message for use in scripts" }),
34
+ silent: cli_boolean_always({ help: "suppress all normal output - errors and aborts still display" }),
35
+ debug_llm_prompts: cli_boolean_always({ help: "debug prompts sent to the LLM" }),
36
+ debug_llm_inputs: cli_boolean_always({ help: "debug inputs object sent to the LLM" }),
37
+ debug_llm_outputs: cli_boolean_always({ help: "debug outputs object received from the LLM" }),
31
38
  };
32
39
  export const diffdash_cli_parser = cli_make_parser({
33
40
  cli_schema: diffdash_cli_schema,
@@ -6,11 +6,12 @@ import { diffdash_llm_model_details } from "./lib_diffdash_llm.js";
6
6
  import { file_io_read_text } from "./lib_file_io.js";
7
7
  import { file_is_file } from "./lib_file_is.js";
8
8
  import { json5_parse } from "./lib_json5.js";
9
+ import { language_get_name_from_code } from "./lib_language.js";
9
10
  import { llm_config_get, llm_config_get_all } from "./lib_llm_config.js";
10
11
  import { llm_list_models } from "./lib_llm_list.js";
11
12
  import { PACKAGE_NAME, PACKAGE_VERSION } from "./lib_package.js";
12
13
  import { tell_plain } from "./lib_tell.js";
13
- import { tui_quote_smart_single } from "./lib_tui_quote.js";
14
+ import { tui_quote_smart_single as qss } from "./lib_tui_quote.js";
14
15
  const diffdash_config_file_schema = z
15
16
  .object({
16
17
  extra_prompts: z.string().array().optional(),
@@ -25,7 +26,7 @@ function diffdash_config_file_read(config) {
25
26
  const parsed_json = json5_parse(config_content);
26
27
  const validation_result = diffdash_config_file_schema.safeParse(parsed_json);
27
28
  if (!validation_result.success) {
28
- abort_with_error(`Unable to parse DiffDash config file: ${tui_quote_smart_single(config_file_name)}`);
29
+ abort_with_error(`Unable to parse DiffDash config file: ${qss(config_file_name)}`);
29
30
  }
30
31
  const data = validation_result.data;
31
32
  if (data.extra_prompts) {
@@ -33,7 +34,7 @@ function diffdash_config_file_read(config) {
33
34
  }
34
35
  }
35
36
  export function diffdash_config_get() {
36
- const { version, auto_add, auto_commit, auto_push, disable_add, disable_commit, disable_preview, disable_status, disable_push, add_prefix, add_suffix, llm_list, llm_compare, llm_model, llm_excludes, no_secret_check, no_verify, force, just_output, silent, debug_llm_prompts, debug_llm_inputs, debug_llm_outputs, } = diffdash_cli_parsed_args;
37
+ const { version, auto_add, auto_commit, auto_push, disable_add, disable_commit, disable_preview, disable_status, disable_push, add_prefix, add_suffix, language, llm_list, llm_compare, llm_model, llm_excludes, no_secret_check, no_verify, force, just_output, silent, debug_llm_prompts, debug_llm_inputs, debug_llm_outputs, } = diffdash_cli_parsed_args;
37
38
  if (version) {
38
39
  tell_plain(`${PACKAGE_NAME} v${PACKAGE_VERSION}`);
39
40
  process.exit(0);
@@ -42,6 +43,7 @@ export function diffdash_config_get() {
42
43
  llm_list_models({ llm_model_details: diffdash_llm_model_details });
43
44
  process.exit(0);
44
45
  }
46
+ const language_name = language_get_name_from_code(language);
45
47
  const llm_config = llm_config_get({ llm_model_details: diffdash_llm_model_details, llm_model_name: llm_model });
46
48
  const all_llm_configs = llm_config_get_all({ llm_model_details: diffdash_llm_model_details, llm_excludes });
47
49
  debug_channels.llm_prompts = debug_llm_prompts;
@@ -58,6 +60,7 @@ export function diffdash_config_get() {
58
60
  disable_push,
59
61
  add_prefix,
60
62
  add_suffix,
63
+ language_name,
61
64
  no_secret_check,
62
65
  no_verify,
63
66
  force,
@@ -1,15 +1,15 @@
1
1
  import { env_get_substitute } from "./lib_env.js";
2
2
  import { llm_model_get_choices, llm_model_get_details } from "./lib_llm_model.js";
3
- const model_name_default = "gpt-4.1-mini";
3
+ const model_name_default = "gpt-5-mini-minimal";
4
4
  const model_name_options = [
5
- "claude-3.5-haiku", // fallback
5
+ "claude-haiku-4.5", // fallback
6
6
  "deepseek-chat",
7
7
  "gemini-2.5-flash",
8
8
  "gemini-3-flash-preview-low",
9
- "gpt-4.1-mini", // the best
9
+ "gpt-4.1-mini", // fallback
10
10
  "gpt-4.1-nano",
11
11
  "gpt-5-mini",
12
- "gpt-5-mini-minimal", // fallback
12
+ "gpt-5-mini-minimal", // the best
13
13
  "gpt-5-nano",
14
14
  "gpt-5-nano-minimal",
15
15
  "grok-code-fast-1",
@@ -93,7 +93,7 @@ async function phase_status({ config, git }) {
93
93
  }
94
94
  async function phase_compare({ config, git }) {
95
95
  const { silent } = config;
96
- const { all_llm_configs, add_prefix, add_suffix, no_secret_check, extra_prompts } = config;
96
+ const { all_llm_configs, add_prefix, add_suffix, no_secret_check, extra_prompts, language_name } = config;
97
97
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
98
98
  const diff = await git_simple_staging_get_staged_diff(git);
99
99
  if (!no_secret_check) {
@@ -104,7 +104,7 @@ async function phase_compare({ config, git }) {
104
104
  abort_with_error(`Aborting: ${error_get_message(error)}`);
105
105
  }
106
106
  }
107
- const inputs = { diffstat, diff, extra_prompts };
107
+ const inputs = { diffstat, diff, extra_prompts, language_name };
108
108
  if (!silent) {
109
109
  tell_action("Generating Git commit messages using all models in parallel");
110
110
  }
@@ -129,7 +129,7 @@ async function phase_compare({ config, git }) {
129
129
  llm_results_summary(all_results);
130
130
  }
131
131
  async function phase_generate({ config, git }) {
132
- const { disable_preview, add_prefix, add_suffix, llm_config, no_secret_check, just_output, silent, extra_prompts } = config;
132
+ const { disable_preview, add_prefix, add_suffix, llm_config, no_secret_check, just_output, silent, extra_prompts, language_name, } = config;
133
133
  const { llm_model_name } = llm_config;
134
134
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
135
135
  const diff = await git_simple_staging_get_staged_diff(git);
@@ -141,7 +141,7 @@ async function phase_generate({ config, git }) {
141
141
  abort_with_error(`Aborting: ${error_get_message(error)}`);
142
142
  }
143
143
  }
144
- const inputs = { diffstat, diff, extra_prompts };
144
+ const inputs = { diffstat, diff, extra_prompts, language_name };
145
145
  if (!silent && !just_output) {
146
146
  tell_action(`Generating the Git commit message using ${llm_model_name}`);
147
147
  }
@@ -2,10 +2,12 @@ import { LF } from "./lib_char_control.js";
2
2
  import { EMPTY } from "./lib_char_empty.js";
3
3
  import { tell_warning } from "./lib_tell.js";
4
4
  const LF_LF = LF + LF;
5
- const portion_role = `
6
- Your role is to generate a Git commit message in conversational English.
5
+ function portion_role(language_name) {
6
+ return (`
7
+ Your role is to generate a Git commit message in conversational ${language_name}.
7
8
  The user does not want Conventional Commits - the summary line must be a normal sentence.
8
- `.trim() + LF_LF;
9
+ `.trim() + LF_LF);
10
+ }
9
11
  const portion_inputs = `
10
12
  The user will send you a <diffstat> block, the output of a 'git diff --staged --stat' command.
11
13
  The user will send you a <diff> block, the output of a 'git diff --staged' command.
@@ -54,7 +56,7 @@ Therefore, you must just output the Git message itself without any introductory
54
56
  `.trim() + LF_LF;
55
57
  export function git_message_prompt_get_system({ has_structured_json, inputs, }) {
56
58
  let system_prompt = EMPTY;
57
- system_prompt += portion_role;
59
+ system_prompt += portion_role(inputs.language_name);
58
60
  system_prompt += portion_inputs;
59
61
  system_prompt += portion_reminders;
60
62
  system_prompt += portion_format(has_structured_json);
@@ -0,0 +1,59 @@
1
+ import { abort_with_error } from "./lib_abort.js";
2
+ import { tui_quote_smart_single as qss } from "./lib_tui_quote.js";
3
+ export const LANGUAGE_CODE_ENGLISH = "en";
4
+ export const LANGUAGE_DETAILS = [
5
+ { code: "af", name: "Afrikaans" },
6
+ { code: "bg", name: "Bulgarian" },
7
+ { code: "bn", name: "Bengali" },
8
+ { code: "ca", name: "Catalan" },
9
+ { code: "cs", name: "Czech" },
10
+ { code: "da", name: "Danish" },
11
+ { code: "de", name: "German" },
12
+ { code: "el", name: "Greek" },
13
+ { code: "en", name: "English" },
14
+ { code: "es", name: "Spanish" },
15
+ { code: "et", name: "Estonian" },
16
+ { code: "fi", name: "Finnish" },
17
+ { code: "fr", name: "French" },
18
+ { code: "hi", name: "Hindi" },
19
+ { code: "hr", name: "Croatian" },
20
+ { code: "hu", name: "Hungarian" },
21
+ { code: "id", name: "Indonesian" },
22
+ { code: "it", name: "Italian" },
23
+ { code: "ja", name: "Japanese" },
24
+ { code: "ko", name: "Korean" },
25
+ { code: "lt", name: "Lithuanian" },
26
+ { code: "lv", name: "Latvian" },
27
+ { code: "ms", name: "Malay" },
28
+ { code: "nl", name: "Dutch" },
29
+ { code: "no", name: "Norwegian" },
30
+ { code: "pl", name: "Polish" },
31
+ { code: "pt", name: "Portuguese" },
32
+ { code: "ro", name: "Romanian" },
33
+ { code: "ru", name: "Russian" },
34
+ { code: "sk", name: "Slovak" },
35
+ { code: "sl", name: "Slovenian" },
36
+ { code: "sr", name: "Serbian" },
37
+ { code: "sv", name: "Swedish" },
38
+ { code: "sw", name: "Swahili" },
39
+ { code: "ta", name: "Tamil" },
40
+ { code: "te", name: "Telugu" },
41
+ { code: "th", name: "Thai" },
42
+ { code: "tr", name: "Turkish" },
43
+ { code: "uk", name: "Ukrainian" },
44
+ { code: "vi", name: "Vietnamese" },
45
+ { code: "zh-CN", name: "Chinese (Simplified)" },
46
+ { code: "zh-HK", name: "Chinese (Traditional)" },
47
+ { code: "zh-SG", name: "Chinese (Simplified)" },
48
+ { code: "zh-TW", name: "Chinese (Traditional)" },
49
+ ];
50
+ export function language_get_code_choices() {
51
+ return LANGUAGE_DETAILS.map((language) => language.code);
52
+ }
53
+ export function language_get_name_from_code(code) {
54
+ const language_entry = LANGUAGE_DETAILS.find((language) => language.code === code);
55
+ if (!language_entry) {
56
+ abort_with_error(`Unknown language code: ${qss(code)}`);
57
+ }
58
+ return language_entry.name;
59
+ }
@@ -54,6 +54,19 @@ export const LLM_MODEL_DETAILS = [
54
54
  recommended_temperature: undefined,
55
55
  provider_options: provider_options_anthropic({ thinking: false }),
56
56
  },
57
+ {
58
+ llm_model_name: "claude-haiku-4.5",
59
+ llm_model_code: "claude-haiku-4-5",
60
+ llm_api_code: "anthropic",
61
+ context_window: 200_000,
62
+ max_output_tokens: 64_000,
63
+ cents_input: 100,
64
+ cents_output: 500,
65
+ default_reasoning: false,
66
+ has_structured_json: true,
67
+ recommended_temperature: undefined,
68
+ provider_options: provider_options_anthropic({ thinking: false }),
69
+ },
57
70
  {
58
71
  llm_model_name: "claude-opus-4.5",
59
72
  llm_model_code: "claude-opus-4-5",
@@ -80,6 +93,32 @@ export const LLM_MODEL_DETAILS = [
80
93
  recommended_temperature: undefined,
81
94
  provider_options: provider_options_anthropic({ thinking: true }),
82
95
  },
96
+ {
97
+ llm_model_name: "claude-opus-4.6",
98
+ llm_model_code: "claude-opus-4-6",
99
+ llm_api_code: "anthropic",
100
+ context_window: 200_000,
101
+ max_output_tokens: 64_000,
102
+ cents_input: 300, // for input tokens <= 200K
103
+ cents_output: 1500, // for input tokens <= 200K
104
+ default_reasoning: false,
105
+ has_structured_json: true,
106
+ recommended_temperature: undefined,
107
+ provider_options: provider_options_anthropic({ thinking: false }),
108
+ },
109
+ {
110
+ llm_model_name: "claude-opus-4.6-thinking",
111
+ llm_model_code: "claude-opus-4-6",
112
+ llm_api_code: "anthropic",
113
+ context_window: 200_000,
114
+ max_output_tokens: 64_000 - 1024,
115
+ cents_input: 300, // for input tokens <= 200K
116
+ cents_output: 1500, // for input tokens <= 200K
117
+ default_reasoning: false,
118
+ has_structured_json: true,
119
+ recommended_temperature: undefined,
120
+ provider_options: provider_options_anthropic({ thinking: true }),
121
+ },
83
122
  {
84
123
  llm_model_name: "claude-sonnet-4",
85
124
  llm_model_code: "claude-sonnet-4-0",
@@ -275,6 +314,19 @@ export const LLM_MODEL_DETAILS = [
275
314
  recommended_temperature: undefined,
276
315
  provider_options: provider_options_openrouter({ only: "z-ai" }),
277
316
  },
317
+ {
318
+ llm_model_name: "glm-4.7-flash@z-ai",
319
+ llm_model_code: "z-ai/glm-4.7-flash",
320
+ llm_api_code: "openrouter",
321
+ context_window: 200_000,
322
+ max_output_tokens: 131_072,
323
+ cents_input: 7,
324
+ cents_output: 40,
325
+ default_reasoning: true,
326
+ has_structured_json: false,
327
+ recommended_temperature: undefined,
328
+ provider_options: provider_options_openrouter({ only: "z-ai" }),
329
+ },
278
330
  {
279
331
  llm_model_name: "gpt-4.1",
280
332
  llm_model_code: "gpt-4.1",
@@ -548,6 +600,19 @@ export const LLM_MODEL_DETAILS = [
548
600
  recommended_temperature: undefined,
549
601
  provider_options: provider_options_openrouter({ only: "groq" }),
550
602
  },
603
+ {
604
+ llm_model_name: "kimi-k2.5",
605
+ llm_model_code: "moonshotai/kimi-k2.5",
606
+ llm_api_code: "openrouter",
607
+ context_window: 131_072,
608
+ max_output_tokens: 131_072,
609
+ cents_input: 60,
610
+ cents_output: 300,
611
+ default_reasoning: false,
612
+ has_structured_json: true,
613
+ recommended_temperature: undefined,
614
+ provider_options: provider_options_openrouter({ only: "moonshotai" }),
615
+ },
551
616
  {
552
617
  llm_model_name: "llama-4-maverick@groq",
553
618
  llm_model_code: "meta-llama/llama-4-maverick",
@@ -626,6 +691,19 @@ export const LLM_MODEL_DETAILS = [
626
691
  recommended_temperature: undefined,
627
692
  provider_options: provider_options_openrouter({ only: "minimax" }),
628
693
  },
694
+ {
695
+ llm_model_name: "minimax-m2.5",
696
+ llm_model_code: "minimax/minimax-m2.5",
697
+ llm_api_code: "openrouter",
698
+ context_window: 204_800,
699
+ max_output_tokens: 131_072,
700
+ cents_input: 30,
701
+ cents_output: 120,
702
+ default_reasoning: false,
703
+ has_structured_json: false,
704
+ recommended_temperature: undefined,
705
+ provider_options: provider_options_openrouter({ only: "minimax" }),
706
+ },
629
707
  {
630
708
  llm_model_name: "mistral-medium-3.1",
631
709
  llm_model_code: "mistralai/mistral-medium-3.1",
@@ -3,7 +3,7 @@ import { ansi_yellow } from "./lib_ansi.js";
3
3
  import { DASH } from "./lib_char_punctuation.js";
4
4
  import { text_split_lines } from "./lib_text.js";
5
5
  import { tui_confirm } from "./lib_tui_confirm.js";
6
- import { tui_quote_smart_single } from "./lib_tui_quote.js";
6
+ import { tui_quote_smart_single as qss } from "./lib_tui_quote.js";
7
7
  const regexp_word_global = createRegExp(oneOrMore(anyOf(wordChar, exactly(DASH))), [global]);
8
8
  const regexp_segment_global = createRegExp(oneOrMore(anyOf(letter, digit)), [global]);
9
9
  const regexp_identifier_exactly = createRegExp(anyOf(
@@ -66,7 +66,7 @@ async function is_secret_segment(segment, not_secret_segments, interactive) {
66
66
  }
67
67
  if (interactive) {
68
68
  const confirmed_is_secret = await tui_confirm({
69
- question: `Is ${tui_quote_smart_single(segment)} a secret?`,
69
+ question: `Is ${qss(segment)} a secret?`,
70
70
  default: false,
71
71
  style_message: ansi_yellow,
72
72
  });
@@ -82,7 +82,7 @@ export async function secret_check({ text, interactive }) {
82
82
  const lines = text_split_lines(text);
83
83
  for (const line of lines.toReversed()) {
84
84
  if (is_secret_line(line)) {
85
- throw new Error(`Secret detected: ${tui_quote_smart_single(line.trim())}`);
85
+ throw new Error(`Secret detected: ${qss(line.trim())}`);
86
86
  }
87
87
  const words = line.match(regexp_word_global);
88
88
  const segments = line.match(regexp_segment_global);
@@ -97,12 +97,12 @@ export async function secret_check({ text, interactive }) {
97
97
  }
98
98
  for (const word of words) {
99
99
  if (is_secret_word(word)) {
100
- throw new Error(`Secret detected: ${tui_quote_smart_single(word)}`);
100
+ throw new Error(`Secret detected: ${qss(word)}`);
101
101
  }
102
102
  }
103
103
  for (const segment of segments) {
104
104
  if (await is_secret_segment(segment, not_secret_segments, interactive)) {
105
- throw new Error(`Secret detected: ${tui_quote_smart_single(segment)}`);
105
+ throw new Error(`Secret detected: ${qss(segment)}`);
106
106
  }
107
107
  }
108
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnowennixon/diffdash",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "A command-line tool to generate Git commit messages using AI",
5
5
  "license": "0BSD",
6
6
  "author": "John Owen Nixon",
@@ -42,19 +42,19 @@
42
42
  "test": "run-s -ls lint build"
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/anthropic": "3.0.8",
46
- "@ai-sdk/deepseek": "2.0.4",
47
- "@ai-sdk/google": "3.0.5",
48
- "@ai-sdk/openai": "3.0.21",
49
- "@inquirer/prompts": "8.2.0",
50
- "@openrouter/ai-sdk-provider": "2.1.1",
51
- "ai": "6.0.17",
45
+ "@ai-sdk/anthropic": "3.0.44",
46
+ "@ai-sdk/deepseek": "2.0.20",
47
+ "@ai-sdk/google": "3.0.29",
48
+ "@ai-sdk/openai": "3.0.29",
49
+ "@inquirer/prompts": "8.2.1",
50
+ "@openrouter/ai-sdk-provider": "2.2.3",
51
+ "ai": "6.0.86",
52
52
  "ansis": "4.2.0",
53
53
  "argparse": "2.0.1",
54
54
  "cli-table3": "0.6.5",
55
55
  "json5": "2.2.3",
56
56
  "magic-regexp": "0.10.0",
57
- "simple-git": "3.30.0",
57
+ "simple-git": "3.31.1",
58
58
  "zod": "4.3.6"
59
59
  },
60
60
  "devDependencies": {
@@ -63,12 +63,12 @@
63
63
  "@johnowennixon/add-shebangs": "1.1.0",
64
64
  "@johnowennixon/chmodx": "2.1.0",
65
65
  "@types/argparse": "2.0.17",
66
- "@types/node": "25.0.3",
66
+ "@types/node": "25.2.3",
67
67
  "@typescript/native-preview": "7.0.0-dev.20260103.1",
68
- "knip": "5.82.1",
68
+ "knip": "5.83.1",
69
69
  "markdownlint-cli2": "0.20.0",
70
70
  "npm-run-all2": "8.0.4",
71
- "oxlint": "1.42.0",
71
+ "oxlint": "1.47.0",
72
72
  "rimraf": "6.1.2",
73
73
  "typescript": "5.9.3"
74
74
  }
@@ -1 +0,0 @@
1
- export {};