git-coco 0.7.6 → 0.8.1

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
@@ -14,11 +14,11 @@ var now = require('performance-now');
14
14
  var prettyMilliseconds = require('pretty-ms');
15
15
  var pQueue = require('p-queue');
16
16
  var document = require('langchain/document');
17
- var hf = require('langchain/llms/hf');
18
17
  var chains = require('langchain/chains');
19
- var openai = require('langchain/llms/openai');
20
18
  var text_splitter = require('langchain/text_splitter');
21
19
  var diff = require('diff');
20
+ var ollama = require('langchain/llms/ollama');
21
+ var openai = require('langchain/llms/openai');
22
22
  var minimatch = require('minimatch');
23
23
  var simpleGit = require('simple-git');
24
24
  var tiktoken = require('tiktoken');
@@ -68,33 +68,34 @@ const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
68
68
  inputVariables: inputVariables$2,
69
69
  });
70
70
 
71
+ const DEFAULT_IGNORED_FILES = ['package-lock.json'];
72
+ const DEFAULT_IGNORED_EXTENSIONS = ['.map', '.lock'];
71
73
  /**
72
74
  * Default Config
73
75
  *
74
76
  * @type {Config}
75
77
  */
76
78
  const DEFAULT_CONFIG = {
77
- service: 'openai/gpt-4',
79
+ service: 'openai',
78
80
  verbose: false,
79
81
  tokenLimit: 1024,
80
82
  summarizePrompt: SUMMARIZE_PROMPT.template,
81
83
  temperature: 0.4,
82
84
  mode: 'stdout',
83
- ignoredFiles: ['package-lock.json'],
84
- ignoredExtensions: ['.map', '.lock'],
85
+ ignoredFiles: DEFAULT_IGNORED_FILES,
86
+ ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS,
85
87
  defaultBranch: 'main',
86
88
  };
87
89
  /**
88
90
  * Create a named export of all config keys for use in other modules.
89
91
  *
90
- * @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
92
+ * @see Used in `src/lib/config/services/env.ts` to validate all env vars.
91
93
  *
92
94
  * @type {string[]}
93
95
  */
94
96
  const CONFIG_KEYS = Object.keys({
95
97
  ...DEFAULT_CONFIG,
96
- huggingFaceHubApiKey: '',
97
- openAIApiKey: '',
98
+ endpoint: '',
98
99
  prompt: '',
99
100
  });
100
101
  const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
@@ -173,8 +174,9 @@ function loadEnvConfig(config) {
173
174
  CONFIG_KEYS.forEach((key) => {
174
175
  const envVarName = toEnvVarName(key);
175
176
  const envValue = parseEnvValue(key, process.env[envVarName]);
176
- if (envValue === undefined)
177
+ if (envValue === undefined) {
177
178
  return;
179
+ }
178
180
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
179
181
  // @ts-ignore
180
182
  envConfig[key] = envValue;
@@ -182,27 +184,28 @@ function loadEnvConfig(config) {
182
184
  return { ...config, ...removeUndefined(envConfig) };
183
185
  }
184
186
  function parseEnvValue(key, value) {
185
- if (value === undefined) {
186
- return undefined;
187
- }
188
- else if (key === 'tokenLimit' && typeof value === 'string') {
189
- return parseInt(value);
190
- }
191
- else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
192
- typeof value === 'string' &&
193
- value.includes(',')) {
194
- return value.split(',');
187
+ switch (true) {
188
+ // Handle undefined values
189
+ case value === undefined:
190
+ return undefined;
191
+ // Handle comma separated strings for ignoredFiles and ignoredExtensions arrays
192
+ case (key === 'ignoredFiles' || key === 'ignoredExtensions') &&
193
+ typeof value === 'string' &&
194
+ value.includes(','):
195
+ return value.split(',');
196
+ // Handle boolean values
197
+ case typeof value === 'string' && (value === 'false' || value === 'true'):
198
+ return value === 'true';
199
+ default:
200
+ return value;
195
201
  }
196
- return value;
197
202
  }
198
203
  function toEnvVarName(key) {
199
204
  switch (key) {
200
205
  case 'openAIApiKey':
201
206
  return 'OPENAI_API_KEY';
202
- case 'huggingFaceHubApiKey':
203
- return 'HUGGINGFACE_HUB_API_KEY';
204
207
  default:
205
- return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
208
+ return `COCO_${key.replace(/([A-Z])/g, '_$1').toLocaleUpperCase()}`;
206
209
  }
207
210
  }
208
211
  function formatEnvValue(value) {
@@ -246,13 +249,19 @@ function loadGitConfig(config) {
246
249
  const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
247
250
  config = {
248
251
  ...config,
249
- service: gitConfigParsed.coco?.model || config.service,
250
- openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
251
- huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
252
- tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
252
+ service: gitConfigParsed.coco?.service || config.service,
253
+ ...(config.service === 'ollama'
254
+ ? {
255
+ endpoint: gitConfigParsed.coco?.endpoint || config?.endpoint,
256
+ }
257
+ : {
258
+ openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config?.openAIApiKey,
259
+ }),
260
+ model: gitConfigParsed.coco?.model || config?.model,
261
+ temperature: gitConfigParsed.coco?.temperature || config?.temperature,
262
+ tokenLimit: gitConfigParsed.coco?.tokenLimit || config?.tokenLimit,
253
263
  prompt: gitConfigParsed.coco?.prompt || config.prompt,
254
264
  mode: gitConfigParsed.coco?.mode || config.mode,
255
- temperature: gitConfigParsed.coco?.temperature || config.temperature,
256
265
  summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
257
266
  ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
258
267
  ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
@@ -682,114 +691,49 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
682
691
  };
683
692
  }
684
693
 
685
- function getModelAndProviderFromService(service) {
686
- if (!service) {
687
- throw new Error(`Missing service`);
688
- }
689
- const [provider, model] = service.split(/\/(.*)/s);
690
- if (!model || !provider) {
691
- throw new Error(`Invalid service: ${service}`);
692
- }
693
- return { provider, model };
694
- }
695
- function getModelFromService(service) {
696
- const { model } = getModelAndProviderFromService(service);
697
- return model;
698
- }
699
- /**
700
- * Get LLM Model Based on Configuration
701
- * @param fields
702
- * @param configuration
703
- * @returns LLM Model
704
- */
705
- function getLlm(service, key, fields) {
706
- const { provider, model } = getModelAndProviderFromService(service);
707
- if (!model) {
708
- throw new Error(`Invalid LLM Service: ${service}`);
709
- }
710
- switch (provider) {
711
- case 'huggingface':
712
- return new hf.HuggingFaceInference({
713
- model: model,
714
- apiKey: key,
715
- maxConcurrency: 4,
716
- ...fields,
717
- });
718
- case 'openai':
719
- default:
720
- return new openai.OpenAI({
721
- openAIApiKey: key,
722
- modelName: model,
723
- ...fields,
724
- });
725
- }
726
- }
727
694
  /**
728
- * Retrieve appropriate API key based on selected model
729
- * @param service
695
+ * Get Summarization Chain
696
+ * @param model
730
697
  * @param options
731
698
  * @returns
732
699
  */
733
- function getApiKeyForModel(service, options) {
734
- const { provider } = getModelAndProviderFromService(service);
735
- switch (provider) {
736
- case 'huggingface':
737
- return options.huggingFaceHubApiKey;
738
- case 'openai':
739
- default:
740
- return options.openAIApiKey;
741
- }
700
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
701
+ return chains.loadSummarizationChain(model, options);
742
702
  }
703
+
743
704
  /**
744
705
  * Get Recursive Character Text Splitter
706
+ *
745
707
  * @param options
746
708
  * @returns
747
709
  */
748
710
  function getTextSplitter(options = {}) {
749
711
  return new text_splitter.RecursiveCharacterTextSplitter(options);
750
712
  }
713
+
751
714
  /**
752
- * Get Summarization Chain
753
- * @param model
754
- * @param options
755
- * @returns
756
- */
757
- function getSummarizationChain(model, options = { type: 'map_reduce' }) {
758
- return chains.loadSummarizationChain(model, options);
759
- }
760
- function getPrompt({ template, variables, fallback }) {
761
- if (!template && !fallback)
762
- throw new Error('Must provide either a template or a fallback');
763
- return (template
764
- ? new prompts.PromptTemplate({
765
- template,
766
- inputVariables: variables,
767
- })
768
- : fallback);
769
- }
770
- /**
771
- * Verify template string contains all required input variables
772
- * @param text template string
773
- * @param inputVariables template variables
774
- * @returns boolean or error message
715
+ * Parses the default file diff for a given nodeFile.
716
+ *
717
+ * @param nodeFile - The file change object.
718
+ * @param commit - The commit to diff against. Defaults to '--staged'.
719
+ * @param git - The SimpleGit instance.
720
+ * @returns A Promise that resolves to the file diff as a string.
775
721
  */
776
- function validatePromptTemplate(text, inputVariables) {
777
- if (!text) {
778
- return 'Prompt template cannot be empty';
779
- }
780
- if (!inputVariables.some((entry) => text.includes(entry))) {
781
- return ('Prompt template must include at least one of the following input variables: ' +
782
- inputVariables.map((value) => `{${value}}`).join(', '));
783
- }
784
- return true;
785
- }
786
-
787
722
  async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
788
723
  if (commit !== '--staged') {
789
724
  return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
790
725
  }
791
726
  return await git.diff([commit, nodeFile.filePath]);
792
727
  }
728
+ /**
729
+ * Parses the diff for a renamed file.
730
+ *
731
+ * @param nodeFile - The file change object.
732
+ * @param commit - The commit hash or '--staged'.
733
+ * @param git - The SimpleGit instance.
734
+ * @param logger - The logger instance.
735
+ * @returns A Promise that resolves to the diff string.
736
+ */
793
737
  async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
794
738
  let result = '';
795
739
  const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
@@ -828,6 +772,18 @@ async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
828
772
  }
829
773
  return result;
830
774
  }
775
+ /**
776
+ * Retrieves the diff for a given file change in a specific commit.
777
+ * If the file is deleted, it returns a message indicating that the file has been deleted.
778
+ * If the file is renamed, it parses the renamed file diff and returns it.
779
+ * Otherwise, it retrieves the default diff from the index and returns it.
780
+ *
781
+ * @param nodeFile - The file change object.
782
+ * @param commit - The commit hash.
783
+ * @param git - The SimpleGit instance.
784
+ * @param logger - The logger instance.
785
+ * @returns A promise that resolves to the diff as a string.
786
+ */
831
787
  async function getDiff(nodeFile, commit, { git, logger, }) {
832
788
  if (nodeFile.status === 'deleted') {
833
789
  return 'This file has been deleted.';
@@ -870,14 +826,14 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
870
826
  }
871
827
 
872
828
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
873
- Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
829
+ 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:
874
830
 
875
- - Typically a hyphen or asterisk is used for the bullet
876
831
  - Write concisely using an informal tone
877
832
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
878
- - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
879
833
  - DO NOT use specific names or files from the code
834
+ - DO NOT include any diffs or file changes in the commit message
880
835
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
836
+ - ONLY respond with the resulting commit message.
881
837
 
882
838
  """{summary}"""
883
839
 
@@ -888,6 +844,151 @@ const COMMIT_PROMPT = new prompts.PromptTemplate({
888
844
  inputVariables: inputVariables$1,
889
845
  });
890
846
 
847
+ const DEFAULT_OLLAMA_LLM_SERVICE = {
848
+ provider: 'ollama',
849
+ model: 'codellama',
850
+ endpoint: 'http://localhost:11434',
851
+ maxConcurrent: 1,
852
+ tokenLimit: 1024,
853
+ };
854
+ const DEFAULT_OPENAI_LLM_SERVICE = {
855
+ provider: 'openai',
856
+ model: 'gpt-4',
857
+ authentication: {
858
+ type: 'APIKey',
859
+ credentials: {
860
+ apiKey: '',
861
+ },
862
+ },
863
+ tokenLimit: 1024,
864
+ };
865
+
866
+ /**
867
+ * Retrieves the provider and model from the given configuration object.
868
+ * @param config The configuration object.
869
+ * @returns An object containing the provider and model.
870
+ * @throws Error if the configuration is invalid or missing required properties.
871
+ */
872
+ function getModelAndProviderFromConfig(config) {
873
+ if (!config.service) {
874
+ throw new Error('Invalid service: undefined');
875
+ }
876
+ let result;
877
+ switch (typeof config.service) {
878
+ case 'string':
879
+ result = getDefaultServiceConfigFromAlias(config.service, config?.model);
880
+ break;
881
+ case 'object':
882
+ default:
883
+ result = config.service;
884
+ break;
885
+ }
886
+ const { provider, model } = result;
887
+ if (!model || !provider) {
888
+ throw new Error(`Invalid service: ${config.service}`);
889
+ }
890
+ return { provider, model };
891
+ }
892
+ /**
893
+ * Retrieve appropriate API key based on selected model
894
+ * @param service
895
+ * @param options
896
+ * @returns API Key
897
+ */
898
+ function getApiKeyForModel(config) {
899
+ const { provider } = getModelAndProviderFromConfig(config);
900
+ switch (provider) {
901
+ case 'openai':
902
+ return config.openAIApiKey || getDefaultServiceApiKey(config);
903
+ default:
904
+ return getDefaultServiceApiKey(config);
905
+ }
906
+ }
907
+ /**
908
+ * Retrieves the default service API key from the given configuration.
909
+ * @param config The configuration object.
910
+ * @returns The default service API key.
911
+ */
912
+ function getDefaultServiceApiKey(config) {
913
+ const service = config.service;
914
+ if (service.authentication.type === 'APIKey') {
915
+ return service.authentication.credentials?.apiKey;
916
+ }
917
+ else if (service.authentication.type === 'OAuth') {
918
+ return service.authentication.credentials?.token;
919
+ }
920
+ return '';
921
+ }
922
+ /**
923
+ * Retrieves the default service configuration based on the provided alias and optional model.
924
+ * @param alias - The alias of the service.
925
+ * @param model - The optional model to be used.
926
+ * @returns The default service configuration.
927
+ * @throws Error if the alias is invalid or undefined.
928
+ */
929
+ function getDefaultServiceConfigFromAlias(alias, model) {
930
+ if (!alias) {
931
+ throw new Error('Invalid alias: undefined');
932
+ }
933
+ switch (alias) {
934
+ case 'openai':
935
+ return {
936
+ ...DEFAULT_OPENAI_LLM_SERVICE,
937
+ model: model || DEFAULT_OPENAI_LLM_SERVICE.model,
938
+ };
939
+ case 'ollama':
940
+ return {
941
+ ...DEFAULT_OLLAMA_LLM_SERVICE,
942
+ model: model || DEFAULT_OLLAMA_LLM_SERVICE.model,
943
+ };
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Get LLM Model Based on Configuration
949
+ *
950
+ * @param fields
951
+ * @param configuration
952
+ * @returns LLM Model
953
+ */
954
+ function getLlm(provider, model, config) {
955
+ if (!model) {
956
+ throw new Error(`Invalid LLM Service: ${provider}/${model}`);
957
+ }
958
+ switch (provider) {
959
+ case 'ollama':
960
+ return new ollama.Ollama({
961
+ baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
962
+ model,
963
+ });
964
+ case 'openai':
965
+ default:
966
+ return new openai.OpenAI({
967
+ openAIApiKey: config.openAIApiKey,
968
+ modelName: model,
969
+ });
970
+ }
971
+ }
972
+
973
+ function getPrompt({ template, variables, fallback }) {
974
+ if (!template && !fallback)
975
+ throw new Error('Must provide either a template or a fallback');
976
+ return (template
977
+ ? new prompts.PromptTemplate({
978
+ template,
979
+ inputVariables: variables,
980
+ })
981
+ : fallback);
982
+ }
983
+
984
+ /**
985
+ * Determines the status of a file based on its changes in the Git repository.
986
+ *
987
+ * @param file - The file to check the status of.
988
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
989
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
990
+ * @throws Error if the file type is invalid.
991
+ */
891
992
  function getStatus(file, location = 'index') {
892
993
  if ('index' in file && 'working_dir' in file) {
893
994
  const statusCode = file[location];
@@ -924,6 +1025,14 @@ function getStatus(file, location = 'index') {
924
1025
  }
925
1026
  }
926
1027
 
1028
+ /**
1029
+ * Returns the summary text for a file change.
1030
+ *
1031
+ * @param file - The file status or diff result.
1032
+ * @param change - The partial file change object.
1033
+ * @returns The summary text for the file change.
1034
+ * @throws Error if the file type is invalid.
1035
+ */
927
1036
  function getSummaryText(file, change) {
928
1037
  const status = change.status || getStatus(file);
929
1038
  let filePath;
@@ -942,9 +1051,12 @@ function getSummaryText(file, change) {
942
1051
  return `${status}: ${filePath}`;
943
1052
  }
944
1053
 
945
- const config = loadConfig();
946
- const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
947
- const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
1054
+ /**
1055
+ * Retrieves the changes in the Git repository.
1056
+ *
1057
+ * @param {GetChangesInput} options - The options for retrieving the changes.
1058
+ * @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
1059
+ */
948
1060
  async function getChanges({ git, options }) {
949
1061
  const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
950
1062
  const staged = [];
@@ -1010,13 +1122,13 @@ async function noResult({ git, logger }) {
1010
1122
  else if (hasUnstaged || hasUntracked) {
1011
1123
  logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
1012
1124
  if (hasUnstaged) {
1013
- logger.log('\nDetected unstaged changes', { color: 'yellow' });
1125
+ logger.log('\nChanges not staged for commit:', { color: 'yellow' });
1014
1126
  logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
1015
1127
  color: 'red',
1016
1128
  });
1017
1129
  }
1018
1130
  if (hasUntracked) {
1019
- logger.log('\nDetected untracked changes', { color: 'yellow' });
1131
+ logger.log('\nUntracked changes:', { color: 'yellow' });
1020
1132
  logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
1021
1133
  color: 'red',
1022
1134
  });
@@ -1090,6 +1202,24 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
1090
1202
  }));
1091
1203
  }
1092
1204
 
1205
+ /**
1206
+ * Verify template string contains all required input variables
1207
+ *
1208
+ * @param text template string
1209
+ * @param inputVariables template variables
1210
+ * @returns boolean or error message
1211
+ */
1212
+ function validatePromptTemplate(text, inputVariables) {
1213
+ if (!text) {
1214
+ return 'Prompt template cannot be empty';
1215
+ }
1216
+ if (!inputVariables.some((entry) => text.includes(entry))) {
1217
+ return ('Prompt template must include at least one of the following input variables: ' +
1218
+ inputVariables.map((value) => `{${value}}`).join(', '));
1219
+ }
1220
+ return true;
1221
+ }
1222
+
1093
1223
  async function editPrompt(options) {
1094
1224
  return await prompts$1.editor({
1095
1225
  message: 'Edit the prompt',
@@ -1220,6 +1350,10 @@ async function handleResult({ result, mode, interactiveHandler }) {
1220
1350
  process.exit(0);
1221
1351
  }
1222
1352
 
1353
+ /**
1354
+ * Retrieves the SimpleGit instance for the repository.
1355
+ * @returns {SimpleGit} The SimpleGit instance.
1356
+ */
1223
1357
  const getRepo = () => {
1224
1358
  let git;
1225
1359
  try {
@@ -1232,34 +1366,50 @@ const getRepo = () => {
1232
1366
  return git;
1233
1367
  };
1234
1368
 
1369
+ /**
1370
+ * Retrieves a TikToken for the specified model.
1371
+ *
1372
+ * @param {TiktokenModel} modelName - The name of the TiktokenModel.
1373
+ * @returns A Promise that resolves to the TikToken.
1374
+ */
1235
1375
  const getTikToken = async (modelName) => {
1236
1376
  return await tiktoken.encoding_for_model(modelName);
1237
1377
  };
1238
- const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1239
- // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1240
- const tokens = tokenizer.encode(text);
1241
- // console.log('Tokens', { tokenCount: tokens.length })
1242
- return tokens.length;
1243
- });
1378
+ /**
1379
+ * Retrieves the token counter for a given model name.
1380
+ *
1381
+ * @param {TikTokenModel} modelName - The name of the Tiktoken model.
1382
+ * @returns A promise that resolves to a function that calculates the number of tokens in a given text.
1383
+ */
1384
+ const getTokenCounter = async (modelName) => {
1385
+ return getTikToken(modelName).then((tokenizer) => (text) => {
1386
+ const tokens = tokenizer.encode(text);
1387
+ return tokens.length;
1388
+ });
1389
+ };
1244
1390
 
1245
- async function createCommit(commitMsg, git) {
1246
- return await git.commit(commitMsg);
1391
+ /**
1392
+ * Creates a commit with the specified commit message.
1393
+ *
1394
+ * @param message The commit message.
1395
+ * @param git The SimpleGit instance.
1396
+ * @returns A Promise that resolves to the CommitResult.
1397
+ */
1398
+ async function createCommit(message, git) {
1399
+ return await git.commit(message);
1247
1400
  }
1248
1401
 
1249
1402
  const handler$2 = async (argv, logger) => {
1250
1403
  const git = getRepo();
1251
1404
  const options = loadConfig(argv);
1252
- const { service } = options;
1253
- const key = getApiKeyForModel(service, options);
1254
- const tokenizer = await getTokenCounter(getModelFromService(service));
1405
+ const key = getApiKeyForModel(options);
1255
1406
  if (!key) {
1256
1407
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1257
1408
  process.exit(1);
1258
1409
  }
1259
- const llm = getLlm(service, key, {
1260
- temperature: 0.4,
1261
- maxConcurrency: 10,
1262
- });
1410
+ const { provider, model } = getModelAndProviderFromConfig(options);
1411
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
1412
+ const llm = getLlm(provider, model, options);
1263
1413
  const INTERACTIVE = isInteractive(options);
1264
1414
  if (INTERACTIVE) {
1265
1415
  logger.log(LOGO);
@@ -1325,16 +1475,10 @@ const handler$2 = async (argv, logger) => {
1325
1475
  * Command line options via yargs
1326
1476
  */
1327
1477
  const options$2 = {
1328
- model: { type: 'string', description: 'LLM/Model-Name' },
1478
+ service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
1329
1479
  openAIApiKey: {
1330
1480
  type: 'string',
1331
1481
  description: 'OpenAI API Key',
1332
- conflicts: 'huggingFaceHubApiKey',
1333
- },
1334
- huggingFaceHubApiKey: {
1335
- type: 'string',
1336
- description: 'HuggingFace Hub API Key',
1337
- conflicts: 'openAIApiKey',
1338
1482
  },
1339
1483
  tokenLimit: { type: 'number', description: 'Token limit' },
1340
1484
  prompt: {
@@ -1396,6 +1540,15 @@ const CHANGELOG_PROMPT = new prompts.PromptTemplate({
1396
1540
  inputVariables,
1397
1541
  });
1398
1542
 
1543
+ /**
1544
+ * Retrieves the commit log range between two specified commits.
1545
+ *
1546
+ * @param from - The starting commit.
1547
+ * @param to - The ending commit.
1548
+ * @param options - Additional options for retrieving the commit log range.
1549
+ * @returns A promise that resolves to an array of commit log messages.
1550
+ * @throws If there is an error retrieving the commit log range.
1551
+ */
1399
1552
  async function getCommitLogRange(from, to, { noMerges, git }) {
1400
1553
  try {
1401
1554
  const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
@@ -1409,10 +1562,26 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
1409
1562
  }
1410
1563
  }
1411
1564
 
1565
+ /**
1566
+ * Retrieves the name of the current branch.
1567
+ *
1568
+ * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
1569
+ * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
1570
+ */
1412
1571
  async function getCurrentBranchName({ git }) {
1413
1572
  return await git.revparse(['--abbrev-ref', 'HEAD']);
1414
1573
  }
1415
1574
 
1575
+ /**
1576
+ * Retrieves the commit log for the current branch.
1577
+ *
1578
+ * @param {Object} options - The options for retrieving the commit log.
1579
+ * @param {SimpleGit} options.git - The SimpleGit instance.
1580
+ * @param {Logger} options.logger - The logger for logging messages.
1581
+ * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
1582
+ * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
1583
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
1584
+ */
1416
1585
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
1417
1586
  try {
1418
1587
  // Get the current branch name
@@ -1458,15 +1627,13 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
1458
1627
  const handler$1 = async (argv, logger) => {
1459
1628
  const options = loadConfig(argv);
1460
1629
  const git = getRepo();
1461
- const key = getApiKeyForModel(options.service, options);
1630
+ const key = getApiKeyForModel(options);
1462
1631
  if (!key) {
1463
1632
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1464
1633
  process.exit(1);
1465
1634
  }
1466
- const model = getLlm(options.service, key, {
1467
- temperature: 0.4,
1468
- maxConcurrency: 10,
1469
- });
1635
+ const { provider, model } = getModelAndProviderFromConfig(options);
1636
+ const llm = getLlm(provider, model, options);
1470
1637
  const INTERACTIVE = isInteractive(options);
1471
1638
  if (INTERACTIVE) {
1472
1639
  logger.log(LOGO);
@@ -1498,7 +1665,7 @@ const handler$1 = async (argv, logger) => {
1498
1665
  fallback: CHANGELOG_PROMPT,
1499
1666
  });
1500
1667
  return await executeChain({
1501
- llm: model,
1668
+ llm,
1502
1669
  prompt,
1503
1670
  variables: { summary: context },
1504
1671
  });
@@ -1655,14 +1822,10 @@ async function installNpmPackage({ name, flags = [], cwd = process.cwd(), }) {
1655
1822
  */
1656
1823
  function isPackageInstalled(packageName, projectPath) {
1657
1824
  try {
1658
- // Construct the path to the package.json file
1659
1825
  const packageJsonPath = path.join(projectPath, 'package.json');
1660
- // Read the package.json file
1661
1826
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1662
- // Check both dependencies and devDependencies
1663
1827
  const dependencies = packageJson.dependencies || {};
1664
1828
  const devDependencies = packageJson.devDependencies || {};
1665
- // Return true if the package is found in either
1666
1829
  return dependencies.hasOwnProperty(packageName) || devDependencies.hasOwnProperty(packageName);
1667
1830
  }
1668
1831
  catch (error) {