git-coco 0.10.0 → 0.11.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.d.ts CHANGED
@@ -53,8 +53,6 @@ declare const _default$1: {
53
53
  commit: boolean;
54
54
  summarizePrompt: string;
55
55
  openInEditor: boolean;
56
- ignoredFiles: string[];
57
- ignoredExtensions: string[];
58
56
  interactive: boolean;
59
57
  help: boolean;
60
58
  verbose: boolean;
@@ -67,8 +65,6 @@ declare const _default$1: {
67
65
  commit: boolean;
68
66
  summarizePrompt: string;
69
67
  openInEditor: boolean;
70
- ignoredFiles: string[];
71
- ignoredExtensions: string[];
72
68
  interactive: boolean;
73
69
  help: boolean;
74
70
  verbose: boolean;
@@ -6,11 +6,11 @@ import * as fs from 'fs';
6
6
  import fs__default from 'fs';
7
7
  import { confirm, editor, select, password, input } from '@inquirer/prompts';
8
8
  import chalk from 'chalk';
9
+ import * as ini from 'ini';
9
10
  import * as os from 'os';
10
11
  import os__default from 'os';
11
12
  import * as path from 'path';
12
13
  import path__default from 'path';
13
- import * as ini from 'ini';
14
14
  import Ajv from 'ajv';
15
15
  import ora from 'ora';
16
16
  import now from 'performance-now';
@@ -21,7 +21,7 @@ import { RUN_KEY } from '@langchain/core/outputs';
21
21
  import { CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager';
22
22
  import { ensureConfig, Runnable } from '@langchain/core/runnables';
23
23
  import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
24
- import { BaseOutputParser } from '@langchain/core/output_parsers';
24
+ import { BaseOutputParser, JsonOutputParser } from '@langchain/core/output_parsers';
25
25
  import '@langchain/core/messages';
26
26
  import '@langchain/core/memory';
27
27
  import '@langchain/core/chat_history';
@@ -259,12 +259,35 @@ function loadEnvConfig(config) {
259
259
  if (envValue === undefined) {
260
260
  return;
261
261
  }
262
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
263
- // @ts-ignore
264
- envConfig[key] = envValue;
262
+ if (key === 'COCO_SERVICE_PROVIDER' || key === 'COCO_SERVICE_MODEL' || key === 'OPEN_AI_KEY') {
263
+ // NOTE: We want to ensure that the service object is always defined
264
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
265
+ // @ts-ignore
266
+ envConfig.service = envConfig.service || {};
267
+ handleServiceEnvVar(envConfig.service, key, envValue);
268
+ }
269
+ else {
270
+ envConfig[key] = envValue;
271
+ }
265
272
  });
266
273
  return { ...config, ...removeUndefined(envConfig) };
267
274
  }
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ function handleServiceEnvVar(service, key, value) {
277
+ switch (key) {
278
+ case 'COCO_SERVICE_PROVIDER':
279
+ service.provider = value;
280
+ break;
281
+ case 'COCO_SERVICE_MODEL':
282
+ service.model = value;
283
+ break;
284
+ case 'OPEN_AI_KEY':
285
+ if (service.provider === 'openai') {
286
+ service.fields = { apiKey: value };
287
+ }
288
+ break;
289
+ }
290
+ }
268
291
  function parseEnvValue(key, value) {
269
292
  switch (true) {
270
293
  // Handle undefined values
@@ -343,7 +366,12 @@ function loadGitConfig(config) {
343
366
  if (fs.existsSync(gitConfigPath)) {
344
367
  const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
345
368
  const gitConfigParsed = ini.parse(gitConfigRaw);
346
- const service = gitConfigParsed.coco?.service || config.service;
369
+ const gitServiceAlias = gitConfigParsed.coco?.service;
370
+ let service = config.service;
371
+ if (gitServiceAlias) {
372
+ const gitServiceConfig = getDefaultServiceConfigFromAlias(gitServiceAlias);
373
+ service = parseServiceConfig$1(gitServiceConfig || config.service);
374
+ }
347
375
  config = {
348
376
  ...config,
349
377
  service: service,
@@ -358,6 +386,28 @@ function loadGitConfig(config) {
358
386
  }
359
387
  return removeUndefined(config);
360
388
  }
389
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
+ function parseServiceConfig$1(service) {
391
+ if (!service)
392
+ return undefined;
393
+ switch (service.provider) {
394
+ case 'openai':
395
+ return {
396
+ provider: 'openai',
397
+ model: service.model,
398
+ fields: { apiKey: service.apiKey },
399
+ };
400
+ case 'ollama':
401
+ return {
402
+ provider: 'ollama',
403
+ model: service.model,
404
+ endpoint: service.endpoint,
405
+ fields: service.fields,
406
+ };
407
+ default:
408
+ return undefined;
409
+ }
410
+ }
361
411
  /**
362
412
  * Appends the provided configuration to a git config file.
363
413
  *
@@ -1732,10 +1782,42 @@ function loadXDGConfig(config) {
1732
1782
  const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
1733
1783
  if (fs.existsSync(xdgConfigPath)) {
1734
1784
  const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
1735
- config = { ...config, ...xdgConfig };
1785
+ const service = parseServiceConfig(xdgConfig.service || config.service);
1786
+ config = {
1787
+ ...config,
1788
+ ...xdgConfig,
1789
+ service: service
1790
+ };
1736
1791
  }
1737
1792
  return config;
1738
1793
  }
1794
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1795
+ function parseServiceConfig(service) {
1796
+ if (!service)
1797
+ return undefined;
1798
+ switch (service.provider) {
1799
+ case 'openai':
1800
+ return {
1801
+ provider: 'openai',
1802
+ model: service.model,
1803
+ authentication: {
1804
+ type: 'APIKey',
1805
+ credentials: {
1806
+ apiKey: service.apiKey
1807
+ }
1808
+ }
1809
+ };
1810
+ case 'ollama':
1811
+ return {
1812
+ provider: 'ollama',
1813
+ model: service.model,
1814
+ endpoint: service.endpoint,
1815
+ fields: service.fields
1816
+ };
1817
+ default:
1818
+ return undefined;
1819
+ }
1820
+ }
1739
1821
 
1740
1822
  /**
1741
1823
  * Load application config
@@ -5417,21 +5499,18 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
5417
5499
  }
5418
5500
 
5419
5501
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
5420
- 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. The detailed summary should be separated from the short description by a blank line. Please follow the guidelines below when writing your commit message:
5502
+ 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:
5421
5503
 
5422
5504
  - Write concisely using an informal tone
5423
5505
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
5424
5506
  - DO NOT use specific names or files from the code
5425
5507
  - DO NOT include any diffs or file changes in the commit message
5426
5508
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
5427
- - ONLY respond with the resulting commit message.
5428
5509
 
5429
- --------
5430
- {summary}
5431
- --------
5510
+ {format_instructions}
5432
5511
 
5433
- COMMIT MESSAGE:`;
5434
- const inputVariables$1 = ['summary'];
5512
+ """{summary}"""`;
5513
+ const inputVariables$1 = ['summary', 'format_instructions'];
5435
5514
  const COMMIT_PROMPT = new PromptTemplate({
5436
5515
  template: template$1,
5437
5516
  inputVariables: inputVariables$1,
@@ -5460,6 +5539,7 @@ function getLlm(provider, model, config) {
5460
5539
  const openAiModel = new ChatOpenAI({
5461
5540
  openAIApiKey: getApiKeyForModel(config),
5462
5541
  model,
5542
+ temperature: config.service.temperature || 0.2,
5463
5543
  });
5464
5544
  return openAiModel;
5465
5545
  }
@@ -5634,8 +5714,32 @@ async function noResult({ git, logger }) {
5634
5714
  }
5635
5715
  }
5636
5716
 
5637
- function logResult(label, result) {
5638
- console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
5717
+ /**
5718
+ * Verify template string contains all required input variables
5719
+ *
5720
+ * @param text template string
5721
+ * @param inputVariables template variables
5722
+ * @returns boolean or error message
5723
+ */
5724
+ function validatePromptTemplate(text, inputVariables) {
5725
+ if (!text) {
5726
+ return 'Prompt template cannot be empty';
5727
+ }
5728
+ if (!inputVariables.some((entry) => text.includes(entry))) {
5729
+ return ('Prompt template must include at least one of the following input variables: ' +
5730
+ inputVariables.map((value) => `{${value}}`).join(', '));
5731
+ }
5732
+ return true;
5733
+ }
5734
+
5735
+ async function editPrompt(options) {
5736
+ return await editor({
5737
+ message: 'Edit the prompt',
5738
+ default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
5739
+ waitForUseInput: false,
5740
+ postfix: 'Press ENTER to continue',
5741
+ validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
5742
+ });
5639
5743
  }
5640
5744
 
5641
5745
  async function editResult(result, options) {
@@ -5697,32 +5801,8 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
5697
5801
  }));
5698
5802
  }
5699
5803
 
5700
- /**
5701
- * Verify template string contains all required input variables
5702
- *
5703
- * @param text template string
5704
- * @param inputVariables template variables
5705
- * @returns boolean or error message
5706
- */
5707
- function validatePromptTemplate(text, inputVariables) {
5708
- if (!text) {
5709
- return 'Prompt template cannot be empty';
5710
- }
5711
- if (!inputVariables.some((entry) => text.includes(entry))) {
5712
- return ('Prompt template must include at least one of the following input variables: ' +
5713
- inputVariables.map((value) => `{${value}}`).join(', '));
5714
- }
5715
- return true;
5716
- }
5717
-
5718
- async function editPrompt(options) {
5719
- return await editor({
5720
- message: 'Edit the prompt',
5721
- default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
5722
- waitForUseInput: false,
5723
- postfix: 'Press ENTER to continue',
5724
- validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
5725
- });
5804
+ function logResult(label, result) {
5805
+ console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
5726
5806
  }
5727
5807
 
5728
5808
  async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
@@ -5733,7 +5813,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5733
5813
  let result = '';
5734
5814
  const changes = await factory();
5735
5815
  // if we don't have any changes, bail.
5736
- if (!changes || !changes.length) {
5816
+ if (!changes || !Object.keys(changes).length) {
5737
5817
  await noResult(options);
5738
5818
  }
5739
5819
  while (continueLoop) {
@@ -5768,7 +5848,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5768
5848
  logResult(label, result);
5769
5849
  const reviewAnswer = await getUserReviewDecision({
5770
5850
  label,
5771
- ...options?.review || {},
5851
+ ...(options?.review || {}),
5772
5852
  });
5773
5853
  if (reviewAnswer === 'cancel') {
5774
5854
  process.exit(0);
@@ -5800,11 +5880,11 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5800
5880
  return result;
5801
5881
  }
5802
5882
 
5803
- const executeChain = async ({ llm, prompt, variables }) => {
5883
+ const executeChain = async ({ llm, prompt, variables, parser, }) => {
5804
5884
  if (!llm || !prompt || !variables) {
5805
5885
  throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
5806
5886
  }
5807
- const chain = prompt.pipe(llm);
5887
+ const chain = prompt.pipe(llm).pipe(parser);
5808
5888
  let res;
5809
5889
  try {
5810
5890
  res = await chain.invoke(variables);
@@ -5817,7 +5897,7 @@ const executeChain = async ({ llm, prompt, variables }) => {
5817
5897
  if (!res) {
5818
5898
  throw new Error('Empty response from LLMChain call');
5819
5899
  }
5820
- return res.content;
5900
+ return res;
5821
5901
  };
5822
5902
 
5823
5903
  const logSuccess = () => {
@@ -5938,16 +6018,20 @@ const handler$2 = async (argv, logger) => {
5938
6018
  factory,
5939
6019
  parser,
5940
6020
  agent: async (context, options) => {
6021
+ const parser = new JsonOutputParser();
5941
6022
  const prompt = getPrompt({
5942
6023
  template: options.prompt,
5943
6024
  variables: COMMIT_PROMPT.inputVariables,
5944
6025
  fallback: COMMIT_PROMPT,
5945
6026
  });
5946
- return await executeChain({
6027
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body'.";
6028
+ const commitMsg = await executeChain({
5947
6029
  llm,
5948
6030
  prompt,
5949
- variables: { summary: context },
6031
+ variables: { summary: context, format_instructions: formatInstructions },
6032
+ parser,
5950
6033
  });
6034
+ return `${commitMsg.title}\n\n${commitMsg.body}`;
5951
6035
  },
5952
6036
  noResult: async () => {
5953
6037
  await noResult({ git, logger });
@@ -6020,20 +6104,6 @@ var commit = {
6020
6104
  options: options$2,
6021
6105
  };
6022
6106
 
6023
- const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
6024
-
6025
- - Typically a hyphen or asterisk is used for the bullet
6026
- - Summarize dependency updates
6027
-
6028
- """{summary}"""
6029
-
6030
- Changelog:`;
6031
- const inputVariables = ['summary'];
6032
- const CHANGELOG_PROMPT = new PromptTemplate({
6033
- template,
6034
- inputVariables,
6035
- });
6036
-
6037
6107
  /**
6038
6108
  * Retrieves the commit log range between two specified commits.
6039
6109
  *
@@ -6047,7 +6117,7 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
6047
6117
  try {
6048
6118
  const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
6049
6119
  const commitLog = await git.log(logOptions);
6050
- return commitLog.all.map(({ message, date, body, author_name }) => `[${date}] ${message}\n${body}\n - ${author_name}`);
6120
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n - ${author_name}<${author_email}> (${hash})`);
6051
6121
  }
6052
6122
  catch (error) {
6053
6123
  // If there's an error, handle it appropriately
@@ -6118,6 +6188,20 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
6118
6188
  return [];
6119
6189
  }
6120
6190
 
6191
+ const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
6192
+
6193
+ - Typically a hyphen or asterisk is used for the bullet
6194
+ - Summarize dependency updates
6195
+
6196
+ {format_instructions}
6197
+
6198
+ """{summary}"""`;
6199
+ const inputVariables = ['format_instructions', 'summary'];
6200
+ const CHANGELOG_PROMPT = new PromptTemplate({
6201
+ template,
6202
+ inputVariables,
6203
+ });
6204
+
6121
6205
  const handler$1 = async (argv, logger) => {
6122
6206
  const config = loadConfig(argv);
6123
6207
  const git = getRepo();
@@ -6133,19 +6217,27 @@ const handler$1 = async (argv, logger) => {
6133
6217
  logger.log(LOGO);
6134
6218
  }
6135
6219
  async function factory() {
6220
+ const branchName = await getCurrentBranchName({ git });
6136
6221
  if (config.range && config.range.includes(':')) {
6137
6222
  const [from, to] = config.range.split(':');
6138
6223
  if (!from || !to) {
6139
6224
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
6140
6225
  process.exit(1);
6141
6226
  }
6142
- return await getCommitLogRange(from, to, { git, noMerges: true });
6227
+ return {
6228
+ branch: branchName,
6229
+ commits: await getCommitLogRange(from, to, { git, noMerges: true }),
6230
+ };
6143
6231
  }
6144
6232
  logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
6145
- return await getCommitLogCurrentBranch({ git, logger });
6233
+ return {
6234
+ branch: branchName,
6235
+ commits: await getCommitLogCurrentBranch({ git, logger }),
6236
+ };
6146
6237
  }
6147
- async function parser(messages) {
6148
- const result = messages.join('\n');
6238
+ async function parser({ branch, commits }) {
6239
+ console.log({ branch, commits });
6240
+ const result = `## ${branch}\n\n${commits.map((commit) => `- ${commit}`).join('\n')}`;
6149
6241
  return result;
6150
6242
  }
6151
6243
  const changelogMsg = await generateAndReviewLoop({
@@ -6153,16 +6245,24 @@ const handler$1 = async (argv, logger) => {
6153
6245
  factory,
6154
6246
  parser,
6155
6247
  agent: async (context, options) => {
6248
+ const parser = new JsonOutputParser();
6156
6249
  const prompt = getPrompt({
6157
6250
  template: options.prompt,
6158
6251
  variables: CHANGELOG_PROMPT.inputVariables,
6159
6252
  fallback: CHANGELOG_PROMPT,
6160
6253
  });
6161
- return await executeChain({
6254
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content'.";
6255
+ const changelog = await executeChain({
6162
6256
  llm,
6163
6257
  prompt,
6164
- variables: { summary: context },
6258
+ variables: {
6259
+ summary: context,
6260
+ format_instructions: formatInstructions,
6261
+ },
6262
+ parser,
6165
6263
  });
6264
+ console.log({ changelog });
6265
+ return `${changelog.header}\n\n${changelog.content}`;
6166
6266
  },
6167
6267
  noResult: async () => {
6168
6268
  if (config.range) {
@@ -6201,17 +6301,6 @@ const options$1 = {
6201
6301
  alias: 'r',
6202
6302
  description: 'Commit range e.g `HEAD~2:HEAD`',
6203
6303
  },
6204
- model: { type: 'string', description: 'LLM/Model-Name' },
6205
- openAIApiKey: {
6206
- type: 'string',
6207
- description: 'OpenAI API Key',
6208
- conflicts: 'huggingFaceHubApiKey',
6209
- },
6210
- huggingFaceHubApiKey: {
6211
- type: 'string',
6212
- description: 'HuggingFace Hub API Key',
6213
- conflicts: 'openAIApiKey',
6214
- },
6215
6304
  tokenLimit: { type: 'number', description: 'Token limit' },
6216
6305
  prompt: {
6217
6306
  type: 'string',
@@ -6232,14 +6321,6 @@ const options$1 = {
6232
6321
  type: 'string',
6233
6322
  description: 'Prompt for summarizing large files',
6234
6323
  },
6235
- ignoredFiles: {
6236
- type: 'array',
6237
- description: 'Ignored files',
6238
- },
6239
- ignoredExtensions: {
6240
- type: 'array',
6241
- description: 'Ignored extensions',
6242
- },
6243
6324
  };
6244
6325
  const builder$1 = (yargs) => {
6245
6326
  return yargs.options(options$1);
package/dist/index.js CHANGED
@@ -7,9 +7,9 @@ var yargs = require('yargs');
7
7
  var fs = require('fs');
8
8
  var prompts$1 = require('@inquirer/prompts');
9
9
  var chalk = require('chalk');
10
+ var ini = require('ini');
10
11
  var os = require('os');
11
12
  var path = require('path');
12
- var ini = require('ini');
13
13
  var Ajv = require('ajv');
14
14
  var ora = require('ora');
15
15
  var now = require('performance-now');
@@ -54,9 +54,9 @@ function _interopNamespaceDefault(e) {
54
54
  }
55
55
 
56
56
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
57
+ var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
57
58
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
58
59
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
59
- var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
60
60
 
61
61
  /**
62
62
  * Returns a new object with all undefined keys removed
@@ -280,12 +280,35 @@ function loadEnvConfig(config) {
280
280
  if (envValue === undefined) {
281
281
  return;
282
282
  }
283
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
284
- // @ts-ignore
285
- envConfig[key] = envValue;
283
+ if (key === 'COCO_SERVICE_PROVIDER' || key === 'COCO_SERVICE_MODEL' || key === 'OPEN_AI_KEY') {
284
+ // NOTE: We want to ensure that the service object is always defined
285
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
286
+ // @ts-ignore
287
+ envConfig.service = envConfig.service || {};
288
+ handleServiceEnvVar(envConfig.service, key, envValue);
289
+ }
290
+ else {
291
+ envConfig[key] = envValue;
292
+ }
286
293
  });
287
294
  return { ...config, ...removeUndefined(envConfig) };
288
295
  }
296
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
297
+ function handleServiceEnvVar(service, key, value) {
298
+ switch (key) {
299
+ case 'COCO_SERVICE_PROVIDER':
300
+ service.provider = value;
301
+ break;
302
+ case 'COCO_SERVICE_MODEL':
303
+ service.model = value;
304
+ break;
305
+ case 'OPEN_AI_KEY':
306
+ if (service.provider === 'openai') {
307
+ service.fields = { apiKey: value };
308
+ }
309
+ break;
310
+ }
311
+ }
289
312
  function parseEnvValue(key, value) {
290
313
  switch (true) {
291
314
  // Handle undefined values
@@ -364,7 +387,12 @@ function loadGitConfig(config) {
364
387
  if (fs__namespace.existsSync(gitConfigPath)) {
365
388
  const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
366
389
  const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
367
- const service = gitConfigParsed.coco?.service || config.service;
390
+ const gitServiceAlias = gitConfigParsed.coco?.service;
391
+ let service = config.service;
392
+ if (gitServiceAlias) {
393
+ const gitServiceConfig = getDefaultServiceConfigFromAlias(gitServiceAlias);
394
+ service = parseServiceConfig$1(gitServiceConfig || config.service);
395
+ }
368
396
  config = {
369
397
  ...config,
370
398
  service: service,
@@ -379,6 +407,28 @@ function loadGitConfig(config) {
379
407
  }
380
408
  return removeUndefined(config);
381
409
  }
410
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
411
+ function parseServiceConfig$1(service) {
412
+ if (!service)
413
+ return undefined;
414
+ switch (service.provider) {
415
+ case 'openai':
416
+ return {
417
+ provider: 'openai',
418
+ model: service.model,
419
+ fields: { apiKey: service.apiKey },
420
+ };
421
+ case 'ollama':
422
+ return {
423
+ provider: 'ollama',
424
+ model: service.model,
425
+ endpoint: service.endpoint,
426
+ fields: service.fields,
427
+ };
428
+ default:
429
+ return undefined;
430
+ }
431
+ }
382
432
  /**
383
433
  * Appends the provided configuration to a git config file.
384
434
  *
@@ -1753,10 +1803,42 @@ function loadXDGConfig(config) {
1753
1803
  const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
1754
1804
  if (fs__namespace.existsSync(xdgConfigPath)) {
1755
1805
  const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
1756
- config = { ...config, ...xdgConfig };
1806
+ const service = parseServiceConfig(xdgConfig.service || config.service);
1807
+ config = {
1808
+ ...config,
1809
+ ...xdgConfig,
1810
+ service: service
1811
+ };
1757
1812
  }
1758
1813
  return config;
1759
1814
  }
1815
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1816
+ function parseServiceConfig(service) {
1817
+ if (!service)
1818
+ return undefined;
1819
+ switch (service.provider) {
1820
+ case 'openai':
1821
+ return {
1822
+ provider: 'openai',
1823
+ model: service.model,
1824
+ authentication: {
1825
+ type: 'APIKey',
1826
+ credentials: {
1827
+ apiKey: service.apiKey
1828
+ }
1829
+ }
1830
+ };
1831
+ case 'ollama':
1832
+ return {
1833
+ provider: 'ollama',
1834
+ model: service.model,
1835
+ endpoint: service.endpoint,
1836
+ fields: service.fields
1837
+ };
1838
+ default:
1839
+ return undefined;
1840
+ }
1841
+ }
1760
1842
 
1761
1843
  /**
1762
1844
  * Load application config
@@ -5438,21 +5520,18 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
5438
5520
  }
5439
5521
 
5440
5522
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
5441
- 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. The detailed summary should be separated from the short description by a blank line. Please follow the guidelines below when writing your commit message:
5523
+ 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:
5442
5524
 
5443
5525
  - Write concisely using an informal tone
5444
5526
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
5445
5527
  - DO NOT use specific names or files from the code
5446
5528
  - DO NOT include any diffs or file changes in the commit message
5447
5529
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
5448
- - ONLY respond with the resulting commit message.
5449
5530
 
5450
- --------
5451
- {summary}
5452
- --------
5531
+ {format_instructions}
5453
5532
 
5454
- COMMIT MESSAGE:`;
5455
- const inputVariables$1 = ['summary'];
5533
+ """{summary}"""`;
5534
+ const inputVariables$1 = ['summary', 'format_instructions'];
5456
5535
  const COMMIT_PROMPT = new prompts.PromptTemplate({
5457
5536
  template: template$1,
5458
5537
  inputVariables: inputVariables$1,
@@ -5481,6 +5560,7 @@ function getLlm(provider, model, config) {
5481
5560
  const openAiModel = new openai.ChatOpenAI({
5482
5561
  openAIApiKey: getApiKeyForModel(config),
5483
5562
  model,
5563
+ temperature: config.service.temperature || 0.2,
5484
5564
  });
5485
5565
  return openAiModel;
5486
5566
  }
@@ -5655,8 +5735,32 @@ async function noResult({ git, logger }) {
5655
5735
  }
5656
5736
  }
5657
5737
 
5658
- function logResult(label, result) {
5659
- console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
5738
+ /**
5739
+ * Verify template string contains all required input variables
5740
+ *
5741
+ * @param text template string
5742
+ * @param inputVariables template variables
5743
+ * @returns boolean or error message
5744
+ */
5745
+ function validatePromptTemplate(text, inputVariables) {
5746
+ if (!text) {
5747
+ return 'Prompt template cannot be empty';
5748
+ }
5749
+ if (!inputVariables.some((entry) => text.includes(entry))) {
5750
+ return ('Prompt template must include at least one of the following input variables: ' +
5751
+ inputVariables.map((value) => `{${value}}`).join(', '));
5752
+ }
5753
+ return true;
5754
+ }
5755
+
5756
+ async function editPrompt(options) {
5757
+ return await prompts$1.editor({
5758
+ message: 'Edit the prompt',
5759
+ default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
5760
+ waitForUseInput: false,
5761
+ postfix: 'Press ENTER to continue',
5762
+ validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
5763
+ });
5660
5764
  }
5661
5765
 
5662
5766
  async function editResult(result, options) {
@@ -5718,32 +5822,8 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
5718
5822
  }));
5719
5823
  }
5720
5824
 
5721
- /**
5722
- * Verify template string contains all required input variables
5723
- *
5724
- * @param text template string
5725
- * @param inputVariables template variables
5726
- * @returns boolean or error message
5727
- */
5728
- function validatePromptTemplate(text, inputVariables) {
5729
- if (!text) {
5730
- return 'Prompt template cannot be empty';
5731
- }
5732
- if (!inputVariables.some((entry) => text.includes(entry))) {
5733
- return ('Prompt template must include at least one of the following input variables: ' +
5734
- inputVariables.map((value) => `{${value}}`).join(', '));
5735
- }
5736
- return true;
5737
- }
5738
-
5739
- async function editPrompt(options) {
5740
- return await prompts$1.editor({
5741
- message: 'Edit the prompt',
5742
- default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
5743
- waitForUseInput: false,
5744
- postfix: 'Press ENTER to continue',
5745
- validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
5746
- });
5825
+ function logResult(label, result) {
5826
+ console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
5747
5827
  }
5748
5828
 
5749
5829
  async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
@@ -5754,7 +5834,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5754
5834
  let result = '';
5755
5835
  const changes = await factory();
5756
5836
  // if we don't have any changes, bail.
5757
- if (!changes || !changes.length) {
5837
+ if (!changes || !Object.keys(changes).length) {
5758
5838
  await noResult(options);
5759
5839
  }
5760
5840
  while (continueLoop) {
@@ -5789,7 +5869,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5789
5869
  logResult(label, result);
5790
5870
  const reviewAnswer = await getUserReviewDecision({
5791
5871
  label,
5792
- ...options?.review || {},
5872
+ ...(options?.review || {}),
5793
5873
  });
5794
5874
  if (reviewAnswer === 'cancel') {
5795
5875
  process.exit(0);
@@ -5821,11 +5901,11 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
5821
5901
  return result;
5822
5902
  }
5823
5903
 
5824
- const executeChain = async ({ llm, prompt, variables }) => {
5904
+ const executeChain = async ({ llm, prompt, variables, parser, }) => {
5825
5905
  if (!llm || !prompt || !variables) {
5826
5906
  throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
5827
5907
  }
5828
- const chain = prompt.pipe(llm);
5908
+ const chain = prompt.pipe(llm).pipe(parser);
5829
5909
  let res;
5830
5910
  try {
5831
5911
  res = await chain.invoke(variables);
@@ -5838,7 +5918,7 @@ const executeChain = async ({ llm, prompt, variables }) => {
5838
5918
  if (!res) {
5839
5919
  throw new Error('Empty response from LLMChain call');
5840
5920
  }
5841
- return res.content;
5921
+ return res;
5842
5922
  };
5843
5923
 
5844
5924
  const logSuccess = () => {
@@ -5959,16 +6039,20 @@ const handler$2 = async (argv, logger) => {
5959
6039
  factory,
5960
6040
  parser,
5961
6041
  agent: async (context, options) => {
6042
+ const parser = new output_parsers.JsonOutputParser();
5962
6043
  const prompt = getPrompt({
5963
6044
  template: options.prompt,
5964
6045
  variables: COMMIT_PROMPT.inputVariables,
5965
6046
  fallback: COMMIT_PROMPT,
5966
6047
  });
5967
- return await executeChain({
6048
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body'.";
6049
+ const commitMsg = await executeChain({
5968
6050
  llm,
5969
6051
  prompt,
5970
- variables: { summary: context },
6052
+ variables: { summary: context, format_instructions: formatInstructions },
6053
+ parser,
5971
6054
  });
6055
+ return `${commitMsg.title}\n\n${commitMsg.body}`;
5972
6056
  },
5973
6057
  noResult: async () => {
5974
6058
  await noResult({ git, logger });
@@ -6041,20 +6125,6 @@ var commit = {
6041
6125
  options: options$2,
6042
6126
  };
6043
6127
 
6044
- const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
6045
-
6046
- - Typically a hyphen or asterisk is used for the bullet
6047
- - Summarize dependency updates
6048
-
6049
- """{summary}"""
6050
-
6051
- Changelog:`;
6052
- const inputVariables = ['summary'];
6053
- const CHANGELOG_PROMPT = new prompts.PromptTemplate({
6054
- template,
6055
- inputVariables,
6056
- });
6057
-
6058
6128
  /**
6059
6129
  * Retrieves the commit log range between two specified commits.
6060
6130
  *
@@ -6068,7 +6138,7 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
6068
6138
  try {
6069
6139
  const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
6070
6140
  const commitLog = await git.log(logOptions);
6071
- return commitLog.all.map(({ message, date, body, author_name }) => `[${date}] ${message}\n${body}\n - ${author_name}`);
6141
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n - ${author_name}<${author_email}> (${hash})`);
6072
6142
  }
6073
6143
  catch (error) {
6074
6144
  // If there's an error, handle it appropriately
@@ -6139,6 +6209,20 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
6139
6209
  return [];
6140
6210
  }
6141
6211
 
6212
+ const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
6213
+
6214
+ - Typically a hyphen or asterisk is used for the bullet
6215
+ - 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
+
6142
6226
  const handler$1 = async (argv, logger) => {
6143
6227
  const config = loadConfig(argv);
6144
6228
  const git = getRepo();
@@ -6154,19 +6238,27 @@ const handler$1 = async (argv, logger) => {
6154
6238
  logger.log(LOGO);
6155
6239
  }
6156
6240
  async function factory() {
6241
+ const branchName = await getCurrentBranchName({ git });
6157
6242
  if (config.range && config.range.includes(':')) {
6158
6243
  const [from, to] = config.range.split(':');
6159
6244
  if (!from || !to) {
6160
6245
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
6161
6246
  process.exit(1);
6162
6247
  }
6163
- return await getCommitLogRange(from, to, { git, noMerges: true });
6248
+ return {
6249
+ branch: branchName,
6250
+ commits: await getCommitLogRange(from, to, { git, noMerges: true }),
6251
+ };
6164
6252
  }
6165
6253
  logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
6166
- return await getCommitLogCurrentBranch({ git, logger });
6254
+ return {
6255
+ branch: branchName,
6256
+ commits: await getCommitLogCurrentBranch({ git, logger }),
6257
+ };
6167
6258
  }
6168
- async function parser(messages) {
6169
- const result = messages.join('\n');
6259
+ async function parser({ branch, commits }) {
6260
+ console.log({ branch, commits });
6261
+ const result = `## ${branch}\n\n${commits.map((commit) => `- ${commit}`).join('\n')}`;
6170
6262
  return result;
6171
6263
  }
6172
6264
  const changelogMsg = await generateAndReviewLoop({
@@ -6174,16 +6266,24 @@ const handler$1 = async (argv, logger) => {
6174
6266
  factory,
6175
6267
  parser,
6176
6268
  agent: async (context, options) => {
6269
+ const parser = new output_parsers.JsonOutputParser();
6177
6270
  const prompt = getPrompt({
6178
6271
  template: options.prompt,
6179
6272
  variables: CHANGELOG_PROMPT.inputVariables,
6180
6273
  fallback: CHANGELOG_PROMPT,
6181
6274
  });
6182
- return await executeChain({
6275
+ const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content'.";
6276
+ const changelog = await executeChain({
6183
6277
  llm,
6184
6278
  prompt,
6185
- variables: { summary: context },
6279
+ variables: {
6280
+ summary: context,
6281
+ format_instructions: formatInstructions,
6282
+ },
6283
+ parser,
6186
6284
  });
6285
+ console.log({ changelog });
6286
+ return `${changelog.header}\n\n${changelog.content}`;
6187
6287
  },
6188
6288
  noResult: async () => {
6189
6289
  if (config.range) {
@@ -6222,17 +6322,6 @@ const options$1 = {
6222
6322
  alias: 'r',
6223
6323
  description: 'Commit range e.g `HEAD~2:HEAD`',
6224
6324
  },
6225
- model: { type: 'string', description: 'LLM/Model-Name' },
6226
- openAIApiKey: {
6227
- type: 'string',
6228
- description: 'OpenAI API Key',
6229
- conflicts: 'huggingFaceHubApiKey',
6230
- },
6231
- huggingFaceHubApiKey: {
6232
- type: 'string',
6233
- description: 'HuggingFace Hub API Key',
6234
- conflicts: 'openAIApiKey',
6235
- },
6236
6325
  tokenLimit: { type: 'number', description: 'Token limit' },
6237
6326
  prompt: {
6238
6327
  type: 'string',
@@ -6253,14 +6342,6 @@ const options$1 = {
6253
6342
  type: 'string',
6254
6343
  description: 'Prompt for summarizing large files',
6255
6344
  },
6256
- ignoredFiles: {
6257
- type: 'array',
6258
- description: 'Ignored files',
6259
- },
6260
- ignoredExtensions: {
6261
- type: 'array',
6262
- description: 'Ignored extensions',
6263
- },
6264
6345
  };
6265
6346
  const builder$1 = (yargs) => {
6266
6347
  return yargs.options(options$1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "chalk": "4.1.2",
93
93
  "diff": "5.2.0",
94
94
  "ini": "4.1.1",
95
- "minimatch": "9.0.4",
95
+ "minimatch": "10.0.1",
96
96
  "ora": "5.4.1",
97
97
  "p-queue": "5.0.0",
98
98
  "performance-now": "2.1.0",