git-coco 0.14.8 → 0.14.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,9 @@ coco changelog -r HEAD~5:HEAD
83
83
 
84
84
  # For a target branch
85
85
  coco changelog -b other-branch
86
+
87
+ # For all commits since the last tag
88
+ coco changelog -t
86
89
  ```
87
90
 
88
91
  ### **`coco recap`**
package/dist/index.d.ts CHANGED
@@ -150,6 +150,7 @@ interface BaseCommandOptions extends BaseArgvOptions {
150
150
  interface ChangelogOptions extends BaseCommandOptions {
151
151
  range: string;
152
152
  branch: string;
153
+ sinceLastTag: boolean;
153
154
  }
154
155
  type ChangelogArgv = Arguments<ChangelogOptions>;
155
156
 
@@ -166,6 +167,7 @@ interface CommitOptions extends BaseCommandOptions {
166
167
  openInEditor: boolean;
167
168
  ignoredFiles: string[];
168
169
  ignoredExtensions: string[];
170
+ withPreviousCommits: number;
169
171
  }
170
172
  type CommitArgv = Arguments<CommitOptions>;
171
173
 
@@ -18,7 +18,7 @@ import prettyMilliseconds from 'pretty-ms';
18
18
  import { ChatAnthropic } from '@langchain/anthropic';
19
19
  import { ChatOllama } from '@langchain/ollama';
20
20
  import { ChatOpenAI } from '@langchain/openai';
21
- import { JsonOutputParser, BaseOutputParser } from '@langchain/core/output_parsers';
21
+ import { JsonOutputParser, BaseOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
22
22
  import { simpleGit } from 'simple-git';
23
23
  import pQueue from 'p-queue';
24
24
  import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
@@ -55,7 +55,7 @@ import * as readline from 'readline';
55
55
  /**
56
56
  * Current build version from package.json
57
57
  */
58
- const BUILD_VERSION = "0.14.8";
58
+ const BUILD_VERSION = "0.14.9";
59
59
 
60
60
  const isInteractive = (config) => {
61
61
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1860,6 +1860,12 @@ const options$4 = {
1860
1860
  alias: 'b',
1861
1861
  description: 'Target branch to compare against',
1862
1862
  },
1863
+ sinceLastTag: {
1864
+ type: 'boolean',
1865
+ alias: 't',
1866
+ description: 'Generate changelog for all commits since the last tag',
1867
+ default: false,
1868
+ },
1863
1869
  i: {
1864
1870
  type: 'boolean',
1865
1871
  alias: 'interactive',
@@ -1912,6 +1918,7 @@ function getPrompt({ template, variables, fallback }) {
1912
1918
  ? new PromptTemplate({
1913
1919
  template,
1914
1920
  inputVariables: variables,
1921
+ templateFormat: 'mustache',
1915
1922
  })
1916
1923
  : fallback);
1917
1924
  }
@@ -1942,6 +1949,35 @@ function extractTicketIdFromBranchName(branchName) {
1942
1949
  return match ? match[0] : null;
1943
1950
  }
1944
1951
 
1952
+ /**
1953
+ * Formats a commit log into a readable string format.
1954
+ *
1955
+ * @param commitLog - The commit log result containing an array of commit details.
1956
+ * @returns An array of formatted commit log strings.
1957
+ *
1958
+ * Each formatted string includes:
1959
+ * - The date of the commit in square brackets.
1960
+ * - The commit message.
1961
+ * - The commit body.
1962
+ * - The commit hash in parentheses.
1963
+ * - The author's name and email in angle brackets.
1964
+ */
1965
+ const formatCommitLog = (commitLog) => {
1966
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
1967
+ };
1968
+
1969
+ const getChangesSinceLastTag = async ({ git }) => {
1970
+ const tags = await git.tags();
1971
+ if (tags.all.length > 0) {
1972
+ const lastTag = tags.latest;
1973
+ const commitLog = await git.log({ from: lastTag });
1974
+ return formatCommitLog(commitLog);
1975
+ }
1976
+ else {
1977
+ return ['No tags found in the repository.'];
1978
+ }
1979
+ };
1980
+
1945
1981
  /**
1946
1982
  * Retrieves the commit log range between two specified commits.
1947
1983
  *
@@ -2075,8 +2111,17 @@ const getRepo = () => {
2075
2111
  return git;
2076
2112
  };
2077
2113
 
2078
- const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2079
- 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.
2114
+ /**
2115
+ * Template for generating git commit messages based on code changes
2116
+ *
2117
+ * Variables:
2118
+ * - summary: Contains the diff summary of staged changes
2119
+ * - format_instructions: Instructions for the output format (JSON with title and body)
2120
+ * - additional_context: Optional user-provided context to guide the commit message generation
2121
+ * - commit_history: Optional history of previous commits for context
2122
+ */
2123
+ const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2124
+ 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.
2080
2125
 
2081
2126
  Please follow the guidelines below when writing your commit message:
2082
2127
 
@@ -2088,13 +2133,16 @@ Please follow the guidelines below when writing your commit message:
2088
2133
 
2089
2134
  {format_instructions}
2090
2135
 
2136
+ {commit_history}
2137
+
2091
2138
  """"""
2092
2139
  {summary}
2093
2140
  """"""
2094
2141
 
2095
- {additional}
2142
+ {additional_context}
2096
2143
  `;
2097
- const inputVariables$3 = ['summary', 'format_instructions', 'additional'];
2144
+ // Define the variables that will be passed to the prompt template
2145
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history'];
2098
2146
  const COMMIT_PROMPT = new PromptTemplate({
2099
2147
  template: template$4,
2100
2148
  inputVariables: inputVariables$3,
@@ -2260,9 +2308,16 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
2260
2308
  result = '';
2261
2309
  continue;
2262
2310
  }
2311
+ // Only edit the result in interactive mode if approved
2312
+ result = await editResult(result, options);
2313
+ }
2314
+ else {
2315
+ // In non-interactive mode, we return the result as is to be output to stdout by the caller.
2316
+ const displayResult = reviewParser ? reviewParser(result, options) : result;
2317
+ // In non-interactive mode, ensure we return the properly formatted result
2318
+ result = displayResult;
2263
2319
  }
2264
2320
  // if we're here, we're done.
2265
- result = await editResult(result, options);
2266
2321
  continueLoop = false;
2267
2322
  }
2268
2323
  return result;
@@ -2285,7 +2340,8 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
2285
2340
  break;
2286
2341
  case 'stdout':
2287
2342
  default:
2288
- process.stdout.write(result, 'utf8');
2343
+ // Ensure we write the result to stdout in non-interactive mode
2344
+ process.stdout.write(result + '\n', 'utf8');
2289
2345
  break;
2290
2346
  }
2291
2347
  process.exit(0);
@@ -2324,6 +2380,13 @@ const handler$4 = async (argv, logger) => {
2324
2380
  }
2325
2381
  async function factory() {
2326
2382
  const branchName = await getCurrentBranchName({ git });
2383
+ if (config.sinceLastTag) {
2384
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
2385
+ return {
2386
+ branch: branchName,
2387
+ commits: await getChangesSinceLastTag({ git, logger }),
2388
+ };
2389
+ }
2327
2390
  if (config.range && config.range.includes(':')) {
2328
2391
  const [from, to] = config.range.split(':');
2329
2392
  if (!from || !to) {
@@ -2342,7 +2405,7 @@ const handler$4 = async (argv, logger) => {
2342
2405
  commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
2343
2406
  };
2344
2407
  }
2345
- logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
2408
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, { color: 'yellow' });
2346
2409
  return {
2347
2410
  branch: branchName,
2348
2411
  commits: await getCommitLogCurrentBranch({ git, logger }),
@@ -2414,7 +2477,7 @@ const handler$4 = async (argv, logger) => {
2414
2477
 
2415
2478
  var changelog = {
2416
2479
  command: command$4,
2417
- desc: 'Generate a changelog from current or target branch or provided commit range.',
2480
+ desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
2418
2481
  builder: builder$4,
2419
2482
  handler: commandExecutor(handler$4),
2420
2483
  options: options$4,
@@ -2452,6 +2515,12 @@ const options$3 = {
2452
2515
  type: 'string',
2453
2516
  alias: 'a',
2454
2517
  },
2518
+ withPreviousCommits: {
2519
+ description: 'Include previous commits as context (specify number of commits, 0 for none)',
2520
+ type: 'number',
2521
+ default: 0,
2522
+ alias: 'p',
2523
+ },
2455
2524
  };
2456
2525
  const builder$3 = (yargs) => {
2457
2526
  return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
@@ -6189,6 +6258,48 @@ async function getChanges({ git, options }) {
6189
6258
  };
6190
6259
  }
6191
6260
 
6261
+ /**
6262
+ * Format a single commit log entry into a readable string
6263
+ * @param commit - The commit log entry
6264
+ * @returns Formatted commit log string
6265
+ */
6266
+ function formatSingleCommit(commit) {
6267
+ const { hash, date, message, body, author_name } = commit;
6268
+ const shortHash = hash.substring(0, 7);
6269
+ return `Commit: ${shortHash}
6270
+ Author: ${author_name}
6271
+ Date: ${date}
6272
+ Message: ${message}
6273
+ ${body ? `\nDetails: ${body}` : ''}`;
6274
+ }
6275
+
6276
+ /**
6277
+ * Get the specified number of previous commits
6278
+ * @param options - Options for getting previous commits
6279
+ * @returns Formatted commit logs
6280
+ */
6281
+ async function getPreviousCommits(options) {
6282
+ const { git, count = 1 } = options;
6283
+ if (count <= 0) {
6284
+ return '';
6285
+ }
6286
+ try {
6287
+ const logs = await git.log({ maxCount: count });
6288
+ if (!logs || logs.total === 0) {
6289
+ return '';
6290
+ }
6291
+ // Format the commit logs
6292
+ const formattedLogs = logs.all.map((commit) => {
6293
+ return formatSingleCommit(commit);
6294
+ }).join('\n\n');
6295
+ return formattedLogs;
6296
+ }
6297
+ catch (error) {
6298
+ console.error(`Error getting previous commits: ${error.message}`);
6299
+ return '';
6300
+ }
6301
+ }
6302
+
6192
6303
  /**
6193
6304
  * Retrieves a TikToken for the specified model.
6194
6305
  *
@@ -6302,14 +6413,30 @@ const handler$3 = async (argv, logger) => {
6302
6413
  fallback: COMMIT_PROMPT,
6303
6414
  });
6304
6415
  const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
6305
- const additionalContext = argv.additional ? `${argv.additional}` : '';
6416
+ // Get additional context if provided
6417
+ let additional_context = '';
6418
+ if (argv.additional) {
6419
+ additional_context = `## Additional Context\n${argv.additional}`;
6420
+ }
6421
+ // Get commit history if requested
6422
+ let commit_history = '';
6423
+ if (argv.withPreviousCommits > 0) {
6424
+ const commitHistoryData = await getPreviousCommits({
6425
+ git,
6426
+ count: argv.withPreviousCommits
6427
+ });
6428
+ if (commitHistoryData) {
6429
+ commit_history = `## Commit History\n${commitHistoryData}`;
6430
+ }
6431
+ }
6306
6432
  const commitMsg = await executeChain({
6307
6433
  llm,
6308
6434
  prompt,
6309
6435
  variables: {
6310
6436
  summary: context,
6311
6437
  format_instructions: formatInstructions,
6312
- additional: additionalContext,
6438
+ additional_context: additional_context,
6439
+ commit_history: commit_history,
6313
6440
  },
6314
6441
  parser,
6315
6442
  });
@@ -6833,40 +6960,11 @@ const builder$1 = (yargs) => {
6833
6960
  return yargs.options(options$1).usage(getCommandUsageHeader(command$1));
6834
6961
  };
6835
6962
 
6836
- /**
6837
- * Formats a commit log into a readable string format.
6838
- *
6839
- * @param commitLog - The commit log result containing an array of commit details.
6840
- * @returns An array of formatted commit log strings.
6841
- *
6842
- * Each formatted string includes:
6843
- * - The date of the commit in square brackets.
6844
- * - The commit message.
6845
- * - The commit body.
6846
- * - The commit hash in parentheses.
6847
- * - The author's name and email in angle brackets.
6848
- */
6849
- const formatCommitLog = (commitLog) => {
6850
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6851
- };
6852
-
6853
6963
  const getChangesByTimestamp = async ({ since, git }) => {
6854
6964
  const commitLog = await git.log({ '--since': since });
6855
6965
  return formatCommitLog(commitLog);
6856
6966
  };
6857
6967
 
6858
- const getChangesSinceLastTag = async ({ git }) => {
6859
- const tags = await git.tags();
6860
- if (tags.all.length > 0) {
6861
- const lastTag = tags.latest;
6862
- const commitLog = await git.log({ from: lastTag });
6863
- return formatCommitLog(commitLog);
6864
- }
6865
- else {
6866
- return ['No tags found in the repository.'];
6867
- }
6868
- };
6869
-
6870
6968
  async function noResult$1({ logger }) {
6871
6969
  logger.log('No repo changes detected. 👀', { color: 'blue' });
6872
6970
  }
@@ -6876,12 +6974,12 @@ The summarization should descibe in a general sense what has changed in the repo
6876
6974
 
6877
6975
  Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
6878
6976
 
6879
- {timeframe}
6977
+ {{timeframe}}
6880
6978
 
6881
- {format_instructions}
6979
+ {{format_instructions}}
6882
6980
 
6883
- """{changes}"""`;
6884
- const inputVariables$1 = ['format_instructions', 'changes', 'timeframe'];
6981
+ """{{changes}}"""`;
6982
+ const inputVariables$1 = ['timeframe', 'format_instructions', 'changes'];
6885
6983
  const RECAP_PROMPT = new PromptTemplate({
6886
6984
  template: template$1,
6887
6985
  inputVariables: inputVariables$1,
@@ -6898,10 +6996,13 @@ const handler$1 = async (argv, logger) => {
6898
6996
  }
6899
6997
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6900
6998
  const llm = getLlm(provider, model, config);
6901
- const INTERACTIVE = isInteractive(config);
6999
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6902
7000
  if (INTERACTIVE) {
6903
7001
  logger.log(LOGO);
6904
7002
  }
7003
+ else {
7004
+ logger.setConfig({ silent: true });
7005
+ }
6905
7006
  const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
6906
7007
  const timeframe = lastMonth
6907
7008
  ? 'last-month'
@@ -6960,7 +7061,7 @@ const handler$1 = async (argv, logger) => {
6960
7061
  async function parser(changes) {
6961
7062
  return changes.join('\n');
6962
7063
  }
6963
- await generateAndReviewLoop({
7064
+ const recapResult = await generateAndReviewLoop({
6964
7065
  label: 'recap',
6965
7066
  options: {
6966
7067
  ...config,
@@ -6985,30 +7086,61 @@ const handler$1 = async (argv, logger) => {
6985
7086
  factory,
6986
7087
  parser,
6987
7088
  agent: async (context, options) => {
6988
- const parser = new JsonOutputParser();
6989
- const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
7089
+ const formatInstructions = 'Respond in a readable format. Include both high level and detailed information. Use markdown to format the response.';
6990
7090
  const prompt = getPrompt({
6991
7091
  template: options.prompt,
6992
7092
  variables: RECAP_PROMPT.inputVariables,
6993
7093
  fallback: RECAP_PROMPT,
6994
7094
  });
6995
- const response = await executeChain({
6996
- llm,
6997
- prompt,
6998
- variables: {
6999
- changes: context,
7000
- format_instructions: formatInstructions,
7001
- timeframe,
7002
- },
7003
- parser,
7004
- });
7005
- return `${response.summary || 'no response'}`;
7095
+ try {
7096
+ const stringParser = new StringOutputParser();
7097
+ const response = (await executeChain({
7098
+ llm,
7099
+ prompt,
7100
+ variables: {
7101
+ changes: context,
7102
+ format_instructions: formatInstructions,
7103
+ timeframe,
7104
+ },
7105
+ // NOTE: parser is not optional and JSONOutputParser is expected, however making a union type for `executeChain` breaks type generation downstream.
7106
+ // In the future, we should consider making the parser optional in `executeChain` and better handle parser types.
7107
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7108
+ // @ts-expect-error - parser is not optional and JSONOutputParser is expected
7109
+ parser: stringParser,
7110
+ }));
7111
+ return `${response || 'no response'}`;
7112
+ }
7113
+ catch (error) {
7114
+ const errorMessage = error instanceof Error ? error.message : String(error);
7115
+ // Log the error but don't exit
7116
+ logger.log(`Error parsing LLM response: ${errorMessage}`, { color: 'red' });
7117
+ // Always return a fallback message instead of exiting
7118
+ const fallbackMessage = `
7119
+ ## Failed to parse the response [timeframe: ${timeframe}]
7120
+ - There are changes in the codebase that couldn't be properly summarized due to a technical issue.
7121
+ - LLM encountered issues when parsing the changes.
7122
+
7123
+ ### Error encountered
7124
+
7125
+ ${errorMessage}
7126
+ `;
7127
+ return fallbackMessage;
7128
+ }
7006
7129
  },
7007
7130
  noResult: async () => {
7008
7131
  await noResult$1({ git, logger });
7009
7132
  process.exit(0);
7010
7133
  },
7011
7134
  });
7135
+ // Handle the result based on the mode (interactive or stdout)
7136
+ const MODE = (INTERACTIVE && 'interactive') || (config.recap && 'interactive') || config?.mode || 'stdout'; // Default to stdout
7137
+ handleResult({
7138
+ result: recapResult,
7139
+ interactiveModeCallback: async () => {
7140
+ logSuccess();
7141
+ },
7142
+ mode: MODE,
7143
+ });
7012
7144
  };
7013
7145
 
7014
7146
  var recap = {
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline$1);
77
77
  /**
78
78
  * Current build version from package.json
79
79
  */
80
- const BUILD_VERSION = "0.14.8";
80
+ const BUILD_VERSION = "0.14.9";
81
81
 
82
82
  const isInteractive = (config) => {
83
83
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1882,6 +1882,12 @@ const options$4 = {
1882
1882
  alias: 'b',
1883
1883
  description: 'Target branch to compare against',
1884
1884
  },
1885
+ sinceLastTag: {
1886
+ type: 'boolean',
1887
+ alias: 't',
1888
+ description: 'Generate changelog for all commits since the last tag',
1889
+ default: false,
1890
+ },
1885
1891
  i: {
1886
1892
  type: 'boolean',
1887
1893
  alias: 'interactive',
@@ -1934,6 +1940,7 @@ function getPrompt({ template, variables, fallback }) {
1934
1940
  ? new prompts$1.PromptTemplate({
1935
1941
  template,
1936
1942
  inputVariables: variables,
1943
+ templateFormat: 'mustache',
1937
1944
  })
1938
1945
  : fallback);
1939
1946
  }
@@ -1964,6 +1971,35 @@ function extractTicketIdFromBranchName(branchName) {
1964
1971
  return match ? match[0] : null;
1965
1972
  }
1966
1973
 
1974
+ /**
1975
+ * Formats a commit log into a readable string format.
1976
+ *
1977
+ * @param commitLog - The commit log result containing an array of commit details.
1978
+ * @returns An array of formatted commit log strings.
1979
+ *
1980
+ * Each formatted string includes:
1981
+ * - The date of the commit in square brackets.
1982
+ * - The commit message.
1983
+ * - The commit body.
1984
+ * - The commit hash in parentheses.
1985
+ * - The author's name and email in angle brackets.
1986
+ */
1987
+ const formatCommitLog = (commitLog) => {
1988
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
1989
+ };
1990
+
1991
+ const getChangesSinceLastTag = async ({ git }) => {
1992
+ const tags = await git.tags();
1993
+ if (tags.all.length > 0) {
1994
+ const lastTag = tags.latest;
1995
+ const commitLog = await git.log({ from: lastTag });
1996
+ return formatCommitLog(commitLog);
1997
+ }
1998
+ else {
1999
+ return ['No tags found in the repository.'];
2000
+ }
2001
+ };
2002
+
1967
2003
  /**
1968
2004
  * Retrieves the commit log range between two specified commits.
1969
2005
  *
@@ -2097,8 +2133,17 @@ const getRepo = () => {
2097
2133
  return git;
2098
2134
  };
2099
2135
 
2100
- const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2101
- 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.
2136
+ /**
2137
+ * Template for generating git commit messages based on code changes
2138
+ *
2139
+ * Variables:
2140
+ * - summary: Contains the diff summary of staged changes
2141
+ * - format_instructions: Instructions for the output format (JSON with title and body)
2142
+ * - additional_context: Optional user-provided context to guide the commit message generation
2143
+ * - commit_history: Optional history of previous commits for context
2144
+ */
2145
+ const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2146
+ 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.
2102
2147
 
2103
2148
  Please follow the guidelines below when writing your commit message:
2104
2149
 
@@ -2110,13 +2155,16 @@ Please follow the guidelines below when writing your commit message:
2110
2155
 
2111
2156
  {format_instructions}
2112
2157
 
2158
+ {commit_history}
2159
+
2113
2160
  """"""
2114
2161
  {summary}
2115
2162
  """"""
2116
2163
 
2117
- {additional}
2164
+ {additional_context}
2118
2165
  `;
2119
- const inputVariables$3 = ['summary', 'format_instructions', 'additional'];
2166
+ // Define the variables that will be passed to the prompt template
2167
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history'];
2120
2168
  const COMMIT_PROMPT = new prompts$1.PromptTemplate({
2121
2169
  template: template$4,
2122
2170
  inputVariables: inputVariables$3,
@@ -2282,9 +2330,16 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
2282
2330
  result = '';
2283
2331
  continue;
2284
2332
  }
2333
+ // Only edit the result in interactive mode if approved
2334
+ result = await editResult(result, options);
2335
+ }
2336
+ else {
2337
+ // In non-interactive mode, we return the result as is to be output to stdout by the caller.
2338
+ const displayResult = reviewParser ? reviewParser(result, options) : result;
2339
+ // In non-interactive mode, ensure we return the properly formatted result
2340
+ result = displayResult;
2285
2341
  }
2286
2342
  // if we're here, we're done.
2287
- result = await editResult(result, options);
2288
2343
  continueLoop = false;
2289
2344
  }
2290
2345
  return result;
@@ -2307,7 +2362,8 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
2307
2362
  break;
2308
2363
  case 'stdout':
2309
2364
  default:
2310
- process.stdout.write(result, 'utf8');
2365
+ // Ensure we write the result to stdout in non-interactive mode
2366
+ process.stdout.write(result + '\n', 'utf8');
2311
2367
  break;
2312
2368
  }
2313
2369
  process.exit(0);
@@ -2346,6 +2402,13 @@ const handler$4 = async (argv, logger) => {
2346
2402
  }
2347
2403
  async function factory() {
2348
2404
  const branchName = await getCurrentBranchName({ git });
2405
+ if (config.sinceLastTag) {
2406
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
2407
+ return {
2408
+ branch: branchName,
2409
+ commits: await getChangesSinceLastTag({ git, logger }),
2410
+ };
2411
+ }
2349
2412
  if (config.range && config.range.includes(':')) {
2350
2413
  const [from, to] = config.range.split(':');
2351
2414
  if (!from || !to) {
@@ -2364,7 +2427,7 @@ const handler$4 = async (argv, logger) => {
2364
2427
  commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
2365
2428
  };
2366
2429
  }
2367
- logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
2430
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, { color: 'yellow' });
2368
2431
  return {
2369
2432
  branch: branchName,
2370
2433
  commits: await getCommitLogCurrentBranch({ git, logger }),
@@ -2436,7 +2499,7 @@ const handler$4 = async (argv, logger) => {
2436
2499
 
2437
2500
  var changelog = {
2438
2501
  command: command$4,
2439
- desc: 'Generate a changelog from current or target branch or provided commit range.',
2502
+ desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
2440
2503
  builder: builder$4,
2441
2504
  handler: commandExecutor(handler$4),
2442
2505
  options: options$4,
@@ -2474,6 +2537,12 @@ const options$3 = {
2474
2537
  type: 'string',
2475
2538
  alias: 'a',
2476
2539
  },
2540
+ withPreviousCommits: {
2541
+ description: 'Include previous commits as context (specify number of commits, 0 for none)',
2542
+ type: 'number',
2543
+ default: 0,
2544
+ alias: 'p',
2545
+ },
2477
2546
  };
2478
2547
  const builder$3 = (yargs) => {
2479
2548
  return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
@@ -6211,6 +6280,48 @@ async function getChanges({ git, options }) {
6211
6280
  };
6212
6281
  }
6213
6282
 
6283
+ /**
6284
+ * Format a single commit log entry into a readable string
6285
+ * @param commit - The commit log entry
6286
+ * @returns Formatted commit log string
6287
+ */
6288
+ function formatSingleCommit(commit) {
6289
+ const { hash, date, message, body, author_name } = commit;
6290
+ const shortHash = hash.substring(0, 7);
6291
+ return `Commit: ${shortHash}
6292
+ Author: ${author_name}
6293
+ Date: ${date}
6294
+ Message: ${message}
6295
+ ${body ? `\nDetails: ${body}` : ''}`;
6296
+ }
6297
+
6298
+ /**
6299
+ * Get the specified number of previous commits
6300
+ * @param options - Options for getting previous commits
6301
+ * @returns Formatted commit logs
6302
+ */
6303
+ async function getPreviousCommits(options) {
6304
+ const { git, count = 1 } = options;
6305
+ if (count <= 0) {
6306
+ return '';
6307
+ }
6308
+ try {
6309
+ const logs = await git.log({ maxCount: count });
6310
+ if (!logs || logs.total === 0) {
6311
+ return '';
6312
+ }
6313
+ // Format the commit logs
6314
+ const formattedLogs = logs.all.map((commit) => {
6315
+ return formatSingleCommit(commit);
6316
+ }).join('\n\n');
6317
+ return formattedLogs;
6318
+ }
6319
+ catch (error) {
6320
+ console.error(`Error getting previous commits: ${error.message}`);
6321
+ return '';
6322
+ }
6323
+ }
6324
+
6214
6325
  /**
6215
6326
  * Retrieves a TikToken for the specified model.
6216
6327
  *
@@ -6324,14 +6435,30 @@ const handler$3 = async (argv, logger) => {
6324
6435
  fallback: COMMIT_PROMPT,
6325
6436
  });
6326
6437
  const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
6327
- const additionalContext = argv.additional ? `${argv.additional}` : '';
6438
+ // Get additional context if provided
6439
+ let additional_context = '';
6440
+ if (argv.additional) {
6441
+ additional_context = `## Additional Context\n${argv.additional}`;
6442
+ }
6443
+ // Get commit history if requested
6444
+ let commit_history = '';
6445
+ if (argv.withPreviousCommits > 0) {
6446
+ const commitHistoryData = await getPreviousCommits({
6447
+ git,
6448
+ count: argv.withPreviousCommits
6449
+ });
6450
+ if (commitHistoryData) {
6451
+ commit_history = `## Commit History\n${commitHistoryData}`;
6452
+ }
6453
+ }
6328
6454
  const commitMsg = await executeChain({
6329
6455
  llm,
6330
6456
  prompt,
6331
6457
  variables: {
6332
6458
  summary: context,
6333
6459
  format_instructions: formatInstructions,
6334
- additional: additionalContext,
6460
+ additional_context: additional_context,
6461
+ commit_history: commit_history,
6335
6462
  },
6336
6463
  parser,
6337
6464
  });
@@ -6855,40 +6982,11 @@ const builder$1 = (yargs) => {
6855
6982
  return yargs.options(options$1).usage(getCommandUsageHeader(command$1));
6856
6983
  };
6857
6984
 
6858
- /**
6859
- * Formats a commit log into a readable string format.
6860
- *
6861
- * @param commitLog - The commit log result containing an array of commit details.
6862
- * @returns An array of formatted commit log strings.
6863
- *
6864
- * Each formatted string includes:
6865
- * - The date of the commit in square brackets.
6866
- * - The commit message.
6867
- * - The commit body.
6868
- * - The commit hash in parentheses.
6869
- * - The author's name and email in angle brackets.
6870
- */
6871
- const formatCommitLog = (commitLog) => {
6872
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6873
- };
6874
-
6875
6985
  const getChangesByTimestamp = async ({ since, git }) => {
6876
6986
  const commitLog = await git.log({ '--since': since });
6877
6987
  return formatCommitLog(commitLog);
6878
6988
  };
6879
6989
 
6880
- const getChangesSinceLastTag = async ({ git }) => {
6881
- const tags = await git.tags();
6882
- if (tags.all.length > 0) {
6883
- const lastTag = tags.latest;
6884
- const commitLog = await git.log({ from: lastTag });
6885
- return formatCommitLog(commitLog);
6886
- }
6887
- else {
6888
- return ['No tags found in the repository.'];
6889
- }
6890
- };
6891
-
6892
6990
  async function noResult$1({ logger }) {
6893
6991
  logger.log('No repo changes detected. 👀', { color: 'blue' });
6894
6992
  }
@@ -6898,12 +6996,12 @@ The summarization should descibe in a general sense what has changed in the repo
6898
6996
 
6899
6997
  Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
6900
6998
 
6901
- {timeframe}
6999
+ {{timeframe}}
6902
7000
 
6903
- {format_instructions}
7001
+ {{format_instructions}}
6904
7002
 
6905
- """{changes}"""`;
6906
- const inputVariables$1 = ['format_instructions', 'changes', 'timeframe'];
7003
+ """{{changes}}"""`;
7004
+ const inputVariables$1 = ['timeframe', 'format_instructions', 'changes'];
6907
7005
  const RECAP_PROMPT = new prompts$1.PromptTemplate({
6908
7006
  template: template$1,
6909
7007
  inputVariables: inputVariables$1,
@@ -6920,10 +7018,13 @@ const handler$1 = async (argv, logger) => {
6920
7018
  }
6921
7019
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6922
7020
  const llm = getLlm(provider, model, config);
6923
- const INTERACTIVE = isInteractive(config);
7021
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6924
7022
  if (INTERACTIVE) {
6925
7023
  logger.log(LOGO);
6926
7024
  }
7025
+ else {
7026
+ logger.setConfig({ silent: true });
7027
+ }
6927
7028
  const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
6928
7029
  const timeframe = lastMonth
6929
7030
  ? 'last-month'
@@ -6982,7 +7083,7 @@ const handler$1 = async (argv, logger) => {
6982
7083
  async function parser(changes) {
6983
7084
  return changes.join('\n');
6984
7085
  }
6985
- await generateAndReviewLoop({
7086
+ const recapResult = await generateAndReviewLoop({
6986
7087
  label: 'recap',
6987
7088
  options: {
6988
7089
  ...config,
@@ -7007,30 +7108,61 @@ const handler$1 = async (argv, logger) => {
7007
7108
  factory,
7008
7109
  parser,
7009
7110
  agent: async (context, options) => {
7010
- const parser = new output_parsers.JsonOutputParser();
7011
- const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
7111
+ const formatInstructions = 'Respond in a readable format. Include both high level and detailed information. Use markdown to format the response.';
7012
7112
  const prompt = getPrompt({
7013
7113
  template: options.prompt,
7014
7114
  variables: RECAP_PROMPT.inputVariables,
7015
7115
  fallback: RECAP_PROMPT,
7016
7116
  });
7017
- const response = await executeChain({
7018
- llm,
7019
- prompt,
7020
- variables: {
7021
- changes: context,
7022
- format_instructions: formatInstructions,
7023
- timeframe,
7024
- },
7025
- parser,
7026
- });
7027
- return `${response.summary || 'no response'}`;
7117
+ try {
7118
+ const stringParser = new output_parsers.StringOutputParser();
7119
+ const response = (await executeChain({
7120
+ llm,
7121
+ prompt,
7122
+ variables: {
7123
+ changes: context,
7124
+ format_instructions: formatInstructions,
7125
+ timeframe,
7126
+ },
7127
+ // NOTE: parser is not optional and JSONOutputParser is expected, however making a union type for `executeChain` breaks type generation downstream.
7128
+ // In the future, we should consider making the parser optional in `executeChain` and better handle parser types.
7129
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7130
+ // @ts-expect-error - parser is not optional and JSONOutputParser is expected
7131
+ parser: stringParser,
7132
+ }));
7133
+ return `${response || 'no response'}`;
7134
+ }
7135
+ catch (error) {
7136
+ const errorMessage = error instanceof Error ? error.message : String(error);
7137
+ // Log the error but don't exit
7138
+ logger.log(`Error parsing LLM response: ${errorMessage}`, { color: 'red' });
7139
+ // Always return a fallback message instead of exiting
7140
+ const fallbackMessage = `
7141
+ ## Failed to parse the response [timeframe: ${timeframe}]
7142
+ - There are changes in the codebase that couldn't be properly summarized due to a technical issue.
7143
+ - LLM encountered issues when parsing the changes.
7144
+
7145
+ ### Error encountered
7146
+
7147
+ ${errorMessage}
7148
+ `;
7149
+ return fallbackMessage;
7150
+ }
7028
7151
  },
7029
7152
  noResult: async () => {
7030
7153
  await noResult$1({ git, logger });
7031
7154
  process.exit(0);
7032
7155
  },
7033
7156
  });
7157
+ // Handle the result based on the mode (interactive or stdout)
7158
+ const MODE = (INTERACTIVE && 'interactive') || (config.recap && 'interactive') || config?.mode || 'stdout'; // Default to stdout
7159
+ handleResult({
7160
+ result: recapResult,
7161
+ interactiveModeCallback: async () => {
7162
+ logSuccess();
7163
+ },
7164
+ mode: MODE,
7165
+ });
7034
7166
  };
7035
7167
 
7036
7168
  var recap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.14.8",
3
+ "version": "0.14.9",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -52,8 +52,8 @@
52
52
  "@types/async": "^3.2.20",
53
53
  "@types/chunk-text": "^1.0.0",
54
54
  "@types/common-tags": "^1.8.1",
55
- "@types/diff": "^5.0.3",
56
- "@types/ini": "^1.3.31",
55
+ "@types/diff": "^7.0.1",
56
+ "@types/ini": "^4.1.1",
57
57
  "@types/inquirer": "^9.0.3",
58
58
  "@types/jest": "^29.5.10",
59
59
  "@types/node": "^22.7.5",
@@ -93,8 +93,8 @@
93
93
  "ajv": "^8.16.0",
94
94
  "ajv-formats": "^3.0.1",
95
95
  "chalk": "4.1.2",
96
- "diff": "5.2.0",
97
- "ini": "4.1.1",
96
+ "diff": "7.0.0",
97
+ "ini": "5.0.0",
98
98
  "minimatch": "10.0.1",
99
99
  "ora": "5.4.1",
100
100
  "p-queue": "5.0.0",