git-coco 0.11.1 → 0.12.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/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var prompts = require('@langchain/core/prompts');
4
+ var prompts$1 = require('@langchain/core/prompts');
5
5
  var example_selectors = require('@langchain/core/example_selectors');
6
6
  var yargs = require('yargs');
7
- var fs = require('fs');
8
- var prompts$1 = require('@inquirer/prompts');
9
7
  var chalk = require('chalk');
8
+ var fs = require('fs');
9
+ var prompts = require('@inquirer/prompts');
10
10
  var ini = require('ini');
11
11
  var os = require('os');
12
12
  var path = require('path');
@@ -14,9 +14,10 @@ var Ajv = require('ajv');
14
14
  var ora = require('ora');
15
15
  var now = require('performance-now');
16
16
  var prettyMilliseconds = require('pretty-ms');
17
- var output_parsers = require('@langchain/core/output_parsers');
18
17
  var ollama = require('@langchain/ollama');
19
18
  var openai = require('@langchain/openai');
19
+ var output_parsers = require('@langchain/core/output_parsers');
20
+ var simpleGit = require('simple-git');
20
21
  var pQueue = require('p-queue');
21
22
  var documents = require('@langchain/core/documents');
22
23
  var outputs = require('@langchain/core/outputs');
@@ -32,7 +33,6 @@ require('@langchain/core/utils/env');
32
33
  require('@langchain/core/utils/json_patch');
33
34
  var diff = require('diff');
34
35
  var minimatch = require('minimatch');
35
- var simpleGit = require('simple-git');
36
36
  var tiktoken = require('tiktoken');
37
37
  var child_process = require('child_process');
38
38
 
@@ -58,6 +58,27 @@ var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
58
58
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
59
59
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
60
60
 
61
+ const isInteractive = (config) => {
62
+ return config?.mode === 'interactive' || !!config?.interactive;
63
+ };
64
+ const SEPERATOR = chalk.blue('─────────────');
65
+ const LOGO = chalk.green(`┌──────┐
66
+ │┏┏┓┏┏┓│
67
+ │┗┗┛┗┗┛│
68
+ └──────┘`);
69
+ chalk.green(`┌────┐
70
+ │coco│
71
+ └────┘`);
72
+ const USAGE_BANNER = chalk.green(`${LOGO}
73
+ ${chalk.bgGreen(`\xa0v${process.env.npm_package_version}\xa0`)}
74
+ `);
75
+ const getCommandUsageHeader = (command) => {
76
+ return chalk.green(`${USAGE_BANNER}\n${chalk.white('Command:')}\n\xa0\xa0\xa0\xa0\xa0 $0 ${chalk.greenBright(command)} [options]`);
77
+ };
78
+ const CONFIG_ALREADY_EXISTS = (path) => {
79
+ return `coco config found in '${path}', do you want to override it?`;
80
+ };
81
+
61
82
  /**
62
83
  * Returns a new object with all undefined keys removed
63
84
  *
@@ -68,16 +89,64 @@ function removeUndefined(obj) {
68
89
  return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
69
90
  }
70
91
 
71
- const template$3 = `GOAL: Use functional abstractions to summarize the following text
92
+ async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
93
+ const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
94
+ const newLines = [];
95
+ let foundSection = false;
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (lines[i].trim() === startComment) {
98
+ foundSection = true;
99
+ if (confirmUpdate) {
100
+ const confirmOverwrite = await prompts.confirm({
101
+ message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
102
+ default: false,
103
+ });
104
+ if (!confirmOverwrite) {
105
+ // keep all lines until the end comment
106
+ while (i < lines.length && lines[i].trim() !== endComment) {
107
+ newLines.push(lines[i]);
108
+ i++;
109
+ }
110
+ newLines.push(endComment);
111
+ continue;
112
+ }
113
+ }
114
+ newLines.push(startComment);
115
+ // Insert the new content
116
+ const newContent = await getNewContent();
117
+ newLines.push(newContent);
118
+ // Skip the existing content of the section
119
+ while (i < lines.length && lines[i].trim() !== endComment) {
120
+ i++;
121
+ }
122
+ newLines.push(endComment);
123
+ continue;
124
+ }
125
+ if (!foundSection || lines[i].trim() !== endComment) {
126
+ newLines.push(lines[i]);
127
+ }
128
+ }
129
+ // If section wasn't found, append it at the end
130
+ if (!foundSection) {
131
+ newLines.push('\n' + startComment);
132
+ const newContent = await getNewContent();
133
+ newLines.push(newContent);
134
+ newLines.push(endComment);
135
+ }
136
+ // Write the updated contents back to the file
137
+ fs.writeFileSync(filePath, newLines.join('\n'));
138
+ }
139
+
140
+ const template$4 = `GOAL: Use functional abstractions to summarize the following text
72
141
 
73
142
  RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
74
143
 
75
144
  TEXT:"""{text}"""
76
145
  `;
77
- const inputVariables$2 = ['text'];
78
- const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
79
- inputVariables: inputVariables$2,
80
- template: template$3,
146
+ const inputVariables$3 = ['text'];
147
+ const SUMMARIZE_PROMPT = new prompts$1.PromptTemplate({
148
+ inputVariables: inputVariables$3,
149
+ template: template$4,
81
150
  });
82
151
 
83
152
  /**
@@ -203,68 +272,6 @@ const CONFIG_KEYS = Object.keys({
203
272
  prompt: '',
204
273
  });
205
274
 
206
- async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
207
- const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
208
- const newLines = [];
209
- let foundSection = false;
210
- for (let i = 0; i < lines.length; i++) {
211
- if (lines[i].trim() === startComment) {
212
- foundSection = true;
213
- if (confirmUpdate) {
214
- const confirmOverwrite = await prompts$1.confirm({
215
- message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
216
- default: false,
217
- });
218
- if (!confirmOverwrite) {
219
- // keep all lines until the end comment
220
- while (i < lines.length && lines[i].trim() !== endComment) {
221
- newLines.push(lines[i]);
222
- i++;
223
- }
224
- newLines.push(endComment);
225
- continue;
226
- }
227
- }
228
- newLines.push(startComment);
229
- // Insert the new content
230
- const newContent = await getNewContent();
231
- newLines.push(newContent);
232
- // Skip the existing content of the section
233
- while (i < lines.length && lines[i].trim() !== endComment) {
234
- i++;
235
- }
236
- newLines.push(endComment);
237
- continue;
238
- }
239
- if (!foundSection || lines[i].trim() !== endComment) {
240
- newLines.push(lines[i]);
241
- }
242
- }
243
- // If section wasn't found, append it at the end
244
- if (!foundSection) {
245
- newLines.push('\n' + startComment);
246
- const newContent = await getNewContent();
247
- newLines.push(newContent);
248
- newLines.push(endComment);
249
- }
250
- // Write the updated contents back to the file
251
- fs.writeFileSync(filePath, newLines.join('\n'));
252
- }
253
-
254
- const isInteractive = (argv) => {
255
- return argv?.mode === 'interactive' || argv.interactive;
256
- };
257
- const SEPERATOR = chalk.blue('─────────────');
258
- const LOGO = chalk.green(`┌────────────┐
259
- │┌─┐┌─┐┌─┐┌─┐│
260
- ││ │ ││ │ ││
261
- │└─┘└─┘└─┘└─┘│
262
- └────────────┘
263
- `);
264
- const CONFIG_ALREADY_EXISTS = (path) => {
265
- return `coco config found in '${path}', do you want to override it?`;
266
- };
267
-
268
275
  /**
269
276
  * Load environment variables
270
277
  *
@@ -288,6 +295,9 @@ function loadEnvConfig(config) {
288
295
  handleServiceEnvVar(envConfig.service, key, envValue);
289
296
  }
290
297
  else {
298
+ if (key === 'service' || !envValue) {
299
+ return;
300
+ }
291
301
  envConfig[key] = envValue;
292
302
  }
293
303
  });
@@ -396,7 +406,6 @@ function loadGitConfig(config) {
396
406
  config = {
397
407
  ...config,
398
408
  service: service,
399
- temperature: gitConfigParsed.coco?.temperature || config.temperature,
400
409
  prompt: gitConfigParsed.coco?.prompt || config.prompt,
401
410
  mode: gitConfigParsed.coco?.mode || config.mode,
402
411
  summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
@@ -1936,26 +1945,6 @@ function commandExecutor(handler) {
1936
1945
  };
1937
1946
  }
1938
1947
 
1939
- const executeChain = async ({ llm, prompt, variables, parser, }) => {
1940
- if (!llm || !prompt || !variables) {
1941
- throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
1942
- }
1943
- const chain = prompt.pipe(llm).pipe(parser);
1944
- let res;
1945
- try {
1946
- res = await chain.invoke(variables);
1947
- }
1948
- catch (error) {
1949
- if (error instanceof Error) {
1950
- throw new Error(`LLMChain call error: ${error.message}`);
1951
- }
1952
- }
1953
- if (!res) {
1954
- throw new Error('Empty response from LLMChain call');
1955
- }
1956
- return res;
1957
- };
1958
-
1959
1948
  /**
1960
1949
  * Get LLM Model Based on Configuration
1961
1950
  *
@@ -1989,87 +1978,610 @@ function getPrompt({ template, variables, fallback }) {
1989
1978
  if (!template && !fallback)
1990
1979
  throw new Error('Must provide either a template or a fallback');
1991
1980
  return (template
1992
- ? new prompts.PromptTemplate({
1981
+ ? new prompts$1.PromptTemplate({
1993
1982
  template,
1994
1983
  inputVariables: variables,
1995
1984
  })
1996
1985
  : fallback);
1997
1986
  }
1998
1987
 
1999
- /**
2000
- * Extract the path from a file path string.
2001
- * @param {string} filePath - The full file path.
2002
- * @returns {string} The path portion of the file path.
2003
- */
2004
- function getPathFromFilePath(filePath) {
2005
- return filePath.split('/').slice(0, -1).join('/');
2006
- }
1988
+ const executeChain = async ({ llm, prompt, variables, parser }) => {
1989
+ if (!llm || !prompt || !variables) {
1990
+ throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
1991
+ }
1992
+ const chain = prompt.pipe(llm).pipe(parser);
1993
+ let res;
1994
+ try {
1995
+ res = await chain.invoke(variables);
1996
+ }
1997
+ catch (error) {
1998
+ if (error instanceof Error) {
1999
+ throw new Error(`LLMChain call error: ${error.message}`);
2000
+ }
2001
+ }
2002
+ if (!res) {
2003
+ throw new Error('Empty response from LLMChain call');
2004
+ }
2005
+ return res;
2006
+ };
2007
2007
 
2008
- async function summarize(documents$1, { chain, textSplitter, options }) {
2009
- const { returnIntermediateSteps = false } = options || {};
2010
- const docs = await textSplitter.splitDocuments(documents$1.map((doc) => new documents.Document(doc)));
2011
- const res = await chain.invoke({
2012
- input_documents: docs,
2013
- returnIntermediateSteps,
2014
- });
2015
- if (res.error)
2016
- throw new Error(res.error);
2017
- return res.text && res.text.trim();
2008
+ function extractTicketIdFromBranchName(branchName) {
2009
+ const regex = /((?<!([A-Z]+)-?)[A-Z]+-\d+)/;
2010
+ const match = branchName.match(regex);
2011
+ return match ? match[0] : null;
2018
2012
  }
2019
2013
 
2020
2014
  /**
2021
- * Create groups from a given node info.
2022
- * @param {DiffNode} node - The node info to start grouping.
2023
- * @returns {DirectoryDiff[]} The groups created.
2015
+ * Retrieves the commit log range between two specified commits.
2016
+ *
2017
+ * @param from - The starting commit.
2018
+ * @param to - The ending commit.
2019
+ * @param options - Additional options for retrieving the commit log range.
2020
+ * @returns A promise that resolves to an array of commit log messages.
2021
+ * @throws If there is an error retrieving the commit log range.
2024
2022
  */
2025
- function createDirectoryDiffs(node) {
2026
- const groupByPath = {};
2027
- function traverse(node) {
2028
- node.diffs.forEach((diff) => {
2029
- const path = getPathFromFilePath(diff.file);
2030
- if (!groupByPath[path]) {
2031
- groupByPath[path] = { diffs: [], path, tokenCount: 0 };
2032
- }
2033
- groupByPath[path].diffs.push(diff);
2034
- groupByPath[path].tokenCount += diff.tokenCount;
2035
- });
2036
- node.children.forEach(traverse);
2023
+ async function getCommitLogRange(from, to, { noMerges, git }) {
2024
+ try {
2025
+ const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
2026
+ const commitLog = await git.log(logOptions);
2027
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
2028
+ }
2029
+ catch (error) {
2030
+ // If there's an error, handle it appropriately
2031
+ throw error;
2037
2032
  }
2038
- traverse(node);
2039
- return Object.values(groupByPath);
2040
2033
  }
2034
+
2041
2035
  /**
2042
- * Summarize a directory diff asynchronously.
2036
+ * Retrieves the name of the current branch.
2037
+ *
2038
+ * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
2039
+ * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
2043
2040
  */
2044
- async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
2041
+ async function getCurrentBranchName({ git }) {
2042
+ return await git.revparse(['--abbrev-ref', 'HEAD']);
2043
+ }
2044
+
2045
+ /**
2046
+ * Retrieves the commit log between the current branch and a specified target branch.
2047
+ *
2048
+ * @param {Object} options - The options for retrieving the commit log.
2049
+ * @param {SimpleGit} options.git - The SimpleGit instance.
2050
+ * @param {Logger} options.logger - The logger for logging messages.
2051
+ * @param {string} options.targetBranch - The target branch to compare against.
2052
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
2053
+ */
2054
+ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
2045
2055
  try {
2046
- const directorySummary = await summarize(directory.diffs.map((diff) => ({
2047
- pageContent: diff.diff,
2048
- metadata: {
2049
- file: diff.file,
2050
- summary: diff.summary,
2051
- },
2052
- })), {
2053
- chain,
2054
- textSplitter,
2055
- options: {
2056
- returnIntermediateSteps: true,
2057
- },
2058
- });
2059
- const newTokenTotal = tokenizer(directorySummary);
2060
- return {
2061
- diffs: directory.diffs,
2062
- path: directory.path,
2063
- summary: directorySummary,
2064
- tokenCount: newTokenTotal,
2065
- };
2056
+ // Get the current branch name
2057
+ const currentBranch = await getCurrentBranchName({ git });
2058
+ // Get the list of commits that are unique to the current branch compared to the target branch
2059
+ const uniqueCommits = (await git.raw(['rev-list', `${targetBranch}..${currentBranch}`]))
2060
+ .split('\n')
2061
+ .filter(Boolean)
2062
+ .reverse();
2063
+ logger?.verbose(`Found ${uniqueCommits.length} unique commits between "${currentBranch}" and "${targetBranch}"`, { color: 'blue' });
2064
+ const firstCommit = uniqueCommits[0];
2065
+ const lastCommit = uniqueCommits[uniqueCommits.length - 1];
2066
+ if (!firstCommit || !lastCommit) {
2067
+ logger?.log('Unable to determine first and last commit between branches', { color: 'yellow' });
2068
+ return [];
2069
+ }
2070
+ // Retrieve commit log with messages
2071
+ return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
2066
2072
  }
2067
2073
  catch (error) {
2068
- console.error(error);
2069
- return directory;
2074
+ logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
2070
2075
  }
2076
+ return [];
2071
2077
  }
2072
- const defaultOutputCallback = (group) => {
2078
+
2079
+ /**
2080
+ * Retrieves the commit log for the current branch.
2081
+ *
2082
+ * @param {Object} options - The options for retrieving the commit log.
2083
+ * @param {SimpleGit} options.git - The SimpleGit instance.
2084
+ * @param {Logger} options.logger - The logger for logging messages.
2085
+ * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
2086
+ * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
2087
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
2088
+ */
2089
+ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
2090
+ try {
2091
+ // Get the current branch name
2092
+ const branch = await getCurrentBranchName({ git });
2093
+ // Check if the current branch has any commits
2094
+ const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
2095
+ if (!hasCommits) {
2096
+ logger?.log('No commits on the current branch.');
2097
+ return [];
2098
+ }
2099
+ // Get the list of commits that are unique to the current branch
2100
+ let uniqueCommits;
2101
+ if (comparisonBranch === branch) {
2102
+ // If the comparison branch is the same as the current branch, we compare against the remote.
2103
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
2104
+ .split('\n')
2105
+ .filter(Boolean)
2106
+ .reverse();
2107
+ }
2108
+ else {
2109
+ // Your existing code for different branches
2110
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
2111
+ .split('\n')
2112
+ .filter(Boolean)
2113
+ .reverse();
2114
+ }
2115
+ logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
2116
+ const firstCommit = uniqueCommits[0];
2117
+ const lastCommit = uniqueCommits[uniqueCommits.length - 1];
2118
+ if (!firstCommit || !lastCommit) {
2119
+ logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
2120
+ return [];
2121
+ }
2122
+ // Retrieve commit log with messages
2123
+ return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
2124
+ }
2125
+ catch (error) {
2126
+ logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
2127
+ }
2128
+ return [];
2129
+ }
2130
+
2131
+ /**
2132
+ * Retrieves the SimpleGit instance for the repository.
2133
+ * @returns {SimpleGit} The SimpleGit instance.
2134
+ */
2135
+ const getRepo = () => {
2136
+ let git;
2137
+ try {
2138
+ git = simpleGit.simpleGit();
2139
+ }
2140
+ catch (e) {
2141
+ console.log('Error initializing git repo', e);
2142
+ process.exit(1);
2143
+ }
2144
+ return git;
2145
+ };
2146
+
2147
+ const template$3 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2148
+ Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
2149
+
2150
+ Please follow the guidelines below when writing your commit message:
2151
+
2152
+ - Write concisely using an informal tone
2153
+ - Avoid phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
2154
+ - Avoid referencing specific files names or long paths in the commit message
2155
+ - DO NOT include any diffs or file changes in the commit message
2156
+ - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
2157
+
2158
+ {format_instructions}
2159
+
2160
+ """"""
2161
+ {summary}
2162
+ """"""
2163
+
2164
+ {additional}
2165
+ `;
2166
+ const inputVariables$2 = ['summary', 'format_instructions', 'additional'];
2167
+ const COMMIT_PROMPT = new prompts$1.PromptTemplate({
2168
+ template: template$3,
2169
+ inputVariables: inputVariables$2,
2170
+ });
2171
+
2172
+ /**
2173
+ * Verify template string contains all required input variables
2174
+ *
2175
+ * @param text template string
2176
+ * @param inputVariables template variables
2177
+ * @returns boolean or error message
2178
+ */
2179
+ function validatePromptTemplate(text, inputVariables) {
2180
+ if (!text) {
2181
+ return 'Prompt template cannot be empty';
2182
+ }
2183
+ if (!inputVariables.some((entry) => text.includes(entry))) {
2184
+ return ('Prompt template must include at least one of the following input variables: ' +
2185
+ inputVariables.map((value) => `{${value}}`).join(', '));
2186
+ }
2187
+ return true;
2188
+ }
2189
+
2190
+ async function editPrompt(options) {
2191
+ return await prompts.editor({
2192
+ message: 'Edit the prompt',
2193
+ default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
2194
+ waitForUseInput: false,
2195
+ postfix: 'Press ENTER to continue',
2196
+ validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
2197
+ });
2198
+ }
2199
+
2200
+ async function editResult(result, options) {
2201
+ if (options.openInEditor) {
2202
+ return await prompts.editor({
2203
+ message: 'Edit the commit message',
2204
+ default: result,
2205
+ waitForUseInput: false,
2206
+ validate: (text) => (text ? true : 'Commit message cannot be empty'),
2207
+ });
2208
+ }
2209
+ return result;
2210
+ }
2211
+
2212
+ async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
2213
+ const choices = [
2214
+ {
2215
+ name: '✨ Looks good!',
2216
+ value: 'approve',
2217
+ description: descriptions?.approve || `Continue with the generated ${label}`,
2218
+ },
2219
+ {
2220
+ name: '📝 Edit',
2221
+ value: 'edit',
2222
+ description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
2223
+ },
2224
+ ];
2225
+ if (enableModifyPrompt) {
2226
+ choices.push({
2227
+ name: '🪶 Modify Prompt',
2228
+ value: 'modifyPrompt',
2229
+ description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
2230
+ });
2231
+ }
2232
+ if (enableRetry) {
2233
+ choices.push({
2234
+ name: '🔄 Retry',
2235
+ value: 'retryMessageOnly',
2236
+ description: descriptions?.retryMessageOnly ||
2237
+ `Restart the function execution from generating the ${label}`,
2238
+ });
2239
+ }
2240
+ if (enableFullRetry) {
2241
+ choices.push({
2242
+ name: '🔄 Retry Full',
2243
+ value: 'retryFull',
2244
+ description: descriptions?.retryFull ||
2245
+ `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
2246
+ });
2247
+ }
2248
+ choices.push({
2249
+ name: '💣 Cancel',
2250
+ value: 'cancel',
2251
+ description: descriptions?.cancel || `Cancel the ${label}`,
2252
+ });
2253
+ return (await prompts.select({
2254
+ message: `Would you like to make any changes to the ${label}?`,
2255
+ choices,
2256
+ }));
2257
+ }
2258
+
2259
+ function logResult(label, result) {
2260
+ console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
2261
+ }
2262
+
2263
+ async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
2264
+ const { logger } = options;
2265
+ let continueLoop = true;
2266
+ let modifyPrompt = false;
2267
+ let context = '';
2268
+ let result = '';
2269
+ const changes = await factory();
2270
+ // if we don't have any changes, bail.
2271
+ if (!changes || !Object.keys(changes).length) {
2272
+ await noResult(options);
2273
+ }
2274
+ while (continueLoop) {
2275
+ if (!context.length) {
2276
+ context = await parser(changes, result, options);
2277
+ }
2278
+ // if we still don't have a context, bail.
2279
+ if (!context.length) {
2280
+ await noResult(options);
2281
+ }
2282
+ if (modifyPrompt) {
2283
+ options.prompt = await editPrompt(options);
2284
+ }
2285
+ logger.startTimer().startSpinner(`Generating ${label}\n`, {
2286
+ color: 'blue',
2287
+ });
2288
+ result = await agent(context, options);
2289
+ if (!result) {
2290
+ logger.stopSpinner('💀 Agent failed to return content.', {
2291
+ mode: 'fail',
2292
+ color: 'red',
2293
+ });
2294
+ process.exit(0);
2295
+ }
2296
+ logger
2297
+ .stopSpinner(`Generated ${label}`, {
2298
+ color: 'green',
2299
+ mode: 'succeed',
2300
+ })
2301
+ .stopTimer();
2302
+ if (options?.interactive) {
2303
+ logResult(label, result);
2304
+ const reviewAnswer = await getUserReviewDecision({
2305
+ label,
2306
+ ...(options?.review || {}),
2307
+ });
2308
+ if (reviewAnswer === 'cancel') {
2309
+ process.exit(0);
2310
+ }
2311
+ if (reviewAnswer === 'edit') {
2312
+ options.openInEditor = true;
2313
+ }
2314
+ if (reviewAnswer === 'retryFull') {
2315
+ context = '';
2316
+ result = '';
2317
+ options.prompt = '';
2318
+ continue;
2319
+ }
2320
+ if (reviewAnswer === 'retryMessageOnly') {
2321
+ modifyPrompt = false;
2322
+ result = '';
2323
+ continue;
2324
+ }
2325
+ if (reviewAnswer === 'modifyPrompt') {
2326
+ modifyPrompt = true;
2327
+ result = '';
2328
+ continue;
2329
+ }
2330
+ }
2331
+ // if we're here, we're done.
2332
+ result = await editResult(result, options);
2333
+ continueLoop = false;
2334
+ }
2335
+ return result;
2336
+ }
2337
+
2338
+ const logSuccess = () => {
2339
+ console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
2340
+ };
2341
+
2342
+ async function handleResult({ result, mode, interactiveHandler }) {
2343
+ switch (mode) {
2344
+ case 'interactive':
2345
+ if (interactiveHandler) {
2346
+ await interactiveHandler(result);
2347
+ }
2348
+ else {
2349
+ console.warn('No result handler provided for interactive mode.');
2350
+ logSuccess();
2351
+ }
2352
+ break;
2353
+ case 'stdout':
2354
+ default:
2355
+ process.stdout.write(result, 'utf8');
2356
+ break;
2357
+ }
2358
+ process.exit(0);
2359
+ }
2360
+
2361
+ const template$2 = `Write informative git changelog, in the imperative, based on a series of individual messages.
2362
+
2363
+ - Include the git commit hash as reference for each change, including just the first 7 characters
2364
+ - Logically group changes, and if necessary, summarize dependency updates
2365
+
2366
+ {format_instructions}
2367
+
2368
+ """{summary}"""`;
2369
+ const inputVariables$1 = ['format_instructions', 'summary'];
2370
+ const CHANGELOG_PROMPT = new prompts$1.PromptTemplate({
2371
+ template: template$2,
2372
+ inputVariables: inputVariables$1,
2373
+ });
2374
+
2375
+ const handler$3 = async (argv, logger) => {
2376
+ const config = loadConfig(argv);
2377
+ const git = getRepo();
2378
+ const key = getApiKeyForModel(config);
2379
+ const { provider, model } = getModelAndProviderFromConfig(config);
2380
+ if (config.service.authentication.type !== 'None' && !key) {
2381
+ logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
2382
+ process.exit(1);
2383
+ }
2384
+ const llm = getLlm(provider, model, config);
2385
+ const INTERACTIVE = isInteractive(config);
2386
+ if (INTERACTIVE) {
2387
+ logger.log(LOGO);
2388
+ }
2389
+ async function factory() {
2390
+ const branchName = await getCurrentBranchName({ git });
2391
+ if (config.range && config.range.includes(':')) {
2392
+ const [from, to] = config.range.split(':');
2393
+ if (!from || !to) {
2394
+ logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
2395
+ process.exit(1);
2396
+ }
2397
+ return {
2398
+ branch: branchName,
2399
+ commits: await getCommitLogRange(from, to, { git, noMerges: true }),
2400
+ };
2401
+ }
2402
+ if (argv.branch) {
2403
+ logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
2404
+ return {
2405
+ branch: branchName,
2406
+ commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
2407
+ };
2408
+ }
2409
+ logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
2410
+ return {
2411
+ branch: branchName,
2412
+ commits: await getCommitLogCurrentBranch({ git, logger }),
2413
+ };
2414
+ }
2415
+ async function parser({ branch, commits }) {
2416
+ let result;
2417
+ if (!commits || commits.length === 0) {
2418
+ result = `## ${branch}\n\nNo commits found.`;
2419
+ }
2420
+ else {
2421
+ result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
2422
+ }
2423
+ return result;
2424
+ }
2425
+ const changelogMsg = await generateAndReviewLoop({
2426
+ label: 'changelog',
2427
+ factory,
2428
+ parser,
2429
+ agent: async (context, options) => {
2430
+ const parser = new output_parsers.JsonOutputParser();
2431
+ const prompt = getPrompt({
2432
+ template: options.prompt,
2433
+ variables: CHANGELOG_PROMPT.inputVariables,
2434
+ fallback: CHANGELOG_PROMPT,
2435
+ });
2436
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content', both strings.";
2437
+ const changelog = await executeChain({
2438
+ llm,
2439
+ prompt,
2440
+ variables: {
2441
+ summary: context,
2442
+ format_instructions: formatInstructions,
2443
+ },
2444
+ parser,
2445
+ });
2446
+ const branchName = await getCurrentBranchName({ git });
2447
+ const ticketId = extractTicketIdFromBranchName(branchName);
2448
+ const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
2449
+ return `${changelog.header}\n\n${changelog.content}${footer}`;
2450
+ },
2451
+ noResult: async () => {
2452
+ if (config.range) {
2453
+ logger.log(`No commits found in the provided range.`, { color: 'red' });
2454
+ process.exit(0);
2455
+ }
2456
+ logger.log(`No commits found in the current branch.`, { color: 'red' });
2457
+ process.exit(0);
2458
+ },
2459
+ options: {
2460
+ ...config,
2461
+ prompt: config.prompt || CHANGELOG_PROMPT.template,
2462
+ logger,
2463
+ interactive: INTERACTIVE,
2464
+ review: {
2465
+ enableFullRetry: false,
2466
+ },
2467
+ },
2468
+ });
2469
+ const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
2470
+ handleResult({
2471
+ result: changelogMsg,
2472
+ interactiveHandler: async () => {
2473
+ logSuccess();
2474
+ },
2475
+ mode: MODE,
2476
+ });
2477
+ };
2478
+
2479
+ /**
2480
+ * Command line options via yargs
2481
+ */
2482
+ const options$3 = {
2483
+ range: {
2484
+ type: 'string',
2485
+ alias: 'r',
2486
+ description: 'Commit range e.g `HEAD~2:HEAD`',
2487
+ },
2488
+ branch: {
2489
+ type: 'string',
2490
+ alias: 'b',
2491
+ description: 'Target branch to compare against',
2492
+ },
2493
+ i: {
2494
+ type: 'boolean',
2495
+ alias: 'interactive',
2496
+ description: 'Toggle interactive mode',
2497
+ },
2498
+ };
2499
+ const builder$3 = (yargsInstance) => {
2500
+ return yargsInstance.options(options$3).usage(getCommandUsageHeader(changelog.command));
2501
+ };
2502
+
2503
+ var changelog = {
2504
+ command: 'changelog',
2505
+ desc: 'Generate a changelog from current or target branch or provided commit range.',
2506
+ builder: builder$3,
2507
+ handler: commandExecutor(handler$3),
2508
+ options: options$3,
2509
+ };
2510
+
2511
+ /**
2512
+ * Extract the path from a file path string.
2513
+ * @param {string} filePath - The full file path.
2514
+ * @returns {string} The path portion of the file path.
2515
+ */
2516
+ function getPathFromFilePath(filePath) {
2517
+ return filePath.split('/').slice(0, -1).join('/');
2518
+ }
2519
+
2520
+ async function summarize(documents$1, { chain, textSplitter, options }) {
2521
+ const { returnIntermediateSteps = false } = options || {};
2522
+ const docs = await textSplitter.splitDocuments(documents$1.map((doc) => new documents.Document(doc)));
2523
+ const res = await chain.invoke({
2524
+ input_documents: docs,
2525
+ returnIntermediateSteps,
2526
+ });
2527
+ if (res.error)
2528
+ throw new Error(res.error);
2529
+ return res.text && res.text.trim();
2530
+ }
2531
+
2532
+ /**
2533
+ * Create groups from a given node info.
2534
+ * @param {DiffNode} node - The node info to start grouping.
2535
+ * @returns {DirectoryDiff[]} The groups created.
2536
+ */
2537
+ function createDirectoryDiffs(node) {
2538
+ const groupByPath = {};
2539
+ function traverse(node) {
2540
+ node.diffs.forEach((diff) => {
2541
+ const path = getPathFromFilePath(diff.file);
2542
+ if (!groupByPath[path]) {
2543
+ groupByPath[path] = { diffs: [], path, tokenCount: 0 };
2544
+ }
2545
+ groupByPath[path].diffs.push(diff);
2546
+ groupByPath[path].tokenCount += diff.tokenCount;
2547
+ });
2548
+ node.children.forEach(traverse);
2549
+ }
2550
+ traverse(node);
2551
+ return Object.values(groupByPath);
2552
+ }
2553
+ /**
2554
+ * Summarize a directory diff asynchronously.
2555
+ */
2556
+ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
2557
+ try {
2558
+ const directorySummary = await summarize(directory.diffs.map((diff) => ({
2559
+ pageContent: diff.diff,
2560
+ metadata: {
2561
+ file: diff.file,
2562
+ summary: diff.summary,
2563
+ },
2564
+ })), {
2565
+ chain,
2566
+ textSplitter,
2567
+ options: {
2568
+ returnIntermediateSteps: true,
2569
+ },
2570
+ });
2571
+ const newTokenTotal = tokenizer(directorySummary);
2572
+ return {
2573
+ diffs: directory.diffs,
2574
+ path: directory.path,
2575
+ summary: directorySummary,
2576
+ tokenCount: newTokenTotal,
2577
+ };
2578
+ }
2579
+ catch (error) {
2580
+ console.error(error);
2581
+ return directory;
2582
+ }
2583
+ }
2584
+ const defaultOutputCallback = (group) => {
2073
2585
  let output = `
2074
2586
  -------\n* changes in "/${group.path}"\n\n`;
2075
2587
  if (group.summary) {
@@ -2645,7 +3157,7 @@ class LLMChain extends BaseChain {
2645
3157
  }
2646
3158
  return new LLMChain({
2647
3159
  llm: await base.BaseLanguageModel.deserialize(llm),
2648
- prompt: await prompts.BasePromptTemplate.deserialize(prompt),
3160
+ prompt: await prompts$1.BasePromptTemplate.deserialize(prompt),
2649
3161
  });
2650
3162
  }
2651
3163
  /** @deprecated */
@@ -2675,7 +3187,7 @@ You should build the API url in order to get a response that is as short as poss
2675
3187
 
2676
3188
  Question:{question}
2677
3189
  API url:`;
2678
- const API_URL_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts.PromptTemplate({
3190
+ const API_URL_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts$1.PromptTemplate({
2679
3191
  inputVariables: ["api_docs", "question"],
2680
3192
  template: API_URL_RAW_PROMPT_TEMPLATE,
2681
3193
  });
@@ -2688,7 +3200,7 @@ Here is the response from the API:
2688
3200
  Summarize this response to answer the original question.
2689
3201
 
2690
3202
  Summary:`;
2691
- const API_RESPONSE_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts.PromptTemplate({
3203
+ const API_RESPONSE_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts$1.PromptTemplate({
2692
3204
  inputVariables: ["api_docs", "question", "api_url", "api_response"],
2693
3205
  template: API_RESPONSE_RAW_PROMPT_TEMPLATE,
2694
3206
  });
@@ -3413,7 +3925,7 @@ class RefineDocumentsChain extends BaseChain {
3413
3925
  return "RefineDocumentsChain";
3414
3926
  }
3415
3927
  get defaultDocumentPrompt() {
3416
- return new prompts.PromptTemplate({
3928
+ return new prompts$1.PromptTemplate({
3417
3929
  inputVariables: ["page_content"],
3418
3930
  template: "{page_content}",
3419
3931
  });
@@ -3570,7 +4082,7 @@ var combine_docs_chain = /*#__PURE__*/Object.freeze({
3570
4082
  });
3571
4083
 
3572
4084
  /* eslint-disable spaced-comment */
3573
- const DEFAULT_QA_PROMPT = /*#__PURE__*/ new prompts.PromptTemplate({
4085
+ const DEFAULT_QA_PROMPT = /*#__PURE__*/ new prompts$1.PromptTemplate({
3574
4086
  template: "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n\n{context}\n\nQuestion: {question}\nHelpful Answer:",
3575
4087
  inputVariables: ["context", "question"],
3576
4088
  });
@@ -3579,10 +4091,10 @@ If you don't know the answer, just say that you don't know, don't try to make up
3579
4091
  ----------------
3580
4092
  {context}`;
3581
4093
  const messages = [
3582
- /*#__PURE__*/ prompts.SystemMessagePromptTemplate.fromTemplate(system_template),
3583
- /*#__PURE__*/ prompts.HumanMessagePromptTemplate.fromTemplate("{question}"),
4094
+ /*#__PURE__*/ prompts$1.SystemMessagePromptTemplate.fromTemplate(system_template),
4095
+ /*#__PURE__*/ prompts$1.HumanMessagePromptTemplate.fromTemplate("{question}"),
3584
4096
  ];
3585
- const CHAT_PROMPT = /*#__PURE__*/ prompts.ChatPromptTemplate.fromMessages(messages);
4097
+ const CHAT_PROMPT = /*#__PURE__*/ prompts$1.ChatPromptTemplate.fromMessages(messages);
3586
4098
  const QA_PROMPT_SELECTOR = /*#__PURE__*/ new example_selectors.ConditionalPromptSelector(DEFAULT_QA_PROMPT, [[example_selectors.isChatModel, CHAT_PROMPT]]);
3587
4099
 
3588
4100
  /**
@@ -4333,15 +4845,15 @@ var vector_db_qa = /*#__PURE__*/Object.freeze({
4333
4845
  });
4334
4846
 
4335
4847
  /* eslint-disable spaced-comment */
4336
- const template$2 = `Write a concise summary of the following:
4848
+ const template$1 = `Write a concise summary of the following:
4337
4849
 
4338
4850
 
4339
4851
  "{text}"
4340
4852
 
4341
4853
 
4342
4854
  CONCISE SUMMARY:`;
4343
- const DEFAULT_PROMPT = /*#__PURE__*/ new prompts.PromptTemplate({
4344
- template: template$2,
4855
+ const DEFAULT_PROMPT = /*#__PURE__*/ new prompts$1.PromptTemplate({
4856
+ template: template$1,
4345
4857
  inputVariables: ["text"],
4346
4858
  });
4347
4859
 
@@ -4357,7 +4869,7 @@ Given the new context, refine the original summary
4357
4869
  If the context isn't useful, return the original summary.
4358
4870
 
4359
4871
  REFINED SUMMARY:`;
4360
- const REFINE_PROMPT = /* #__PURE__ */ new prompts.PromptTemplate({
4872
+ const REFINE_PROMPT = /* #__PURE__ */ new prompts$1.PromptTemplate({
4361
4873
  template: refinePromptTemplate,
4362
4874
  inputVariables: ["existing_answer", "text"],
4363
4875
  });
@@ -5534,412 +6046,189 @@ async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
5534
6046
  */
5535
6047
  async function getDiff(nodeFile, commit, { git, logger, }) {
5536
6048
  if (nodeFile.status === 'deleted') {
5537
- return 'This file has been deleted.';
5538
- }
5539
- if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
5540
- const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
5541
- return renamedDiff;
5542
- }
5543
- // If not deleted or renamed, get the diff from the index
5544
- const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
5545
- return defaultDiff;
5546
- }
5547
-
5548
- // Max tokens for GPT-3 is 4096
5549
- // const MAX_TOKENS_PER_SUMMARY = 4096
5550
- const MAX_TOKENS_PER_SUMMARY = 12288;
5551
- async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
5552
- const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
5553
- // const textSplitter = new TokenTextSplitter({
5554
- // chunkSize: 10000,
5555
- // chunkOverlap: 250,
5556
- // });
5557
- const summarizationChain = getSummarizationChain(model, {
5558
- type: 'map_reduce',
5559
- combineMapPrompt: SUMMARIZE_PROMPT,
5560
- combinePrompt: SUMMARIZE_PROMPT,
5561
- });
5562
- logger.startTimer();
5563
- const rootTreeNode = createDiffTree(changes);
5564
- logger.stopTimer('Created file hierarchy');
5565
- // Collect diffs
5566
- logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
5567
- const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
5568
- logger.stopSpinner('Diffs Collected').stopTimer();
5569
- // Summarize diffs
5570
- logger.startTimer();
5571
- const summary = await summarizeDiffs(diffs, {
5572
- tokenizer,
5573
- maxTokens: MAX_TOKENS_PER_SUMMARY,
5574
- textSplitter,
5575
- chain: summarizationChain,
5576
- logger,
5577
- });
5578
- logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
5579
- return summary;
5580
- }
5581
-
5582
- /**
5583
- * Creates a commit with the specified commit message.
5584
- *
5585
- * @param message The commit message.
5586
- * @param git The SimpleGit instance.
5587
- * @returns A Promise that resolves to the CommitResult.
5588
- */
5589
- async function createCommit(message, git) {
5590
- return await git.commit(message);
5591
- }
5592
-
5593
- /**
5594
- * Determines the status of a file based on its changes in the Git repository.
5595
- *
5596
- * @param file - The file to check the status of.
5597
- * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
5598
- * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
5599
- * @throws Error if the file type is invalid.
5600
- */
5601
- function getStatus(file, location = 'index') {
5602
- if ('index' in file && 'working_dir' in file) {
5603
- const statusCode = file[location];
5604
- switch (statusCode) {
5605
- case 'A':
5606
- return 'added';
5607
- case 'D':
5608
- return 'deleted';
5609
- case 'M':
5610
- return 'modified';
5611
- case 'R':
5612
- return 'renamed';
5613
- case '?':
5614
- return 'untracked';
5615
- default:
5616
- return 'unknown';
5617
- }
5618
- }
5619
- else if ('changes' in file && 'binary' in file) {
5620
- if (file.changes === 0)
5621
- return 'untracked';
5622
- if (file.file.includes('=>'))
5623
- return 'renamed';
5624
- if (file.deletions === 0 && file.insertions > 0)
5625
- return 'added';
5626
- if (file.insertions === 0 && file.deletions > 0)
5627
- return 'deleted';
5628
- if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
5629
- return 'modified';
5630
- return 'unknown';
5631
- }
5632
- else {
5633
- throw new Error('Invalid file type');
5634
- }
5635
- }
5636
-
5637
- /**
5638
- * Returns the summary text for a file change.
5639
- *
5640
- * @param file - The file status or diff result.
5641
- * @param change - The partial file change object.
5642
- * @returns The summary text for the file change.
5643
- * @throws Error if the file type is invalid.
5644
- */
5645
- function getSummaryText(file, change) {
5646
- const status = change.status || getStatus(file);
5647
- let filePath;
5648
- if ('path' in file) {
5649
- filePath = file.path;
5650
- }
5651
- else if ('file' in file) {
5652
- filePath = change?.filePath || file.file;
5653
- }
5654
- else {
5655
- throw new Error('Invalid file type');
5656
- }
5657
- if (change.oldFilePath) {
5658
- return `${status}: ${change.oldFilePath} -> ${filePath}`;
5659
- }
5660
- return `${status}: ${filePath}`;
5661
- }
5662
-
5663
- /**
5664
- * Retrieves the changes in the Git repository.
5665
- *
5666
- * @param {GetChangesInput} options - The options for retrieving the changes.
5667
- * @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
5668
- */
5669
- async function getChanges({ git, options }) {
5670
- const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
5671
- const staged = [];
5672
- const unstaged = [];
5673
- const untracked = [];
5674
- const status = await git.status();
5675
- status.files.forEach((file) => {
5676
- const fileChange = {
5677
- filePath: file.path,
5678
- oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
5679
- };
5680
- // Unstaged files
5681
- if (file.working_dir !== '?' && file.working_dir !== ' ') {
5682
- fileChange.status = getStatus(file, 'working_dir');
5683
- fileChange.summary = getSummaryText(file, fileChange);
5684
- unstaged.push(fileChange);
5685
- }
5686
- // Staged files
5687
- if (file.index !== ' ' && file.index !== '?') {
5688
- fileChange.status = getStatus(file);
5689
- fileChange.summary = getSummaryText(file, fileChange);
5690
- staged.push(fileChange);
5691
- }
5692
- // Untracked files
5693
- if (file.working_dir === '?' && file.index === '?') {
5694
- fileChange.status = 'added';
5695
- fileChange.summary = getSummaryText(file, fileChange);
5696
- untracked.push(fileChange);
5697
- }
5698
- });
5699
- const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
5700
- const filteredStaged = staged.filter((file) => {
5701
- const extension = path.extname(file.filePath).toLowerCase();
5702
- return (!ignoredExtensionsSet.has(extension) &&
5703
- !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
5704
- });
5705
- const filteredUnstaged = unstaged.filter((file) => {
5706
- const extension = path.extname(file.filePath).toLowerCase();
5707
- return (!ignoredExtensionsSet.has(extension) &&
5708
- !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
5709
- });
5710
- const filteredUntracked = untracked.filter((file) => {
5711
- const extension = path.extname(file.filePath).toLowerCase();
5712
- return (!ignoredExtensionsSet.has(extension) &&
5713
- !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
5714
- });
5715
- return {
5716
- staged: filteredStaged,
5717
- unstaged: filteredUnstaged,
5718
- untracked: filteredUntracked,
5719
- };
5720
- }
5721
-
5722
- /**
5723
- * Retrieves the SimpleGit instance for the repository.
5724
- * @returns {SimpleGit} The SimpleGit instance.
5725
- */
5726
- const getRepo = () => {
5727
- let git;
5728
- try {
5729
- git = simpleGit.simpleGit();
5730
- }
5731
- catch (e) {
5732
- console.log('Error initializing git repo', e);
5733
- process.exit(1);
5734
- }
5735
- return git;
5736
- };
5737
-
5738
- const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
5739
- Commit Messages must have a short description that is less than 50 characters and a longer detailed summary no more than 300 characters, the shorter and more concise the better. Please follow the guidelines below when writing your commit message:
5740
-
5741
- - Write concisely using an informal tone
5742
- - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
5743
- - DO NOT use specific names or files from the code
5744
- - DO NOT include any diffs or file changes in the commit message
5745
- - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
5746
-
5747
- {format_instructions}
5748
-
5749
- """{summary}"""`;
5750
- const inputVariables$1 = ['summary', 'format_instructions'];
5751
- const COMMIT_PROMPT = new prompts.PromptTemplate({
5752
- template: template$1,
5753
- inputVariables: inputVariables$1,
5754
- });
5755
-
5756
- /**
5757
- * Verify template string contains all required input variables
5758
- *
5759
- * @param text template string
5760
- * @param inputVariables template variables
5761
- * @returns boolean or error message
5762
- */
5763
- function validatePromptTemplate(text, inputVariables) {
5764
- if (!text) {
5765
- return 'Prompt template cannot be empty';
5766
- }
5767
- if (!inputVariables.some((entry) => text.includes(entry))) {
5768
- return ('Prompt template must include at least one of the following input variables: ' +
5769
- inputVariables.map((value) => `{${value}}`).join(', '));
6049
+ return 'This file has been deleted.';
5770
6050
  }
5771
- return true;
6051
+ if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
6052
+ const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
6053
+ return renamedDiff;
6054
+ }
6055
+ // If not deleted or renamed, get the diff from the index
6056
+ const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
6057
+ return defaultDiff;
5772
6058
  }
5773
6059
 
5774
- async function editPrompt(options) {
5775
- return await prompts$1.editor({
5776
- message: 'Edit the prompt',
5777
- default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
5778
- waitForUseInput: false,
5779
- postfix: 'Press ENTER to continue',
5780
- validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
6060
+ // Max tokens for GPT-3 is 4096
6061
+ // const MAX_TOKENS_PER_SUMMARY = 4096
6062
+ const MAX_TOKENS_PER_SUMMARY = 12288;
6063
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
6064
+ const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
6065
+ // const textSplitter = new TokenTextSplitter({
6066
+ // chunkSize: 10000,
6067
+ // chunkOverlap: 250,
6068
+ // });
6069
+ const summarizationChain = getSummarizationChain(model, {
6070
+ type: 'map_reduce',
6071
+ combineMapPrompt: SUMMARIZE_PROMPT,
6072
+ combinePrompt: SUMMARIZE_PROMPT,
6073
+ });
6074
+ logger.startTimer();
6075
+ const rootTreeNode = createDiffTree(changes);
6076
+ logger.stopTimer('Created file hierarchy');
6077
+ // Collect diffs
6078
+ logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
6079
+ const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
6080
+ logger.stopSpinner('Diffs Collected').stopTimer();
6081
+ // Summarize diffs
6082
+ logger.startTimer();
6083
+ const summary = await summarizeDiffs(diffs, {
6084
+ tokenizer,
6085
+ maxTokens: MAX_TOKENS_PER_SUMMARY,
6086
+ textSplitter,
6087
+ chain: summarizationChain,
6088
+ logger,
5781
6089
  });
6090
+ logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
6091
+ return summary;
5782
6092
  }
5783
6093
 
5784
- async function editResult(result, options) {
5785
- if (options.openInEditor) {
5786
- return await prompts$1.editor({
5787
- message: 'Edit the commit message',
5788
- default: result,
5789
- waitForUseInput: false,
5790
- validate: (text) => (text ? true : 'Commit message cannot be empty'),
5791
- });
5792
- }
5793
- return result;
6094
+ /**
6095
+ * Creates a commit with the specified commit message.
6096
+ *
6097
+ * @param message The commit message.
6098
+ * @param git The SimpleGit instance.
6099
+ * @returns A Promise that resolves to the CommitResult.
6100
+ */
6101
+ async function createCommit(message, git) {
6102
+ return await git.commit(message);
5794
6103
  }
5795
6104
 
5796
- async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
5797
- const choices = [
5798
- {
5799
- name: '✨ Looks good!',
5800
- value: 'approve',
5801
- description: descriptions?.approve || `Continue with the generated ${label}`,
5802
- },
5803
- {
5804
- name: '📝 Edit',
5805
- value: 'edit',
5806
- description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
5807
- },
5808
- ];
5809
- if (enableModifyPrompt) {
5810
- choices.push({
5811
- name: '🪶 Modify Prompt',
5812
- value: 'modifyPrompt',
5813
- description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
5814
- });
6105
+ /**
6106
+ * Determines the status of a file based on its changes in the Git repository.
6107
+ *
6108
+ * @param file - The file to check the status of.
6109
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
6110
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
6111
+ * @throws Error if the file type is invalid.
6112
+ */
6113
+ function getStatus(file, location = 'index') {
6114
+ if ('index' in file && 'working_dir' in file) {
6115
+ const statusCode = file[location];
6116
+ switch (statusCode) {
6117
+ case 'A':
6118
+ return 'added';
6119
+ case 'D':
6120
+ return 'deleted';
6121
+ case 'M':
6122
+ return 'modified';
6123
+ case 'R':
6124
+ return 'renamed';
6125
+ case '?':
6126
+ return 'untracked';
6127
+ default:
6128
+ return 'unknown';
6129
+ }
5815
6130
  }
5816
- if (enableRetry) {
5817
- choices.push({
5818
- name: '🔄 Retry',
5819
- value: 'retryMessageOnly',
5820
- description: descriptions?.retryMessageOnly ||
5821
- `Restart the function execution from generating the ${label}`,
5822
- });
6131
+ else if ('changes' in file && 'binary' in file) {
6132
+ if (file.changes === 0)
6133
+ return 'untracked';
6134
+ if (file.file.includes('=>'))
6135
+ return 'renamed';
6136
+ if (file.deletions === 0 && file.insertions > 0)
6137
+ return 'added';
6138
+ if (file.insertions === 0 && file.deletions > 0)
6139
+ return 'deleted';
6140
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
6141
+ return 'modified';
6142
+ return 'unknown';
5823
6143
  }
5824
- if (enableFullRetry) {
5825
- choices.push({
5826
- name: '🔄 Retry Full',
5827
- value: 'retryFull',
5828
- description: descriptions?.retryFull ||
5829
- `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
5830
- });
6144
+ else {
6145
+ throw new Error('Invalid file type');
5831
6146
  }
5832
- choices.push({
5833
- name: '💣 Cancel',
5834
- value: 'cancel',
5835
- description: descriptions?.cancel || `Cancel the ${label}`,
5836
- });
5837
- return (await prompts$1.select({
5838
- message: `Would you like to make any changes to the ${label}?`,
5839
- choices,
5840
- }));
5841
6147
  }
5842
6148
 
5843
- function logResult(label, result) {
5844
- console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
5845
- }
5846
-
5847
- async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
5848
- const { logger } = options;
5849
- let continueLoop = true;
5850
- let modifyPrompt = false;
5851
- let context = '';
5852
- let result = '';
5853
- const changes = await factory();
5854
- // if we don't have any changes, bail.
5855
- if (!changes || !Object.keys(changes).length) {
5856
- await noResult(options);
6149
+ /**
6150
+ * Returns the summary text for a file change.
6151
+ *
6152
+ * @param file - The file status or diff result.
6153
+ * @param change - The partial file change object.
6154
+ * @returns The summary text for the file change.
6155
+ * @throws Error if the file type is invalid.
6156
+ */
6157
+ function getSummaryText(file, change) {
6158
+ const status = change.status || getStatus(file);
6159
+ let filePath;
6160
+ if ('path' in file) {
6161
+ filePath = file.path;
5857
6162
  }
5858
- while (continueLoop) {
5859
- if (!context.length) {
5860
- context = await parser(changes, result, options);
5861
- }
5862
- // if we still don't have a context, bail.
5863
- if (!context.length) {
5864
- await noResult(options);
5865
- }
5866
- if (modifyPrompt) {
5867
- options.prompt = await editPrompt(options);
5868
- }
5869
- logger.startTimer().startSpinner(`Generating ${label}\n`, {
5870
- color: 'blue',
5871
- });
5872
- result = await agent(context, options);
5873
- if (!result) {
5874
- logger.stopSpinner('💀 Agent failed to return content.', {
5875
- mode: 'fail',
5876
- color: 'red',
5877
- });
5878
- process.exit(0);
5879
- }
5880
- logger
5881
- .stopSpinner(`Generated ${label}`, {
5882
- color: 'green',
5883
- mode: 'succeed',
5884
- })
5885
- .stopTimer();
5886
- if (options?.interactive) {
5887
- logResult(label, result);
5888
- const reviewAnswer = await getUserReviewDecision({
5889
- label,
5890
- ...(options?.review || {}),
5891
- });
5892
- if (reviewAnswer === 'cancel') {
5893
- process.exit(0);
5894
- }
5895
- if (reviewAnswer === 'edit') {
5896
- options.openInEditor = true;
5897
- }
5898
- if (reviewAnswer === 'retryFull') {
5899
- context = '';
5900
- result = '';
5901
- options.prompt = '';
5902
- continue;
5903
- }
5904
- if (reviewAnswer === 'retryMessageOnly') {
5905
- modifyPrompt = false;
5906
- result = '';
5907
- continue;
5908
- }
5909
- if (reviewAnswer === 'modifyPrompt') {
5910
- modifyPrompt = true;
5911
- result = '';
5912
- continue;
5913
- }
5914
- }
5915
- // if we're here, we're done.
5916
- result = await editResult(result, options);
5917
- continueLoop = false;
6163
+ else if ('file' in file) {
6164
+ filePath = change?.filePath || file.file;
5918
6165
  }
5919
- return result;
5920
- }
5921
-
5922
- const logSuccess = () => {
5923
- console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
5924
- };
5925
-
5926
- async function handleResult({ result, mode, interactiveHandler }) {
5927
- switch (mode) {
5928
- case 'interactive':
5929
- if (interactiveHandler) {
5930
- await interactiveHandler(result);
5931
- }
5932
- else {
5933
- console.warn('No result handler provided for interactive mode.');
5934
- logSuccess();
5935
- }
5936
- break;
5937
- case 'stdout':
5938
- default:
5939
- process.stdout.write(result, 'utf8');
5940
- break;
6166
+ else {
6167
+ throw new Error('Invalid file type');
5941
6168
  }
5942
- process.exit(0);
6169
+ if (change.oldFilePath) {
6170
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
6171
+ }
6172
+ return `${status}: ${filePath}`;
6173
+ }
6174
+
6175
+ /**
6176
+ * Retrieves the changes in the Git repository.
6177
+ *
6178
+ * @param {GetChangesInput} options - The options for retrieving the changes.
6179
+ * @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
6180
+ */
6181
+ async function getChanges({ git, options }) {
6182
+ const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
6183
+ const staged = [];
6184
+ const unstaged = [];
6185
+ const untracked = [];
6186
+ const status = await git.status();
6187
+ status.files.forEach((file) => {
6188
+ const fileChange = {
6189
+ filePath: file.path,
6190
+ oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
6191
+ };
6192
+ // Unstaged files
6193
+ if (file.working_dir !== '?' && file.working_dir !== ' ') {
6194
+ fileChange.status = getStatus(file, 'working_dir');
6195
+ fileChange.summary = getSummaryText(file, fileChange);
6196
+ unstaged.push(fileChange);
6197
+ }
6198
+ // Staged files
6199
+ if (file.index !== ' ' && file.index !== '?') {
6200
+ fileChange.status = getStatus(file);
6201
+ fileChange.summary = getSummaryText(file, fileChange);
6202
+ staged.push(fileChange);
6203
+ }
6204
+ // Untracked files
6205
+ if (file.working_dir === '?' && file.index === '?') {
6206
+ fileChange.status = 'added';
6207
+ fileChange.summary = getSummaryText(file, fileChange);
6208
+ untracked.push(fileChange);
6209
+ }
6210
+ });
6211
+ const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
6212
+ const filteredStaged = staged.filter((file) => {
6213
+ const extension = path.extname(file.filePath).toLowerCase();
6214
+ return (!ignoredExtensionsSet.has(extension) &&
6215
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
6216
+ });
6217
+ const filteredUnstaged = unstaged.filter((file) => {
6218
+ const extension = path.extname(file.filePath).toLowerCase();
6219
+ return (!ignoredExtensionsSet.has(extension) &&
6220
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
6221
+ });
6222
+ const filteredUntracked = untracked.filter((file) => {
6223
+ const extension = path.extname(file.filePath).toLowerCase();
6224
+ return (!ignoredExtensionsSet.has(extension) &&
6225
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
6226
+ });
6227
+ return {
6228
+ staged: filteredStaged,
6229
+ unstaged: filteredUnstaged,
6230
+ untracked: filteredUntracked,
6231
+ };
5943
6232
  }
5944
6233
 
5945
6234
  /**
@@ -5964,7 +6253,7 @@ const getTokenCounter = async (modelName) => {
5964
6253
  });
5965
6254
  };
5966
6255
 
5967
- async function noResult({ git, logger }) {
6256
+ async function noResult$1({ git, logger }) {
5968
6257
  const { staged, unstaged, untracked } = await getChanges({ git });
5969
6258
  const hasStaged = staged && staged.length > 0;
5970
6259
  const hasUnstaged = unstaged && unstaged.length > 0;
@@ -5973,339 +6262,103 @@ async function noResult({ git, logger }) {
5973
6262
  logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
5974
6263
  logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
5975
6264
  }
5976
- else if (hasUnstaged || hasUntracked) {
5977
- logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
5978
- if (hasUnstaged) {
5979
- logger.log('\nChanges not staged for commit:', { color: 'yellow' });
5980
- logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
5981
- color: 'red',
5982
- });
5983
- }
5984
- if (hasUntracked) {
5985
- logger.log('\nUntracked changes:', { color: 'yellow' });
5986
- logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
5987
- color: 'red',
5988
- });
5989
- }
5990
- }
5991
- else {
5992
- logger.log('No repo changes detected. 👀', { color: 'blue' });
5993
- }
5994
- }
5995
-
5996
- const handler$2 = async (argv, logger) => {
5997
- const git = getRepo();
5998
- const options = loadConfig(argv);
5999
- const key = getApiKeyForModel(options);
6000
- const { provider, model } = getModelAndProviderFromConfig(options);
6001
- if (options.service.authentication.type !== 'None' && !key) {
6002
- logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
6003
- process.exit(1);
6004
- }
6005
- const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
6006
- const llm = getLlm(provider, model, options);
6007
- const INTERACTIVE = isInteractive(options);
6008
- if (INTERACTIVE) {
6009
- logger.log(LOGO);
6010
- }
6011
- async function factory() {
6012
- const changes = await getChanges({ git });
6013
- return changes.staged;
6014
- }
6015
- async function parser(changes) {
6016
- return await fileChangeParser({
6017
- changes,
6018
- commit: '--staged',
6019
- options: { tokenizer, git, llm, logger },
6020
- });
6021
- }
6022
- const commitMsg = await generateAndReviewLoop({
6023
- label: 'commit message',
6024
- options: {
6025
- ...options,
6026
- prompt: options.prompt || COMMIT_PROMPT.template,
6027
- logger,
6028
- interactive: INTERACTIVE,
6029
- review: {
6030
- descriptions: {
6031
- approve: `Commit staged changes with generated commit message`,
6032
- edit: 'Edit the commit message before proceeding',
6033
- modifyPrompt: 'Modify the prompt template and regenerate the commit message',
6034
- retryMessageOnly: 'Restart the function execution from generating the commit message',
6035
- retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
6036
- },
6037
- },
6038
- },
6039
- factory,
6040
- parser,
6041
- agent: async (context, options) => {
6042
- const parser = new output_parsers.JsonOutputParser();
6043
- const prompt = getPrompt({
6044
- template: options.prompt,
6045
- variables: COMMIT_PROMPT.inputVariables,
6046
- fallback: COMMIT_PROMPT,
6047
- });
6048
- const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body'.";
6049
- const commitMsg = await executeChain({
6050
- llm,
6051
- prompt,
6052
- variables: { summary: context, format_instructions: formatInstructions },
6053
- parser,
6054
- });
6055
- return `${commitMsg.title}\n\n${commitMsg.body}`;
6056
- },
6057
- noResult: async () => {
6058
- await noResult({ git, logger });
6059
- process.exit(0);
6060
- },
6061
- });
6062
- const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
6063
- handleResult({
6064
- result: commitMsg,
6065
- interactiveHandler: async (result) => {
6066
- await createCommit(result, git);
6067
- logSuccess();
6068
- },
6069
- mode: MODE,
6070
- });
6071
- };
6072
-
6073
- /**
6074
- * Command line options via yargs
6075
- */
6076
- const options$2 = {
6077
- service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
6078
- openAIApiKey: {
6079
- type: 'string',
6080
- description: 'OpenAI API Key',
6081
- },
6082
- tokenLimit: { type: 'number', description: 'Token limit' },
6083
- prompt: {
6084
- type: 'string',
6085
- alias: 'p',
6086
- description: 'Commit message prompt',
6087
- },
6088
- i: {
6089
- type: 'boolean',
6090
- alias: 'interactive',
6091
- description: 'Toggle interactive mode',
6092
- },
6093
- s: {
6094
- type: 'boolean',
6095
- description: 'Automatically commit staged changes with generated commit message',
6096
- default: false,
6097
- },
6098
- e: {
6099
- type: 'boolean',
6100
- alias: 'edit',
6101
- description: 'Open commit message in editor before proceeding',
6102
- },
6103
- summarizePrompt: {
6104
- type: 'string',
6105
- description: 'Large file summary prompt',
6106
- },
6107
- ignoredFiles: {
6108
- type: 'array',
6109
- description: 'Ignored files',
6110
- },
6111
- ignoredExtensions: {
6112
- type: 'array',
6113
- description: 'Ignored extensions',
6114
- },
6115
- };
6116
- const builder$2 = (yargs) => {
6117
- return yargs.options(options$2);
6118
- };
6119
-
6120
- var commit = {
6121
- command: 'commit',
6122
- desc: 'Write a commit message summarizing the staged changes.',
6123
- builder: builder$2,
6124
- handler: commandExecutor(handler$2),
6125
- options: options$2,
6126
- };
6127
-
6128
- /**
6129
- * Retrieves the commit log range between two specified commits.
6130
- *
6131
- * @param from - The starting commit.
6132
- * @param to - The ending commit.
6133
- * @param options - Additional options for retrieving the commit log range.
6134
- * @returns A promise that resolves to an array of commit log messages.
6135
- * @throws If there is an error retrieving the commit log range.
6136
- */
6137
- async function getCommitLogRange(from, to, { noMerges, git }) {
6138
- try {
6139
- const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
6140
- const commitLog = await git.log(logOptions);
6141
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6142
- }
6143
- catch (error) {
6144
- // If there's an error, handle it appropriately
6145
- console.error('Error getting commit messages:', error);
6146
- throw error;
6147
- }
6148
- }
6149
-
6150
- /**
6151
- * Retrieves the name of the current branch.
6152
- *
6153
- * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
6154
- * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
6155
- */
6156
- async function getCurrentBranchName({ git }) {
6157
- return await git.revparse(['--abbrev-ref', 'HEAD']);
6158
- }
6159
-
6160
- /**
6161
- * Retrieves the commit log for the current branch.
6162
- *
6163
- * @param {Object} options - The options for retrieving the commit log.
6164
- * @param {SimpleGit} options.git - The SimpleGit instance.
6165
- * @param {Logger} options.logger - The logger for logging messages.
6166
- * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
6167
- * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
6168
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
6169
- */
6170
- async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
6171
- try {
6172
- // Get the current branch name
6173
- const branch = await getCurrentBranchName({ git });
6174
- // Check if the current branch has any commits
6175
- const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
6176
- if (!hasCommits) {
6177
- logger?.log('No commits on the current branch.');
6178
- return [];
6179
- }
6180
- // Get the list of commits that are unique to the current branch
6181
- let uniqueCommits;
6182
- if (comparisonBranch === branch) {
6183
- // If the comparison branch is the same as the current branch, we compare against the remote.
6184
- uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
6185
- .split('\n')
6186
- .filter(Boolean)
6187
- .reverse();
6188
- }
6189
- else {
6190
- // Your existing code for different branches
6191
- uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
6192
- .split('\n')
6193
- .filter(Boolean)
6194
- .reverse();
6265
+ else if (hasUnstaged || hasUntracked) {
6266
+ logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
6267
+ if (hasUnstaged) {
6268
+ logger.log('\nChanges not staged for commit:', { color: 'yellow' });
6269
+ logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
6270
+ color: 'red',
6271
+ });
6195
6272
  }
6196
- logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
6197
- const firstCommit = uniqueCommits[0];
6198
- const lastCommit = uniqueCommits[uniqueCommits.length - 1];
6199
- if (!firstCommit || !lastCommit) {
6200
- logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
6201
- return [];
6273
+ if (hasUntracked) {
6274
+ logger.log('\nUntracked changes:', { color: 'yellow' });
6275
+ logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
6276
+ color: 'red',
6277
+ });
6202
6278
  }
6203
- // Retrieve commit log with messages
6204
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
6205
6279
  }
6206
- catch (error) {
6207
- logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
6280
+ else {
6281
+ logger.log('No repo changes detected. 👀', { color: 'blue' });
6208
6282
  }
6209
- return [];
6210
6283
  }
6211
6284
 
6212
- const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
6213
-
6214
- - Include the git commit hash as reference for each change, including just the first 7 characters
6215
- - Logically group changes, and if necessary, summarize dependency updates
6216
-
6217
- {format_instructions}
6218
-
6219
- """{summary}"""`;
6220
- const inputVariables = ['format_instructions', 'summary'];
6221
- const CHANGELOG_PROMPT = new prompts.PromptTemplate({
6222
- template,
6223
- inputVariables,
6224
- });
6225
-
6226
- const handler$1 = async (argv, logger) => {
6227
- const config = loadConfig(argv);
6285
+ const handler$2 = async (argv, logger) => {
6228
6286
  const git = getRepo();
6287
+ const config = loadConfig(argv);
6229
6288
  const key = getApiKeyForModel(config);
6230
6289
  const { provider, model } = getModelAndProviderFromConfig(config);
6231
6290
  if (config.service.authentication.type !== 'None' && !key) {
6232
6291
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
6233
6292
  process.exit(1);
6234
6293
  }
6294
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
6235
6295
  const llm = getLlm(provider, model, config);
6236
6296
  const INTERACTIVE = isInteractive(config);
6237
6297
  if (INTERACTIVE) {
6238
6298
  logger.log(LOGO);
6239
6299
  }
6240
6300
  async function factory() {
6241
- const branchName = await getCurrentBranchName({ git });
6242
- if (config.range && config.range.includes(':')) {
6243
- const [from, to] = config.range.split(':');
6244
- if (!from || !to) {
6245
- logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
6246
- process.exit(1);
6247
- }
6248
- return {
6249
- branch: branchName,
6250
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
6251
- };
6252
- }
6253
- logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
6254
- return {
6255
- branch: branchName,
6256
- commits: await getCommitLogCurrentBranch({ git, logger }),
6257
- };
6301
+ const changes = await getChanges({ git });
6302
+ return changes.staged;
6258
6303
  }
6259
- async function parser({ branch, commits }) {
6260
- const result = `## ${branch}\n\n${commits.map((commit) => `${commit}`).join('\n\n\n')}`;
6261
- console.log({ result });
6262
- return result;
6304
+ async function parser(changes) {
6305
+ return await fileChangeParser({
6306
+ changes,
6307
+ commit: '--staged',
6308
+ options: { tokenizer, git, llm, logger },
6309
+ });
6263
6310
  }
6264
- const changelogMsg = await generateAndReviewLoop({
6265
- label: 'changelog',
6311
+ const commitMsg = await generateAndReviewLoop({
6312
+ label: 'commit message',
6313
+ options: {
6314
+ ...config,
6315
+ prompt: config.prompt || COMMIT_PROMPT.template,
6316
+ logger,
6317
+ interactive: INTERACTIVE,
6318
+ review: {
6319
+ descriptions: {
6320
+ approve: `Commit staged changes with generated commit message`,
6321
+ edit: 'Edit the commit message before proceeding',
6322
+ modifyPrompt: 'Modify the prompt template and regenerate the commit message',
6323
+ retryMessageOnly: 'Restart the function execution from generating the commit message',
6324
+ retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
6325
+ },
6326
+ },
6327
+ },
6266
6328
  factory,
6267
6329
  parser,
6268
6330
  agent: async (context, options) => {
6269
6331
  const parser = new output_parsers.JsonOutputParser();
6270
6332
  const prompt = getPrompt({
6271
6333
  template: options.prompt,
6272
- variables: CHANGELOG_PROMPT.inputVariables,
6273
- fallback: CHANGELOG_PROMPT,
6334
+ variables: COMMIT_PROMPT.inputVariables,
6335
+ fallback: COMMIT_PROMPT,
6274
6336
  });
6275
- const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content', both strings.";
6276
- const changelog = await executeChain({
6337
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
6338
+ const additionalContext = argv.additional ? `${argv.additional}` : '';
6339
+ const commitMsg = await executeChain({
6277
6340
  llm,
6278
6341
  prompt,
6279
6342
  variables: {
6280
6343
  summary: context,
6281
6344
  format_instructions: formatInstructions,
6345
+ additional: additionalContext,
6282
6346
  },
6283
6347
  parser,
6284
6348
  });
6285
- return `${changelog.header}\n\n${changelog.content}`;
6349
+ const appendedText = argv.append ? `\n\n${argv.append}` : '';
6350
+ return `${commitMsg.title}\n\n${commitMsg.body}${appendedText}`;
6286
6351
  },
6287
6352
  noResult: async () => {
6288
- if (config.range) {
6289
- logger.log(`No commits found in the provided range.`, { color: 'red' });
6290
- process.exit(0);
6291
- }
6292
- logger.log(`No commits found in the current branch.`, { color: 'red' });
6353
+ await noResult$1({ git, logger });
6293
6354
  process.exit(0);
6294
6355
  },
6295
- options: {
6296
- ...config,
6297
- prompt: config.prompt || CHANGELOG_PROMPT.template,
6298
- logger,
6299
- interactive: INTERACTIVE,
6300
- review: {
6301
- enableFullRetry: false,
6302
- },
6303
- },
6304
6356
  });
6305
6357
  const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
6306
6358
  handleResult({
6307
- result: changelogMsg,
6308
- interactiveHandler: async () => {
6359
+ result: commitMsg,
6360
+ interactiveHandler: async (result) => {
6361
+ await createCommit(result, git);
6309
6362
  logSuccess();
6310
6363
  },
6311
6364
  mode: MODE,
@@ -6315,43 +6368,39 @@ const handler$1 = async (argv, logger) => {
6315
6368
  /**
6316
6369
  * Command line options via yargs
6317
6370
  */
6318
- const options$1 = {
6319
- range: {
6320
- type: 'string',
6321
- alias: 'r',
6322
- description: 'Commit range e.g `HEAD~2:HEAD`',
6323
- },
6324
- tokenLimit: { type: 'number', description: 'Token limit' },
6325
- prompt: {
6326
- type: 'string',
6327
- alias: 'p',
6328
- description: 'Prompt for llm',
6329
- },
6371
+ const options$2 = {
6330
6372
  i: {
6331
- type: 'boolean',
6332
6373
  alias: 'interactive',
6333
6374
  description: 'Toggle interactive mode',
6334
- },
6335
- e: {
6336
6375
  type: 'boolean',
6337
- alias: 'edit',
6338
- description: 'Open generated changelog message in editor before proceeding',
6339
6376
  },
6340
- summarizePrompt: {
6377
+ ignoredFiles: {
6378
+ description: 'Ignored files',
6379
+ type: 'array',
6380
+ },
6381
+ ignoredExtensions: {
6382
+ description: 'Ignored extensions',
6383
+ type: 'array',
6384
+ },
6385
+ append: {
6386
+ description: 'Add content to the end of the generated commit message',
6387
+ type: 'string',
6388
+ },
6389
+ additional: {
6390
+ description: 'Add extra contextual information to the prompt',
6341
6391
  type: 'string',
6342
- description: 'Prompt for summarizing large files',
6343
6392
  },
6344
6393
  };
6345
- const builder$1 = (yargs) => {
6346
- return yargs.options(options$1);
6394
+ const builder$2 = (yargsInstance) => {
6395
+ return yargsInstance.options(options$2).usage(getCommandUsageHeader(commit.command));
6347
6396
  };
6348
6397
 
6349
- var changelog = {
6350
- command: 'changelog',
6351
- desc: 'Generate a changelog from a commit range',
6352
- builder: builder$1,
6353
- handler: commandExecutor(handler$1),
6354
- options: options$1,
6398
+ var commit = {
6399
+ command: 'commit',
6400
+ desc: 'Summarize the staged changes in a commit message.',
6401
+ builder: builder$2,
6402
+ handler: commandExecutor(handler$2),
6403
+ options: options$2,
6355
6404
  };
6356
6405
 
6357
6406
  /**
@@ -6451,13 +6500,13 @@ async function checkAndHandlePackageInstallation({ global = false, logger, }) {
6451
6500
  const projectRoot = findProjectRoot(process.cwd());
6452
6501
  let shouldInstall = false;
6453
6502
  if (isPackageInstalled(packageName, projectRoot)) {
6454
- shouldInstall = await prompts$1.confirm({
6503
+ shouldInstall = await prompts.confirm({
6455
6504
  message: `'${packageName}' is already installed in '${projectRoot}/package.json', would you like to update?`,
6456
6505
  default: shouldInstall,
6457
6506
  });
6458
6507
  }
6459
6508
  else {
6460
- shouldInstall = await prompts$1.confirm({
6509
+ shouldInstall = await prompts.confirm({
6461
6510
  message: `'${packageName}' is not installed in '${projectRoot}/package.json', would you like to install?`,
6462
6511
  default: shouldInstall,
6463
6512
  });
@@ -6495,7 +6544,7 @@ const questions = {
6495
6544
  /**
6496
6545
  * @description configure coco globally for the current user or project?
6497
6546
  */
6498
- whatScope: async () => await prompts$1.select({
6547
+ whatScope: async () => await prompts.select({
6499
6548
  message: 'configure coco globally for the current user or for the current directory?',
6500
6549
  choices: [
6501
6550
  {
@@ -6510,7 +6559,7 @@ const questions = {
6510
6559
  },
6511
6560
  ],
6512
6561
  }),
6513
- selectLLMProvider: async () => await prompts$1.select({
6562
+ selectLLMProvider: async () => await prompts.select({
6514
6563
  message: 'select language model provider:',
6515
6564
  choices: [
6516
6565
  {
@@ -6552,7 +6601,7 @@ const questions = {
6552
6601
  })),
6553
6602
  ];
6554
6603
  }
6555
- return await prompts$1.select({
6604
+ return await prompts.select({
6556
6605
  message: 'select language model:',
6557
6606
  choices: availableModels,
6558
6607
  });
@@ -6563,7 +6612,7 @@ const questions = {
6563
6612
  * print results to stdout
6564
6613
  * @returns 'interactive' | 'stdout'
6565
6614
  */
6566
- selectMode: async () => await prompts$1.select({
6615
+ selectMode: async () => await prompts.select({
6567
6616
  message: 'select mode:',
6568
6617
  choices: [
6569
6618
  {
@@ -6581,19 +6630,19 @@ const questions = {
6581
6630
  inputOpenAIApiKey: async () => {
6582
6631
  // check for existing env var
6583
6632
  if (process.env.OPENAI_API_KEY) {
6584
- return (await prompts$1.confirm({
6633
+ return (await prompts.confirm({
6585
6634
  message: `use existing OPENAI_API_KEY env var?`,
6586
6635
  default: true,
6587
6636
  }))
6588
6637
  ? process.env.OPENAI_API_KEY
6589
- : await prompts$1.password({
6638
+ : await prompts.password({
6590
6639
  message: `enter your OpenAI API key:`,
6591
6640
  validate(input) {
6592
6641
  return input.length > 0 ? true : 'API key cannot be empty';
6593
6642
  },
6594
6643
  });
6595
6644
  }
6596
- return await prompts$1.password({
6645
+ return await prompts.password({
6597
6646
  message: `enter your OpenAI API key:`,
6598
6647
  validate(input) {
6599
6648
  return input.length > 0 ? true : 'API key cannot be empty';
@@ -6601,48 +6650,48 @@ const questions = {
6601
6650
  });
6602
6651
  },
6603
6652
  inputTokenLimit: async () => {
6604
- const tokenLimit = await prompts$1.input({
6653
+ const tokenLimit = await prompts.input({
6605
6654
  message: 'maximum number of tokens for generating commit messages:',
6606
6655
  default: '300',
6607
6656
  });
6608
6657
  return parseInt(tokenLimit);
6609
6658
  },
6610
6659
  inputModelTemperature: async () => {
6611
- const temperature = await prompts$1.input({
6660
+ const temperature = await prompts.input({
6612
6661
  message: 'model temperature for generating commit messages:',
6613
6662
  default: '0.36',
6614
6663
  });
6615
6664
  return parseFloat(temperature);
6616
6665
  },
6617
- selectDefaultGitBranch: async () => (await prompts$1.input({
6666
+ selectDefaultGitBranch: async () => (await prompts.input({
6618
6667
  message: 'default branch for the repository:',
6619
6668
  default: 'main',
6620
6669
  })) || 'main',
6621
- configureAdvancedOptions: async () => await prompts$1.confirm({
6670
+ configureAdvancedOptions: async () => await prompts.confirm({
6622
6671
  message: 'would you like to configure advanced options?',
6623
6672
  default: false,
6624
6673
  }),
6625
- enableVerboseMode: async () => await prompts$1.confirm({
6674
+ enableVerboseMode: async () => await prompts.confirm({
6626
6675
  message: 'enable verbose logging:',
6627
6676
  default: false,
6628
6677
  }),
6629
- whatFilesToIgnore: async () => (await prompts$1.input({
6678
+ whatFilesToIgnore: async () => (await prompts.input({
6630
6679
  message: 'paths of files to be excluded when generating commit messages (comma-separated):',
6631
6680
  default: 'package-lock.json',
6632
6681
  }))
6633
6682
  ?.split(',')
6634
6683
  ?.map((file) => file.trim()) || [],
6635
- whatExtensionsToIgnore: async () => (await prompts$1.input({
6684
+ whatExtensionsToIgnore: async () => (await prompts.input({
6636
6685
  message: 'file extensions to be excluded when generating commit messages (comma-separated):',
6637
6686
  default: '.map, .lock',
6638
6687
  }))
6639
6688
  ?.split(',')
6640
6689
  ?.map((ext) => ext.trim()) || [],
6641
- modifyCommitPrompt: async () => await prompts$1.editor({
6690
+ modifyCommitPrompt: async () => await prompts.editor({
6642
6691
  message: 'modify default commit message prompt:',
6643
6692
  default: COMMIT_PROMPT.template,
6644
6693
  }),
6645
- selectProjectConfigFileType: async () => await prompts$1.select({
6694
+ selectProjectConfigFileType: async () => await prompts.select({
6646
6695
  message: 'where would you like to store the project config?',
6647
6696
  choices: [
6648
6697
  {
@@ -6657,7 +6706,7 @@ const questions = {
6657
6706
  }),
6658
6707
  };
6659
6708
 
6660
- const handler = async (argv, logger) => {
6709
+ const handler$1 = async (argv, logger) => {
6661
6710
  const options = loadConfig(argv);
6662
6711
  logger.log(LOGO);
6663
6712
  let scope = options?.scope;
@@ -6695,10 +6744,13 @@ const handler = async (argv, logger) => {
6695
6744
  if (advOptions) {
6696
6745
  config.mode = await questions.selectMode();
6697
6746
  config.defaultBranch = await questions.selectDefaultGitBranch();
6698
- config.temperature = await questions.inputModelTemperature();
6699
- config.tokenLimit = await questions.inputTokenLimit();
6747
+ config.service = {
6748
+ ...config.service,
6749
+ temperature: await questions.inputModelTemperature(),
6750
+ tokenLimit: await questions.inputTokenLimit(),
6751
+ };
6700
6752
  config.verbose = await questions.enableVerboseMode();
6701
- const promptForIgnores = await prompts$1.confirm({
6753
+ const promptForIgnores = await prompts.confirm({
6702
6754
  message: 'would you like to configure ignored files and extensions?',
6703
6755
  default: false,
6704
6756
  });
@@ -6706,7 +6758,7 @@ const handler = async (argv, logger) => {
6706
6758
  config.ignoredFiles = await questions.whatFilesToIgnore();
6707
6759
  config.ignoredExtensions = await questions.whatExtensionsToIgnore();
6708
6760
  }
6709
- const promptForCommitPrompt = await prompts$1.confirm({
6761
+ const promptForCommitPrompt = await prompts.confirm({
6710
6762
  message: 'would you like to configure the commit message prompt?',
6711
6763
  default: false,
6712
6764
  });
@@ -6723,7 +6775,7 @@ const handler = async (argv, logger) => {
6723
6775
  approvalMessage = 'looking good? (API key hidden for security)';
6724
6776
  }
6725
6777
  }
6726
- const isApproved = await prompts$1.confirm({
6778
+ const isApproved = await prompts.confirm({
6727
6779
  message: approvalMessage,
6728
6780
  });
6729
6781
  let configFilePath = '';
@@ -6760,20 +6812,217 @@ const handler = async (argv, logger) => {
6760
6812
  /**
6761
6813
  * Command line options via yargs
6762
6814
  */
6763
- const options = {
6815
+ const options$1 = {
6764
6816
  scope: {
6765
6817
  type: 'string',
6766
6818
  description: 'configure coco for the current user or project?',
6767
6819
  choices: ['global', 'project'],
6768
6820
  },
6769
6821
  };
6770
- const builder = (yargs) => {
6771
- return yargs.options(options);
6822
+ const builder$1 = (yargs) => {
6823
+ return yargs.options(options$1).usage(getCommandUsageHeader(init.command));
6772
6824
  };
6773
6825
 
6774
6826
  var init = {
6775
6827
  command: 'init',
6776
6828
  desc: 'install & configure coco globally or for the current project',
6829
+ builder: builder$1,
6830
+ handler: commandExecutor(handler$1),
6831
+ options: options$1,
6832
+ };
6833
+
6834
+ /**
6835
+ * Formats a commit log into a readable string format.
6836
+ *
6837
+ * @param commitLog - The commit log result containing an array of commit details.
6838
+ * @returns An array of formatted commit log strings.
6839
+ *
6840
+ * Each formatted string includes:
6841
+ * - The date of the commit in square brackets.
6842
+ * - The commit message.
6843
+ * - The commit body.
6844
+ * - The commit hash in parentheses.
6845
+ * - The author's name and email in angle brackets.
6846
+ */
6847
+ const formatCommitLog = (commitLog) => {
6848
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6849
+ };
6850
+
6851
+ const getChangesByTimestamp = async ({ since, git }) => {
6852
+ const commitLog = await git.log({ '--since': since });
6853
+ return formatCommitLog(commitLog);
6854
+ };
6855
+
6856
+ const getChangesSinceLastTag = async ({ git }) => {
6857
+ const tags = await git.tags();
6858
+ if (tags.all.length > 0) {
6859
+ const lastTag = tags.latest;
6860
+ const commitLog = await git.log({ from: lastTag });
6861
+ return formatCommitLog(commitLog);
6862
+ }
6863
+ else {
6864
+ return ['No tags found in the repository.'];
6865
+ }
6866
+ };
6867
+
6868
+ async function noResult({ logger }) {
6869
+ logger.log('No repo changes detected. 👀', { color: 'blue' });
6870
+ }
6871
+
6872
+ const template = `Following the formatting instructions, summarize the following changes in the underlying git repository/branch.
6873
+ The summarization should descibe in a general sense what has changed in the repository over the specified timeframe. Specific files can be mentioned, but the summary should be general enough to be useful to someone who has not seen the changes.
6874
+
6875
+ Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
6876
+
6877
+ {timeframe}
6878
+
6879
+ {format_instructions}
6880
+
6881
+ """{changes}"""`;
6882
+ const inputVariables = ['format_instructions', 'changes', 'timeframe'];
6883
+ const RECAP_PROMPT = new prompts$1.PromptTemplate({
6884
+ template,
6885
+ inputVariables,
6886
+ });
6887
+
6888
+ const handler = async (argv, logger) => {
6889
+ const git = getRepo();
6890
+ const config = loadConfig(argv);
6891
+ const key = getApiKeyForModel(config);
6892
+ const { provider, model } = getModelAndProviderFromConfig(config);
6893
+ if (config.service.authentication.type !== 'None' && !key) {
6894
+ logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
6895
+ process.exit(1);
6896
+ }
6897
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
6898
+ const llm = getLlm(provider, model, config);
6899
+ const INTERACTIVE = isInteractive(config);
6900
+ if (INTERACTIVE) {
6901
+ logger.log(LOGO);
6902
+ }
6903
+ const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
6904
+ const timeframe = lastMonth ? 'last-month' : lastTag ? 'last-tag' : yesterday ? 'yesterday' : lastWeek ? 'last-week' : 'current';
6905
+ logger.log(`Generating recap for timeframe: ${timeframe}`);
6906
+ async function factory() {
6907
+ switch (timeframe) {
6908
+ case 'current':
6909
+ const { staged, unstaged, untracked } = await getChanges({ git });
6910
+ logger.log(`Staged: ${staged.length}, Unstaged: ${unstaged?.length || 0}, Untracked: ${untracked?.length || 0}`);
6911
+ const result = await fileChangeParser({
6912
+ changes: [...staged, ...(unstaged ? unstaged : []), ...(untracked ? untracked : [])],
6913
+ commit: 'HEAD',
6914
+ options: { tokenizer, git, llm, logger },
6915
+ });
6916
+ logger.log(`Parsed Result: ${result}`);
6917
+ return [result];
6918
+ case 'yesterday':
6919
+ const yesterday = new Date();
6920
+ yesterday.setDate(yesterday.getDate() - 1);
6921
+ return await getChangesByTimestamp({ git, since: yesterday.toISOString().split('T')[0] });
6922
+ case 'last-week':
6923
+ const lastWeek = new Date();
6924
+ lastWeek.setDate(lastWeek.getDate() - 7);
6925
+ return await getChangesByTimestamp({ git, since: lastWeek.toISOString().split('T')[0] });
6926
+ case 'last-month':
6927
+ const lastMonth = new Date();
6928
+ lastMonth.setMonth(lastMonth.getMonth() - 1);
6929
+ return await getChangesByTimestamp({ git, since: lastMonth.toISOString().split('T')[0] });
6930
+ case 'last-tag':
6931
+ const tags = await getChangesSinceLastTag({ git });
6932
+ return tags;
6933
+ default:
6934
+ logger.log(`Invalid timeframe: ${timeframe}`, { color: 'red' });
6935
+ return [];
6936
+ }
6937
+ }
6938
+ async function parser(changes) {
6939
+ console.log({ changes });
6940
+ return changes.join('\n');
6941
+ }
6942
+ const recap = await generateAndReviewLoop({
6943
+ label: 'recap',
6944
+ options: {
6945
+ ...config,
6946
+ prompt: config.prompt || RECAP_PROMPT.template,
6947
+ logger,
6948
+ interactive: INTERACTIVE,
6949
+ review: {
6950
+ descriptions: {
6951
+ approve: `Commit staged changes with generated commit message`,
6952
+ edit: 'Edit the commit message before proceeding',
6953
+ modifyPrompt: 'Modify the prompt template and regenerate the commit message',
6954
+ retryMessageOnly: 'Restart the function execution from generating the commit message',
6955
+ retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
6956
+ },
6957
+ },
6958
+ },
6959
+ factory,
6960
+ parser,
6961
+ agent: async (context, options) => {
6962
+ const parser = new output_parsers.JsonOutputParser();
6963
+ const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
6964
+ const prompt = getPrompt({
6965
+ template: options.prompt,
6966
+ variables: RECAP_PROMPT.inputVariables,
6967
+ fallback: RECAP_PROMPT,
6968
+ });
6969
+ const response = await executeChain({
6970
+ llm,
6971
+ prompt,
6972
+ variables: {
6973
+ changes: context,
6974
+ format_instructions: formatInstructions,
6975
+ timeframe,
6976
+ },
6977
+ parser,
6978
+ });
6979
+ logger.log(response.summary || 'no response');
6980
+ return `${response.summary}`;
6981
+ },
6982
+ noResult: async () => {
6983
+ await noResult({ git, logger });
6984
+ process.exit(0);
6985
+ },
6986
+ });
6987
+ logger.log(`Recap generated: ${recap}`, { color: 'green' });
6988
+ };
6989
+
6990
+ /**
6991
+ * Command line options via yargs
6992
+ */
6993
+ const options = {
6994
+ yesterday: {
6995
+ type: 'boolean',
6996
+ description: 'Recap for yesterday',
6997
+ },
6998
+ "last-week": {
6999
+ alias: 'week',
7000
+ type: 'boolean',
7001
+ description: 'Recap for last week',
7002
+ },
7003
+ "last-month": {
7004
+ alias: 'month',
7005
+ type: 'boolean',
7006
+ description: 'Recap for last month',
7007
+ },
7008
+ "last-tag": {
7009
+ alias: 'tag',
7010
+ type: 'boolean',
7011
+ description: 'Recap for last tag',
7012
+ },
7013
+ i: {
7014
+ type: 'boolean',
7015
+ alias: 'interactive',
7016
+ description: 'Toggle interactive mode',
7017
+ },
7018
+ };
7019
+ const builder = (yargsInstance) => {
7020
+ return yargsInstance.options(options).usage(getCommandUsageHeader(recap.command));
7021
+ };
7022
+
7023
+ var recap = {
7024
+ command: 'recap',
7025
+ desc: 'Summarize the changes in the repository over a specified timeframe.',
6777
7026
  builder,
6778
7027
  handler: commandExecutor(handler),
6779
7028
  options,
@@ -6784,25 +7033,15 @@ var types = /*#__PURE__*/Object.freeze({
6784
7033
  });
6785
7034
 
6786
7035
  const y = yargs();
6787
- y.scriptName('coco').usage('$0 <cmd> [args]');
6788
- y.command([commit.command, '$0'], commit.desc,
6789
- // TODO: fix type on builder
6790
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6791
- // @ts-ignore
6792
- commit.builder, commit.handler).options(commit.options);
6793
- y.command(changelog.command, changelog.desc,
6794
- // TODO: fix type on builder
6795
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6796
- // @ts-ignore
6797
- changelog.builder, changelog.handler).options(changelog.options);
6798
- y.command(init.command, init.desc,
6799
- // TODO: fix type on builder
6800
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6801
- // @ts-ignore
6802
- init.builder, init.handler).options(init.options);
6803
- y.parse(process.argv.slice(2));
7036
+ y.scriptName('coco');
7037
+ y.command([commit.command, '$0'], commit.desc, commit.builder, commit.handler);
7038
+ y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
7039
+ y.command(recap.command, recap.desc, recap.builder, recap.handler);
7040
+ y.command(init.command, init.desc, init.builder, init.handler);
7041
+ y.help().parse(process.argv.slice(2));
6804
7042
 
6805
7043
  exports.changelog = changelog;
6806
7044
  exports.commit = commit;
6807
7045
  exports.init = init;
7046
+ exports.recap = recap;
6808
7047
  exports.types = types;