@johnowennixon/diffdash 1.8.0 → 1.10.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
@@ -1,5 +1,8 @@
1
1
  # DiffDash
2
2
 
3
+ ![npm version](https://img.shields.io/npm/v/@johnowennixon/diffdash.svg)
4
+ ![NPM License](https://img.shields.io/npm/l/@johnowennixon/diffdash)
5
+ ![Downloads](https://img.shields.io/npm/dm/@johnowennixon/diffdash.svg)
3
6
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/johnowennixon/diffdash)
4
7
 
5
8
  A command-line tool to generate Git commit messages using AI.
@@ -126,14 +129,15 @@ All command-line arguments are optional.
126
129
  | `--disable-preview` | disable previewing the generated message|
127
130
  | `--disable-commit` | disable committing changes - exit after generating the message |
128
131
  | `--disable-push` | disable pushing changes - exit after making the commit |
129
- | `--push-no-verify` | bypass git hooks when pushing to Git |
130
- | `--push-force` | apply force when pushing to Git |
131
132
  | `--add-prefix PREFIX` | add a prefix to the commit message summary line |
132
133
  | `--add-suffix SUFFIX` | add a suffix to the commit message summary line |
133
134
  | `--llm-list` | display a list of available Large Language Models and exit |
134
135
  | `--llm-compare` | compare the generated messages from all models - but do not commit |
135
136
  | `--llm-model MODEL` | choose the LLM model by name (the default is normally best) |
136
137
  | `--llm-excludes MODELS` | models to exclude from comparison (comma separated) |
138
+ | `--no-secret-check` | bypass checking for secrets in diffs |
139
+ | `--no-verify` | bypass git hooks when committing or pushing to Git |
140
+ | `--force` | apply force when pushing to Git |
137
141
  | `--just-output` | just output the commit message for use in scripts |
138
142
  | `--silent` | suppress all normal output - errors and aborts still display |
139
143
  | `--debug-llm-prompts` | show prompts sent to the LLM |
@@ -150,6 +154,8 @@ Files containing secrets should not be in Git. But if they are, you can add an e
150
154
  .env -diff
151
155
  ```
152
156
 
157
+ There is a rudimentary check for secrets in diffs before submitting to the LLM. If any are found, there is an interactive option to ignore. If you want to bypass this check, you can use the `--no-secret-check` flag.
158
+
153
159
  ## Development
154
160
 
155
161
  To install on your laptop:
package/dist/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@johnowennixon/diffdash",
3
- "version": "1.8.0",
3
+ "version": "1.10.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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/johnowennixon/diffdash.git"
9
+ "url": "git+https://github.com/johnowennixon/diffdash.git"
10
10
  },
11
11
  "engines": {
12
12
  "node": ">=20"
@@ -42,32 +42,34 @@
42
42
  "test": "run-s -ls lint build"
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/anthropic": "2.0.1",
46
- "@ai-sdk/deepseek": "1.0.5",
47
- "@ai-sdk/google": "2.0.4",
48
- "@ai-sdk/openai": "2.0.9",
49
- "@openrouter/ai-sdk-provider": "1.1.2",
50
- "ai": "5.0.9",
51
- "ansis": "4.1.0",
45
+ "@ai-sdk/anthropic": "2.0.23",
46
+ "@ai-sdk/deepseek": "1.0.20",
47
+ "@ai-sdk/google": "2.0.17",
48
+ "@ai-sdk/openai": "2.0.42",
49
+ "@inquirer/prompts": "7.8.6",
50
+ "@openrouter/ai-sdk-provider": "1.2.0",
51
+ "ai": "5.0.60",
52
+ "ansis": "4.2.0",
52
53
  "argparse": "2.0.1",
53
54
  "cli-table3": "0.6.5",
54
55
  "json5": "2.2.3",
56
+ "magic-regexp": "0.10.0",
55
57
  "simple-git": "3.28.0",
56
- "zod": "4.0.17"
58
+ "zod": "4.1.11"
57
59
  },
58
60
  "devDependencies": {
59
- "@biomejs/biome": "2.1.4",
60
- "@candide/tsgolint": "1.3.0",
61
+ "@biomejs/biome": "2.2.5",
62
+ "@candide/tsgolint": "1.4.0",
61
63
  "@johnowennixon/add-shebangs": "1.1.0",
62
- "@johnowennixon/chmodx": "2.0.0",
64
+ "@johnowennixon/chmodx": "2.1.0",
63
65
  "@types/argparse": "2.0.17",
64
- "@types/node": "24.2.1",
65
- "@typescript/native-preview": "7.0.0-dev.20250811.1",
66
- "knip": "5.62.0",
66
+ "@types/node": "24.5.2",
67
+ "@typescript/native-preview": "7.0.0-dev.20250925.1",
68
+ "knip": "5.63.1",
67
69
  "markdownlint-cli2": "0.18.1",
68
70
  "npm-run-all2": "8.0.4",
69
- "oxlint": "1.11.1",
71
+ "oxlint": "1.19.0",
70
72
  "rimraf": "6.0.1",
71
- "typescript": "5.9.2"
73
+ "typescript": "5.9.3"
72
74
  }
73
75
  }
@@ -8,3 +8,4 @@ export const DIGIT_6 = "6";
8
8
  export const DIGIT_7 = "7";
9
9
  export const DIGIT_8 = "8";
10
10
  export const DIGIT_9 = "9";
11
+ export const DIGITS = "0123456789";
@@ -17,12 +17,6 @@ export function cli_integer_always(options = {}) {
17
17
  export function cli_boolean(options = {}) {
18
18
  return { kind: "boolean", options, value: false };
19
19
  }
20
- export function cli_command_sync(command_sync, options = {}) {
21
- return { kind: "boolean", options, value: false, command_sync };
22
- }
23
- export function cli_command_async(command_async, options = {}) {
24
- return { kind: "boolean", options, value: false, command_async };
25
- }
26
20
  export function cli_choice_optional(options = {}) {
27
21
  return { kind: "choice", options, value: undefined };
28
22
  }
@@ -126,50 +120,6 @@ function cli_recursive_parse({ schema, namespace, predicate, }) {
126
120
  }
127
121
  return result;
128
122
  }
129
- function cli_recursive_despatch_sync({ schema, namespace, parsed_args, }) {
130
- for (const key in schema) {
131
- if (!Object.hasOwn(schema, key)) {
132
- continue;
133
- }
134
- const cli = schema[key];
135
- if (!cli) {
136
- continue;
137
- }
138
- if (cli.kind === "meg") {
139
- const nested_schema = cli.value;
140
- cli_recursive_despatch_sync({ schema: nested_schema, namespace, parsed_args });
141
- }
142
- else if (cli.kind === "boolean") {
143
- if (namespace[key]) {
144
- if (cli.command_sync) {
145
- cli.command_sync(parsed_args);
146
- }
147
- }
148
- }
149
- }
150
- }
151
- async function cli_recursive_despatch_async({ schema, namespace, parsed_args, }) {
152
- for (const key in schema) {
153
- if (!Object.hasOwn(schema, key)) {
154
- continue;
155
- }
156
- const cli = schema[key];
157
- if (!cli) {
158
- continue;
159
- }
160
- if (cli.kind === "meg") {
161
- const nested_schema = cli.value;
162
- await cli_recursive_despatch_async({ schema: nested_schema, namespace, parsed_args });
163
- }
164
- else if (cli.kind === "boolean") {
165
- if (namespace[key]) {
166
- if (cli.command_async) {
167
- await cli.command_async(parsed_args);
168
- }
169
- }
170
- }
171
- }
172
- }
173
123
  export function cli_make_parser({ cli_schema, description, }) {
174
124
  const argument_parser_options = { description, allow_abbrev: false };
175
125
  const parser = new ArgumentParser(argument_parser_options);
@@ -177,11 +127,5 @@ export function cli_make_parser({ cli_schema, description, }) {
177
127
  const namespace = parser.parse_args();
178
128
  debug_inspect_when(debug_channels.cli, namespace, "namespace");
179
129
  const parsed_args = cli_recursive_parse({ schema: cli_schema, namespace });
180
- function despatch_sync() {
181
- cli_recursive_despatch_sync({ schema: cli_schema, namespace, parsed_args });
182
- }
183
- async function despatch_async() {
184
- await cli_recursive_despatch_async({ schema: cli_schema, namespace, parsed_args });
185
- }
186
- return { parsed_args, despatch_sync, despatch_async };
130
+ return { parsed_args };
187
131
  }
@@ -29,6 +29,7 @@ export const debug_channels = {
29
29
  path: false,
30
30
  kubectl: false,
31
31
  postgresql: false,
32
+ regex: false,
32
33
  rejects: false,
33
34
  retries: false,
34
35
  sql: false,
@@ -10,8 +10,6 @@ const diffdash_cli_schema = {
10
10
  disable_preview: cli_boolean({ help: "disable previewing the generated message" }),
11
11
  disable_commit: cli_boolean({ help: "disable committing changes - exit after generating the message" }),
12
12
  disable_push: cli_boolean({ help: "disable pushing changes - exit after making the commit" }),
13
- push_no_verify: cli_boolean({ help: "bypass git hooks when pushing to Git" }),
14
- push_force: cli_boolean({ help: "apply force when pushing to Git" }),
15
13
  add_prefix: cli_string({ help: "add a prefix to the commit message summary line", metavar: "PREFIX" }),
16
14
  add_suffix: cli_string({ help: "add a suffix to the commit message summary line", metavar: "SUFFIX" }),
17
15
  llm_list: cli_boolean({ help: "display a list of available Large Language Models and exit" }),
@@ -22,6 +20,9 @@ const diffdash_cli_schema = {
22
20
  default: diffdash_llm_model_default,
23
21
  }),
24
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" }),
25
26
  just_output: cli_boolean({ help: "just output the commit message for use in scripts" }),
26
27
  silent: cli_boolean({ help: "suppress all normal output - errors and aborts still display" }),
27
28
  debug_llm_prompts: cli_boolean({ help: "debug prompts sent to the LLM" }),
@@ -33,7 +33,7 @@ function diffdash_config_file_read(config) {
33
33
  }
34
34
  }
35
35
  export function diffdash_config_get() {
36
- const { version, auto_add, auto_commit, auto_push, disable_add, disable_commit, disable_preview, disable_status, disable_push, push_no_verify, push_force, add_prefix, add_suffix, llm_list, llm_compare, llm_model, llm_excludes, just_output, silent, debug_llm_prompts, debug_llm_inputs, debug_llm_outputs, } = diffdash_cli_parsed_args;
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
37
  if (version) {
38
38
  tell_plain(`${PACKAGE_NAME} v${PACKAGE_VERSION}`);
39
39
  process.exit(0);
@@ -56,10 +56,11 @@ export function diffdash_config_get() {
56
56
  disable_preview,
57
57
  disable_status,
58
58
  disable_push,
59
- push_no_verify,
60
- push_force,
61
59
  add_prefix,
62
60
  add_suffix,
61
+ no_secret_check,
62
+ no_verify,
63
+ force,
63
64
  llm_compare,
64
65
  llm_config,
65
66
  all_llm_configs,
@@ -12,6 +12,7 @@ const model_name_options = [
12
12
  "gpt-5-mini-minimal", // fallback
13
13
  "gpt-5-nano",
14
14
  "gpt-5-nano-minimal",
15
+ "grok-code-fast-1",
15
16
  "llama-4-maverick@cerebras",
16
17
  ];
17
18
  export const diffdash_llm_model_details = llm_model_get_details({ llm_model_names: model_name_options });
@@ -1,17 +1,19 @@
1
1
  import { abort_with_error, abort_with_warning } from "./lib_abort.js";
2
+ import { ansi_blue } from "./lib_ansi.js";
2
3
  import { debug_channels, debug_inspect } from "./lib_debug.js";
3
4
  import { diffdash_add_footer, diffdash_add_prefix_or_suffix } from "./lib_diffdash_add.js";
4
- import { error_get_text } from "./lib_error.js";
5
+ import { error_get_message, error_get_text } from "./lib_error.js";
5
6
  import { git_message_display } from "./lib_git_message_display.js";
6
7
  import { git_message_generate_result } from "./lib_git_message_generate.js";
7
8
  import { git_message_validate_check, git_message_validate_get_result } from "./lib_git_message_validate.js";
8
9
  import { git_simple_open_check_not_bare, git_simple_open_git_repo } from "./lib_git_simple_open.js";
9
10
  import { git_simple_staging_create_commit, git_simple_staging_get_staged_diff, git_simple_staging_get_staged_diffstat, git_simple_staging_has_staged_changes, git_simple_staging_has_unstaged_changes, git_simple_staging_push_to_remote, git_simple_staging_stage_all_changes, } from "./lib_git_simple_staging.js";
10
11
  import { llm_results_summary } from "./lib_llm_results.js";
12
+ import { secret_check } from "./lib_secret_check.js";
11
13
  import { stdio_write_stdout, stdio_write_stdout_linefeed } from "./lib_stdio_write.js";
12
14
  import { tell_action, tell_info, tell_plain, tell_success, tell_warning } from "./lib_tell.js";
15
+ import { tui_confirm } from "./lib_tui_confirm.js";
13
16
  import { tui_justify_left } from "./lib_tui_justify.js";
14
- import { tui_readline_confirm } from "./lib_tui_readline.js";
15
17
  async function phase_open() {
16
18
  const git = await git_simple_open_git_repo();
17
19
  await git_simple_open_check_not_bare(git);
@@ -40,7 +42,11 @@ async function phase_add({ config, git }) {
40
42
  }
41
43
  }
42
44
  else {
43
- const add_confirmed = await tui_readline_confirm("No staged changes found - would you like to add all changes?");
45
+ const add_confirmed = await tui_confirm({
46
+ question: "No staged changes found - would you like to add all changes?",
47
+ default: true,
48
+ style_message: ansi_blue,
49
+ });
44
50
  if (!add_confirmed) {
45
51
  abort_with_warning("Please add changes before creating a commit");
46
52
  }
@@ -87,13 +93,21 @@ async function phase_status({ config, git }) {
87
93
  }
88
94
  async function phase_compare({ config, git }) {
89
95
  const { silent } = config;
90
- if (!silent) {
91
- tell_action("Generating Git commit messages using all models in parallel");
92
- }
93
- const { all_llm_configs, add_prefix, add_suffix, extra_prompts } = config;
96
+ const { all_llm_configs, add_prefix, add_suffix, no_secret_check, extra_prompts } = config;
94
97
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
95
98
  const diff = await git_simple_staging_get_staged_diff(git);
99
+ if (!no_secret_check) {
100
+ try {
101
+ await secret_check({ text: diff, interactive: true });
102
+ }
103
+ catch (error) {
104
+ abort_with_error(`Aborting: ${error_get_message(error)}`);
105
+ }
106
+ }
96
107
  const inputs = { diffstat, diff, extra_prompts };
108
+ if (!silent) {
109
+ tell_action("Generating Git commit messages using all models in parallel");
110
+ }
97
111
  const result_promises = all_llm_configs.map((llm_config) => git_message_generate_result({ llm_config, inputs }));
98
112
  const all_results = await Promise.all(result_promises);
99
113
  for (const result of all_results) {
@@ -115,14 +129,22 @@ async function phase_compare({ config, git }) {
115
129
  llm_results_summary(all_results);
116
130
  }
117
131
  async function phase_generate({ config, git }) {
118
- const { disable_preview, add_prefix, add_suffix, llm_config, just_output, silent, extra_prompts } = config;
132
+ const { disable_preview, add_prefix, add_suffix, llm_config, no_secret_check, just_output, silent, extra_prompts } = config;
119
133
  const { llm_model_name } = llm_config;
120
- if (!silent && !just_output) {
121
- tell_action(`Generating the Git commit message using ${llm_model_name}`);
122
- }
123
134
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
124
135
  const diff = await git_simple_staging_get_staged_diff(git);
136
+ if (!no_secret_check) {
137
+ try {
138
+ await secret_check({ text: diff, interactive: true });
139
+ }
140
+ catch (error) {
141
+ abort_with_error(`Aborting: ${error_get_message(error)}`);
142
+ }
143
+ }
125
144
  const inputs = { diffstat, diff, extra_prompts };
145
+ if (!silent && !just_output) {
146
+ tell_action(`Generating the Git commit message using ${llm_model_name}`);
147
+ }
126
148
  const result = await git_message_generate_result({ llm_config, inputs });
127
149
  const { error_text } = result;
128
150
  if (error_text !== null) {
@@ -142,7 +164,7 @@ async function phase_generate({ config, git }) {
142
164
  return git_message;
143
165
  }
144
166
  async function phase_commit({ config, git, git_message, }) {
145
- const { auto_commit, disable_commit, silent } = config;
167
+ const { auto_commit, disable_commit, no_verify, silent } = config;
146
168
  if (disable_commit) {
147
169
  return;
148
170
  }
@@ -152,18 +174,22 @@ async function phase_commit({ config, git, git_message, }) {
152
174
  }
153
175
  }
154
176
  else {
155
- const commit_confirmed = await tui_readline_confirm("Do you want to commit these changes?");
177
+ const commit_confirmed = await tui_confirm({
178
+ question: "Do you want to commit these changes?",
179
+ default: true,
180
+ style_message: ansi_blue,
181
+ });
156
182
  if (!commit_confirmed) {
157
183
  abort_with_warning("Commit cancelled by user");
158
184
  }
159
185
  }
160
- await git_simple_staging_create_commit(git, git_message);
186
+ await git_simple_staging_create_commit({ git, git_message, no_verify });
161
187
  if (!silent) {
162
188
  tell_success("Changes committed successfully");
163
189
  }
164
190
  }
165
191
  async function phase_push({ config, git }) {
166
- const { auto_push, disable_commit, disable_push, push_no_verify, push_force, silent } = config;
192
+ const { auto_push, disable_commit, disable_push, no_verify, force, silent } = config;
167
193
  if (disable_push || disable_commit) {
168
194
  return;
169
195
  }
@@ -173,13 +199,17 @@ async function phase_push({ config, git }) {
173
199
  }
174
200
  }
175
201
  else {
176
- const push_confirmed = await tui_readline_confirm("Do you want to push these changes?");
202
+ const push_confirmed = await tui_confirm({
203
+ question: "Do you want to push these changes?",
204
+ default: true,
205
+ style_message: ansi_blue,
206
+ });
177
207
  if (!push_confirmed) {
178
208
  return;
179
209
  }
180
210
  }
181
211
  try {
182
- await git_simple_staging_push_to_remote({ git, no_verify: push_no_verify, force: push_force });
212
+ await git_simple_staging_push_to_remote({ git, no_verify, force });
183
213
  }
184
214
  catch (error) {
185
215
  abort_with_error(`Failed to push to remote: ${error_get_text(error)}`);
@@ -1,39 +1,41 @@
1
1
  import { Duration } from "./lib_duration.js";
2
2
  import { error_get_text } from "./lib_error.js";
3
- import { git_message_get_system_prompt, git_message_get_user_prompt } from "./lib_git_message_prompt.js";
3
+ import { git_message_prompt_get_system, git_message_prompt_get_user } from "./lib_git_message_prompt.js";
4
4
  import { git_message_schema, git_message_schema_format } from "./lib_git_message_schema.js";
5
5
  import { llm_chat_generate_object, llm_chat_generate_text } from "./lib_llm_chat.js";
6
- import { llm_tokens_count_estimated, llm_tokens_debug_usage } from "./lib_llm_tokens.js";
7
- async function git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, }) {
8
- const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt });
6
+ import { llm_tokens_debug_usage, llm_tokens_estimate_length_from_tokens, llm_tokens_estimate_tokens_from_length, } from "./lib_llm_tokens.js";
7
+ async function git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
8
+ const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens });
9
9
  return outputs;
10
10
  }
11
- async function git_message_generate_structured({ llm_config, system_prompt, user_prompt, }) {
11
+ async function git_message_generate_structured({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
12
12
  const schema = git_message_schema;
13
13
  const { generated_object, total_usage, provider_metadata } = await llm_chat_generate_object({
14
14
  llm_config,
15
15
  system_prompt,
16
16
  user_prompt,
17
+ max_output_tokens,
17
18
  schema,
18
19
  });
19
20
  const generated_text = git_message_schema_format(generated_object);
20
21
  return { generated_text, reasoning_text: undefined, total_usage, provider_metadata };
21
22
  }
22
- export async function git_message_generate_string({ llm_config, inputs, }) {
23
- const { context_window, has_structured_json } = llm_config.llm_model_detail;
24
- const system_prompt = git_message_get_system_prompt({ has_structured_json, inputs });
25
- // Estimate remaining prompt length
26
- const user_tokens = context_window - llm_tokens_count_estimated({ llm_config, text: system_prompt }) - 1000;
27
- const user_length = user_tokens * 3;
28
- const user_prompt = git_message_get_user_prompt({
23
+ async function git_message_generate_outputs({ llm_config, inputs, }) {
24
+ const { effective_context_window } = llm_config;
25
+ const { has_structured_json } = llm_config.llm_model_detail;
26
+ const system_prompt = git_message_prompt_get_system({ has_structured_json, inputs });
27
+ const user_tokens = effective_context_window - llm_tokens_estimate_tokens_from_length({ llm_config, length: system_prompt.length }) - 1000;
28
+ const user_length = llm_tokens_estimate_length_from_tokens({ llm_config, tokens: user_tokens });
29
+ const user_prompt = git_message_prompt_get_user({
29
30
  has_structured_json,
30
31
  inputs,
31
32
  max_length: user_length,
32
33
  });
34
+ const max_output_tokens = 8192; // This is the maximum for some models
33
35
  llm_tokens_debug_usage({ name: "Inputs", llm_config, text: system_prompt + user_prompt });
34
36
  const outputs = has_structured_json
35
- ? await git_message_generate_structured({ llm_config, system_prompt, user_prompt })
36
- : await git_message_generate_unstructured({ llm_config, system_prompt, user_prompt });
37
+ ? await git_message_generate_structured({ llm_config, system_prompt, user_prompt, max_output_tokens })
38
+ : await git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, max_output_tokens });
37
39
  llm_tokens_debug_usage({ name: "Outputs", llm_config, text: outputs.generated_text });
38
40
  return outputs;
39
41
  }
@@ -41,7 +43,7 @@ export async function git_message_generate_result({ llm_config, inputs, }) {
41
43
  const duration = new Duration();
42
44
  duration.start();
43
45
  try {
44
- const outputs = await git_message_generate_string({ llm_config, inputs });
46
+ const outputs = await git_message_generate_outputs({ llm_config, inputs });
45
47
  duration.stop();
46
48
  const seconds = duration.seconds_rounded();
47
49
  return { llm_config, seconds, error_text: null, outputs };
@@ -1,5 +1,6 @@
1
1
  import { LF } from "./lib_char_control.js";
2
2
  import { EMPTY } from "./lib_char_empty.js";
3
+ import { tell_warning } from "./lib_tell.js";
3
4
  const LF_LF = LF + LF;
4
5
  const portion_role = `
5
6
  Your role is to generate a Git commit message in conversational English.
@@ -45,13 +46,13 @@ A simple change needs only two additional sentences scaling up to a complex chan
45
46
  If there are a lot of changes, you will need to summarize even more.
46
47
  `.trim() + LF_LF;
47
48
  const portion_extra = (extra_prompts) => {
48
- return extra_prompts && extra_prompts.length > 0 ? extra_prompts.map((s) => s.trim).join(LF) + LF_LF : EMPTY;
49
+ return extra_prompts && extra_prompts.length > 0 ? extra_prompts.map((s) => s.trim()).join(LF) + LF_LF : EMPTY;
49
50
  };
50
51
  const portion_final = `
51
52
  Everything you write will be checked for validity and then saved directly to Git - it will not be reviewed by a human.
52
53
  Therefore, you must just output the Git message itself without any introductory or concluding sections.
53
54
  `.trim() + LF_LF;
54
- export function git_message_get_system_prompt({ has_structured_json, inputs, }) {
55
+ export function git_message_prompt_get_system({ has_structured_json, inputs, }) {
55
56
  let system_prompt = EMPTY;
56
57
  system_prompt += portion_role;
57
58
  system_prompt += portion_inputs;
@@ -62,9 +63,12 @@ export function git_message_get_system_prompt({ has_structured_json, inputs, })
62
63
  system_prompt += portion_final;
63
64
  return system_prompt.trim();
64
65
  }
65
- export function git_message_get_user_prompt({ has_structured_json, inputs, max_length, }) {
66
+ export function git_message_prompt_get_user({ has_structured_json, inputs, max_length, }) {
66
67
  const { diffstat, diff } = inputs;
67
68
  const truncate = diffstat.length + diff.length > max_length;
69
+ if (truncate) {
70
+ tell_warning("The Diff is too long to fit in the user prompt - it is being truncated");
71
+ }
68
72
  const diff_truncated = truncate ? diff.slice(0, max_length - diffstat.length) + LF : diff;
69
73
  let user_prompt = EMPTY;
70
74
  user_prompt += "<diffstat>" + LF + diffstat + "</diffstat>" + LF_LF;
@@ -26,16 +26,21 @@ export async function git_simple_staging_get_staged_diffstat(git) {
26
26
  export async function git_simple_staging_get_staged_diff(git) {
27
27
  return await git.diff(["--cached"]);
28
28
  }
29
- export async function git_simple_staging_create_commit(git, git_message) {
30
- await git.commit(git_message);
29
+ export async function git_simple_staging_create_commit({ git, git_message, no_verify = false, }) {
30
+ const options = {};
31
+ if (no_verify) {
32
+ options["--no-verify"] = null;
33
+ }
34
+ await git.commit(git_message, options);
31
35
  }
32
36
  export async function git_simple_staging_push_to_remote({ git, no_verify = false, force = false, }) {
33
- const push_args = ["--follow-tags"];
37
+ const options = {};
38
+ options["--follow-tags"] = null;
34
39
  if (no_verify) {
35
- push_args.push("--no-verify");
40
+ options["--no-verify"] = null;
36
41
  }
37
42
  if (force) {
38
- push_args.push("--force");
43
+ options["--force"] = null;
39
44
  }
40
- await git.push(push_args);
45
+ await git.push(options);
41
46
  }
@@ -10,7 +10,7 @@ import { tui_block_string } from "./lib_tui_block.js";
10
10
  function llm_chat_get_parameters() {
11
11
  return {
12
12
  max_output_tokens: parse_int_or_undefined(env_get_empty("lib_llm_chat_max_output_tokens")),
13
- timeout: parse_int(env_get_substitute("lib_llm_chat_timeout", "60")),
13
+ timeout: parse_int(env_get_substitute("lib_llm_chat_timeout", "90")),
14
14
  };
15
15
  }
16
16
  function llm_chat_debug_prompts({ llm_model_name, system_prompt, user_prompt, }) {
@@ -22,7 +22,7 @@ function llm_chat_debug_prompts({ llm_model_name, system_prompt, user_prompt, })
22
22
  tui_block_string({ teller, title: `LLM user prompt (for ${llm_model_name}):`, content: user_prompt });
23
23
  }
24
24
  }
25
- export async function llm_chat_generate_text({ llm_config, system_prompt, user_prompt, tools, max_steps, min_steps, }) {
25
+ export async function llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens, tools, max_steps, min_steps, }) {
26
26
  const { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key } = llm_config;
27
27
  llm_chat_debug_prompts({ system_prompt, user_prompt, llm_model_name });
28
28
  const ai_sdk_language_model = llm_api_get_ai_sdk_language_model({
@@ -30,9 +30,14 @@ export async function llm_chat_generate_text({ llm_config, system_prompt, user_p
30
30
  llm_api_code,
31
31
  llm_api_key,
32
32
  });
33
- const { recommended_temperature, provider_options } = llm_model_detail;
33
+ const { recommended_temperature, provider_options, max_output_tokens: max_output_tokens_model } = llm_model_detail;
34
34
  const temperature = recommended_temperature;
35
- const { max_output_tokens, timeout } = llm_chat_get_parameters();
35
+ const { timeout, max_output_tokens: max_output_tokens_env } = llm_chat_get_parameters();
36
+ if (max_output_tokens_env !== undefined) {
37
+ max_output_tokens = max_output_tokens_env;
38
+ }
39
+ max_output_tokens =
40
+ max_output_tokens === undefined ? max_output_tokens_model : Math.min(max_output_tokens, max_output_tokens_model);
36
41
  const llm_inputs = {
37
42
  model: ai_sdk_language_model,
38
43
  system: system_prompt,
@@ -57,11 +62,11 @@ export async function llm_chat_generate_text({ llm_config, system_prompt, user_p
57
62
  const { text: generated_text, reasoningText: reasoning_text, totalUsage: total_usage, providerMetadata: provider_metadata, } = llm_outputs;
58
63
  return { generated_text, reasoning_text, total_usage, provider_metadata };
59
64
  }
60
- export async function llm_chat_generate_text_result({ llm_config, system_prompt, user_prompt, }) {
65
+ export async function llm_chat_generate_text_result({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
61
66
  const duration = new Duration();
62
67
  duration.start();
63
68
  try {
64
- const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt });
69
+ const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens });
65
70
  duration.stop();
66
71
  const seconds = duration.seconds_rounded();
67
72
  return { llm_config, seconds, error_text: null, outputs };
@@ -73,7 +78,7 @@ export async function llm_chat_generate_text_result({ llm_config, system_prompt,
73
78
  return { llm_config, seconds, error_text, outputs: null };
74
79
  }
75
80
  }
76
- export async function llm_chat_generate_object({ llm_config, user_prompt, system_prompt, schema, }) {
81
+ export async function llm_chat_generate_object({ llm_config, user_prompt, system_prompt, max_output_tokens, schema, }) {
77
82
  const { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key } = llm_config;
78
83
  llm_chat_debug_prompts({ system_prompt, user_prompt, llm_model_name });
79
84
  const ai_sdk_language_model = llm_api_get_ai_sdk_language_model({
@@ -83,14 +88,14 @@ export async function llm_chat_generate_object({ llm_config, user_prompt, system
83
88
  });
84
89
  const { recommended_temperature, provider_options } = llm_model_detail;
85
90
  const temperature = recommended_temperature;
86
- const { max_output_tokens, timeout } = llm_chat_get_parameters();
91
+ const { timeout, max_output_tokens: max_output_tokens_env } = llm_chat_get_parameters();
87
92
  const llm_inputs = {
88
93
  model: ai_sdk_language_model,
89
94
  system: system_prompt,
90
95
  prompt: user_prompt,
91
96
  output: "object",
92
97
  schema,
93
- maxOutputTokens: max_output_tokens,
98
+ maxOutputTokens: max_output_tokens_env ?? max_output_tokens,
94
99
  temperature,
95
100
  providerOptions: provider_options,
96
101
  abortSignal: AbortSignal.timeout(timeout * 1000),
@@ -1,10 +1,17 @@
1
+ import { env_get } from "./lib_env.js";
1
2
  import { llm_access_available, llm_access_get } from "./lib_llm_access.js";
2
3
  import { llm_model_find_detail, llm_model_get_choices } from "./lib_llm_model.js";
4
+ import { parse_int_or_undefined } from "./lib_parse_number.js";
3
5
  export function llm_config_get({ llm_model_details, llm_model_name, }) {
4
6
  const llm_model_detail = llm_model_find_detail({ llm_model_details, llm_model_name });
5
7
  const access = llm_access_get({ llm_model_details, llm_model_name });
6
8
  const { llm_model_code, llm_api_code, llm_api_key } = access;
7
- return { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key };
9
+ let effective_context_window = llm_model_detail.context_window;
10
+ const env_context_window = parse_int_or_undefined(env_get("LLM_CONFIG_CONTEXT_WINDOW"));
11
+ if (env_context_window !== undefined) {
12
+ effective_context_window = Math.min(effective_context_window, env_context_window);
13
+ }
14
+ return { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key, effective_context_window };
8
15
  }
9
16
  export function llm_config_get_all({ llm_model_details, llm_include, llm_excludes, }) {
10
17
  const choices = llm_model_get_choices({ llm_model_details });
@@ -5,7 +5,7 @@ import { TuiTable } from "./lib_tui_table.js";
5
5
  export function llm_list_models({ llm_model_details }) {
6
6
  const headings = ["NAME", "API", "CONTEXT", "INPUT", "OUTPUT", "REASONING"];
7
7
  const alignments = ["left", "left", "right", "right", "right", "left"];
8
- const table = new TuiTable({ headings, alignments });
8
+ const table = new TuiTable({ headings, alignments, compact: true });
9
9
  for (const detail of llm_model_details) {
10
10
  const { llm_model_name, llm_api_code, context_window, cents_input, cents_output, default_reasoning } = detail;
11
11
  const tui_name = llm_model_name;