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.
@@ -15,11 +15,11 @@ import now from 'performance-now';
15
15
  import prettyMilliseconds from 'pretty-ms';
16
16
  import pQueue from 'p-queue';
17
17
  import { Document } from 'langchain/document';
18
- import { HuggingFaceInference } from 'langchain/llms/hf';
19
18
  import { loadSummarizationChain, LLMChain } from 'langchain/chains';
20
- import { OpenAI } from 'langchain/llms/openai';
21
19
  import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
22
20
  import { createTwoFilesPatch } from 'diff';
21
+ import { Ollama } from 'langchain/llms/ollama';
22
+ import { OpenAI } from 'langchain/llms/openai';
23
23
  import { minimatch } from 'minimatch';
24
24
  import { simpleGit } from 'simple-git';
25
25
  import { encoding_for_model } from 'tiktoken';
@@ -47,33 +47,34 @@ const SUMMARIZE_PROMPT = new PromptTemplate({
47
47
  inputVariables: inputVariables$2,
48
48
  });
49
49
 
50
+ const DEFAULT_IGNORED_FILES = ['package-lock.json'];
51
+ const DEFAULT_IGNORED_EXTENSIONS = ['.map', '.lock'];
50
52
  /**
51
53
  * Default Config
52
54
  *
53
55
  * @type {Config}
54
56
  */
55
57
  const DEFAULT_CONFIG = {
56
- service: 'openai/gpt-4',
58
+ service: 'openai',
57
59
  verbose: false,
58
60
  tokenLimit: 1024,
59
61
  summarizePrompt: SUMMARIZE_PROMPT.template,
60
62
  temperature: 0.4,
61
63
  mode: 'stdout',
62
- ignoredFiles: ['package-lock.json'],
63
- ignoredExtensions: ['.map', '.lock'],
64
+ ignoredFiles: DEFAULT_IGNORED_FILES,
65
+ ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS,
64
66
  defaultBranch: 'main',
65
67
  };
66
68
  /**
67
69
  * Create a named export of all config keys for use in other modules.
68
70
  *
69
- * @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
71
+ * @see Used in `src/lib/config/services/env.ts` to validate all env vars.
70
72
  *
71
73
  * @type {string[]}
72
74
  */
73
75
  const CONFIG_KEYS = Object.keys({
74
76
  ...DEFAULT_CONFIG,
75
- huggingFaceHubApiKey: '',
76
- openAIApiKey: '',
77
+ endpoint: '',
77
78
  prompt: '',
78
79
  });
79
80
  const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
@@ -152,8 +153,9 @@ function loadEnvConfig(config) {
152
153
  CONFIG_KEYS.forEach((key) => {
153
154
  const envVarName = toEnvVarName(key);
154
155
  const envValue = parseEnvValue(key, process.env[envVarName]);
155
- if (envValue === undefined)
156
+ if (envValue === undefined) {
156
157
  return;
158
+ }
157
159
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
158
160
  // @ts-ignore
159
161
  envConfig[key] = envValue;
@@ -161,27 +163,28 @@ function loadEnvConfig(config) {
161
163
  return { ...config, ...removeUndefined(envConfig) };
162
164
  }
163
165
  function parseEnvValue(key, value) {
164
- if (value === undefined) {
165
- return undefined;
166
- }
167
- else if (key === 'tokenLimit' && typeof value === 'string') {
168
- return parseInt(value);
169
- }
170
- else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
171
- typeof value === 'string' &&
172
- value.includes(',')) {
173
- return value.split(',');
166
+ switch (true) {
167
+ // Handle undefined values
168
+ case value === undefined:
169
+ return undefined;
170
+ // Handle comma separated strings for ignoredFiles and ignoredExtensions arrays
171
+ case (key === 'ignoredFiles' || key === 'ignoredExtensions') &&
172
+ typeof value === 'string' &&
173
+ value.includes(','):
174
+ return value.split(',');
175
+ // Handle boolean values
176
+ case typeof value === 'string' && (value === 'false' || value === 'true'):
177
+ return value === 'true';
178
+ default:
179
+ return value;
174
180
  }
175
- return value;
176
181
  }
177
182
  function toEnvVarName(key) {
178
183
  switch (key) {
179
184
  case 'openAIApiKey':
180
185
  return 'OPENAI_API_KEY';
181
- case 'huggingFaceHubApiKey':
182
- return 'HUGGINGFACE_HUB_API_KEY';
183
186
  default:
184
- return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
187
+ return `COCO_${key.replace(/([A-Z])/g, '_$1').toLocaleUpperCase()}`;
185
188
  }
186
189
  }
187
190
  function formatEnvValue(value) {
@@ -225,13 +228,19 @@ function loadGitConfig(config) {
225
228
  const gitConfigParsed = ini.parse(gitConfigRaw);
226
229
  config = {
227
230
  ...config,
228
- service: gitConfigParsed.coco?.model || config.service,
229
- openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
230
- huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
231
- tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
231
+ service: gitConfigParsed.coco?.service || config.service,
232
+ ...(config.service === 'ollama'
233
+ ? {
234
+ endpoint: gitConfigParsed.coco?.endpoint || config?.endpoint,
235
+ }
236
+ : {
237
+ openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config?.openAIApiKey,
238
+ }),
239
+ model: gitConfigParsed.coco?.model || config?.model,
240
+ temperature: gitConfigParsed.coco?.temperature || config?.temperature,
241
+ tokenLimit: gitConfigParsed.coco?.tokenLimit || config?.tokenLimit,
232
242
  prompt: gitConfigParsed.coco?.prompt || config.prompt,
233
243
  mode: gitConfigParsed.coco?.mode || config.mode,
234
- temperature: gitConfigParsed.coco?.temperature || config.temperature,
235
244
  summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
236
245
  ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
237
246
  ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
@@ -661,114 +670,49 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
661
670
  };
662
671
  }
663
672
 
664
- function getModelAndProviderFromService(service) {
665
- if (!service) {
666
- throw new Error(`Missing service`);
667
- }
668
- const [provider, model] = service.split(/\/(.*)/s);
669
- if (!model || !provider) {
670
- throw new Error(`Invalid service: ${service}`);
671
- }
672
- return { provider, model };
673
- }
674
- function getModelFromService(service) {
675
- const { model } = getModelAndProviderFromService(service);
676
- return model;
677
- }
678
- /**
679
- * Get LLM Model Based on Configuration
680
- * @param fields
681
- * @param configuration
682
- * @returns LLM Model
683
- */
684
- function getLlm(service, key, fields) {
685
- const { provider, model } = getModelAndProviderFromService(service);
686
- if (!model) {
687
- throw new Error(`Invalid LLM Service: ${service}`);
688
- }
689
- switch (provider) {
690
- case 'huggingface':
691
- return new HuggingFaceInference({
692
- model: model,
693
- apiKey: key,
694
- maxConcurrency: 4,
695
- ...fields,
696
- });
697
- case 'openai':
698
- default:
699
- return new OpenAI({
700
- openAIApiKey: key,
701
- modelName: model,
702
- ...fields,
703
- });
704
- }
705
- }
706
673
  /**
707
- * Retrieve appropriate API key based on selected model
708
- * @param service
674
+ * Get Summarization Chain
675
+ * @param model
709
676
  * @param options
710
677
  * @returns
711
678
  */
712
- function getApiKeyForModel(service, options) {
713
- const { provider } = getModelAndProviderFromService(service);
714
- switch (provider) {
715
- case 'huggingface':
716
- return options.huggingFaceHubApiKey;
717
- case 'openai':
718
- default:
719
- return options.openAIApiKey;
720
- }
679
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
680
+ return loadSummarizationChain(model, options);
721
681
  }
682
+
722
683
  /**
723
684
  * Get Recursive Character Text Splitter
685
+ *
724
686
  * @param options
725
687
  * @returns
726
688
  */
727
689
  function getTextSplitter(options = {}) {
728
690
  return new RecursiveCharacterTextSplitter(options);
729
691
  }
692
+
730
693
  /**
731
- * Get Summarization Chain
732
- * @param model
733
- * @param options
734
- * @returns
735
- */
736
- function getSummarizationChain(model, options = { type: 'map_reduce' }) {
737
- return loadSummarizationChain(model, options);
738
- }
739
- function getPrompt({ template, variables, fallback }) {
740
- if (!template && !fallback)
741
- throw new Error('Must provide either a template or a fallback');
742
- return (template
743
- ? new PromptTemplate({
744
- template,
745
- inputVariables: variables,
746
- })
747
- : fallback);
748
- }
749
- /**
750
- * Verify template string contains all required input variables
751
- * @param text template string
752
- * @param inputVariables template variables
753
- * @returns boolean or error message
694
+ * Parses the default file diff for a given nodeFile.
695
+ *
696
+ * @param nodeFile - The file change object.
697
+ * @param commit - The commit to diff against. Defaults to '--staged'.
698
+ * @param git - The SimpleGit instance.
699
+ * @returns A Promise that resolves to the file diff as a string.
754
700
  */
755
- function validatePromptTemplate(text, inputVariables) {
756
- if (!text) {
757
- return 'Prompt template cannot be empty';
758
- }
759
- if (!inputVariables.some((entry) => text.includes(entry))) {
760
- return ('Prompt template must include at least one of the following input variables: ' +
761
- inputVariables.map((value) => `{${value}}`).join(', '));
762
- }
763
- return true;
764
- }
765
-
766
701
  async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
767
702
  if (commit !== '--staged') {
768
703
  return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
769
704
  }
770
705
  return await git.diff([commit, nodeFile.filePath]);
771
706
  }
707
+ /**
708
+ * Parses the diff for a renamed file.
709
+ *
710
+ * @param nodeFile - The file change object.
711
+ * @param commit - The commit hash or '--staged'.
712
+ * @param git - The SimpleGit instance.
713
+ * @param logger - The logger instance.
714
+ * @returns A Promise that resolves to the diff string.
715
+ */
772
716
  async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
773
717
  let result = '';
774
718
  const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
@@ -807,6 +751,18 @@ async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
807
751
  }
808
752
  return result;
809
753
  }
754
+ /**
755
+ * Retrieves the diff for a given file change in a specific commit.
756
+ * If the file is deleted, it returns a message indicating that the file has been deleted.
757
+ * If the file is renamed, it parses the renamed file diff and returns it.
758
+ * Otherwise, it retrieves the default diff from the index and returns it.
759
+ *
760
+ * @param nodeFile - The file change object.
761
+ * @param commit - The commit hash.
762
+ * @param git - The SimpleGit instance.
763
+ * @param logger - The logger instance.
764
+ * @returns A promise that resolves to the diff as a string.
765
+ */
810
766
  async function getDiff(nodeFile, commit, { git, logger, }) {
811
767
  if (nodeFile.status === 'deleted') {
812
768
  return 'This file has been deleted.';
@@ -849,14 +805,14 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
849
805
  }
850
806
 
851
807
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
852
- 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.
808
+ 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:
853
809
 
854
- - Typically a hyphen or asterisk is used for the bullet
855
810
  - Write concisely using an informal tone
856
811
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
857
- - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
858
812
  - DO NOT use specific names or files from the code
813
+ - DO NOT include any diffs or file changes in the commit message
859
814
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
815
+ - ONLY respond with the resulting commit message.
860
816
 
861
817
  """{summary}"""
862
818
 
@@ -867,6 +823,151 @@ const COMMIT_PROMPT = new PromptTemplate({
867
823
  inputVariables: inputVariables$1,
868
824
  });
869
825
 
826
+ const DEFAULT_OLLAMA_LLM_SERVICE = {
827
+ provider: 'ollama',
828
+ model: 'codellama',
829
+ endpoint: 'http://localhost:11434',
830
+ maxConcurrent: 1,
831
+ tokenLimit: 1024,
832
+ };
833
+ const DEFAULT_OPENAI_LLM_SERVICE = {
834
+ provider: 'openai',
835
+ model: 'gpt-4',
836
+ authentication: {
837
+ type: 'APIKey',
838
+ credentials: {
839
+ apiKey: '',
840
+ },
841
+ },
842
+ tokenLimit: 1024,
843
+ };
844
+
845
+ /**
846
+ * Retrieves the provider and model from the given configuration object.
847
+ * @param config The configuration object.
848
+ * @returns An object containing the provider and model.
849
+ * @throws Error if the configuration is invalid or missing required properties.
850
+ */
851
+ function getModelAndProviderFromConfig(config) {
852
+ if (!config.service) {
853
+ throw new Error('Invalid service: undefined');
854
+ }
855
+ let result;
856
+ switch (typeof config.service) {
857
+ case 'string':
858
+ result = getDefaultServiceConfigFromAlias(config.service, config?.model);
859
+ break;
860
+ case 'object':
861
+ default:
862
+ result = config.service;
863
+ break;
864
+ }
865
+ const { provider, model } = result;
866
+ if (!model || !provider) {
867
+ throw new Error(`Invalid service: ${config.service}`);
868
+ }
869
+ return { provider, model };
870
+ }
871
+ /**
872
+ * Retrieve appropriate API key based on selected model
873
+ * @param service
874
+ * @param options
875
+ * @returns API Key
876
+ */
877
+ function getApiKeyForModel(config) {
878
+ const { provider } = getModelAndProviderFromConfig(config);
879
+ switch (provider) {
880
+ case 'openai':
881
+ return config.openAIApiKey || getDefaultServiceApiKey(config);
882
+ default:
883
+ return getDefaultServiceApiKey(config);
884
+ }
885
+ }
886
+ /**
887
+ * Retrieves the default service API key from the given configuration.
888
+ * @param config The configuration object.
889
+ * @returns The default service API key.
890
+ */
891
+ function getDefaultServiceApiKey(config) {
892
+ const service = config.service;
893
+ if (service.authentication.type === 'APIKey') {
894
+ return service.authentication.credentials?.apiKey;
895
+ }
896
+ else if (service.authentication.type === 'OAuth') {
897
+ return service.authentication.credentials?.token;
898
+ }
899
+ return '';
900
+ }
901
+ /**
902
+ * Retrieves the default service configuration based on the provided alias and optional model.
903
+ * @param alias - The alias of the service.
904
+ * @param model - The optional model to be used.
905
+ * @returns The default service configuration.
906
+ * @throws Error if the alias is invalid or undefined.
907
+ */
908
+ function getDefaultServiceConfigFromAlias(alias, model) {
909
+ if (!alias) {
910
+ throw new Error('Invalid alias: undefined');
911
+ }
912
+ switch (alias) {
913
+ case 'openai':
914
+ return {
915
+ ...DEFAULT_OPENAI_LLM_SERVICE,
916
+ model: model || DEFAULT_OPENAI_LLM_SERVICE.model,
917
+ };
918
+ case 'ollama':
919
+ return {
920
+ ...DEFAULT_OLLAMA_LLM_SERVICE,
921
+ model: model || DEFAULT_OLLAMA_LLM_SERVICE.model,
922
+ };
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Get LLM Model Based on Configuration
928
+ *
929
+ * @param fields
930
+ * @param configuration
931
+ * @returns LLM Model
932
+ */
933
+ function getLlm(provider, model, config) {
934
+ if (!model) {
935
+ throw new Error(`Invalid LLM Service: ${provider}/${model}`);
936
+ }
937
+ switch (provider) {
938
+ case 'ollama':
939
+ return new Ollama({
940
+ baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
941
+ model,
942
+ });
943
+ case 'openai':
944
+ default:
945
+ return new OpenAI({
946
+ openAIApiKey: config.openAIApiKey,
947
+ modelName: model,
948
+ });
949
+ }
950
+ }
951
+
952
+ function getPrompt({ template, variables, fallback }) {
953
+ if (!template && !fallback)
954
+ throw new Error('Must provide either a template or a fallback');
955
+ return (template
956
+ ? new PromptTemplate({
957
+ template,
958
+ inputVariables: variables,
959
+ })
960
+ : fallback);
961
+ }
962
+
963
+ /**
964
+ * Determines the status of a file based on its changes in the Git repository.
965
+ *
966
+ * @param file - The file to check the status of.
967
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
968
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
969
+ * @throws Error if the file type is invalid.
970
+ */
870
971
  function getStatus(file, location = 'index') {
871
972
  if ('index' in file && 'working_dir' in file) {
872
973
  const statusCode = file[location];
@@ -903,6 +1004,14 @@ function getStatus(file, location = 'index') {
903
1004
  }
904
1005
  }
905
1006
 
1007
+ /**
1008
+ * Returns the summary text for a file change.
1009
+ *
1010
+ * @param file - The file status or diff result.
1011
+ * @param change - The partial file change object.
1012
+ * @returns The summary text for the file change.
1013
+ * @throws Error if the file type is invalid.
1014
+ */
906
1015
  function getSummaryText(file, change) {
907
1016
  const status = change.status || getStatus(file);
908
1017
  let filePath;
@@ -921,9 +1030,12 @@ function getSummaryText(file, change) {
921
1030
  return `${status}: ${filePath}`;
922
1031
  }
923
1032
 
924
- const config = loadConfig();
925
- const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
926
- const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
1033
+ /**
1034
+ * Retrieves the changes in the Git repository.
1035
+ *
1036
+ * @param {GetChangesInput} options - The options for retrieving the changes.
1037
+ * @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
1038
+ */
927
1039
  async function getChanges({ git, options }) {
928
1040
  const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
929
1041
  const staged = [];
@@ -989,13 +1101,13 @@ async function noResult({ git, logger }) {
989
1101
  else if (hasUnstaged || hasUntracked) {
990
1102
  logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
991
1103
  if (hasUnstaged) {
992
- logger.log('\nDetected unstaged changes', { color: 'yellow' });
1104
+ logger.log('\nChanges not staged for commit:', { color: 'yellow' });
993
1105
  logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
994
1106
  color: 'red',
995
1107
  });
996
1108
  }
997
1109
  if (hasUntracked) {
998
- logger.log('\nDetected untracked changes', { color: 'yellow' });
1110
+ logger.log('\nUntracked changes:', { color: 'yellow' });
999
1111
  logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
1000
1112
  color: 'red',
1001
1113
  });
@@ -1069,6 +1181,24 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
1069
1181
  }));
1070
1182
  }
1071
1183
 
1184
+ /**
1185
+ * Verify template string contains all required input variables
1186
+ *
1187
+ * @param text template string
1188
+ * @param inputVariables template variables
1189
+ * @returns boolean or error message
1190
+ */
1191
+ function validatePromptTemplate(text, inputVariables) {
1192
+ if (!text) {
1193
+ return 'Prompt template cannot be empty';
1194
+ }
1195
+ if (!inputVariables.some((entry) => text.includes(entry))) {
1196
+ return ('Prompt template must include at least one of the following input variables: ' +
1197
+ inputVariables.map((value) => `{${value}}`).join(', '));
1198
+ }
1199
+ return true;
1200
+ }
1201
+
1072
1202
  async function editPrompt(options) {
1073
1203
  return await editor({
1074
1204
  message: 'Edit the prompt',
@@ -1199,6 +1329,10 @@ async function handleResult({ result, mode, interactiveHandler }) {
1199
1329
  process.exit(0);
1200
1330
  }
1201
1331
 
1332
+ /**
1333
+ * Retrieves the SimpleGit instance for the repository.
1334
+ * @returns {SimpleGit} The SimpleGit instance.
1335
+ */
1202
1336
  const getRepo = () => {
1203
1337
  let git;
1204
1338
  try {
@@ -1211,34 +1345,50 @@ const getRepo = () => {
1211
1345
  return git;
1212
1346
  };
1213
1347
 
1348
+ /**
1349
+ * Retrieves a TikToken for the specified model.
1350
+ *
1351
+ * @param {TiktokenModel} modelName - The name of the TiktokenModel.
1352
+ * @returns A Promise that resolves to the TikToken.
1353
+ */
1214
1354
  const getTikToken = async (modelName) => {
1215
1355
  return await encoding_for_model(modelName);
1216
1356
  };
1217
- const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1218
- // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1219
- const tokens = tokenizer.encode(text);
1220
- // console.log('Tokens', { tokenCount: tokens.length })
1221
- return tokens.length;
1222
- });
1357
+ /**
1358
+ * Retrieves the token counter for a given model name.
1359
+ *
1360
+ * @param {TikTokenModel} modelName - The name of the Tiktoken model.
1361
+ * @returns A promise that resolves to a function that calculates the number of tokens in a given text.
1362
+ */
1363
+ const getTokenCounter = async (modelName) => {
1364
+ return getTikToken(modelName).then((tokenizer) => (text) => {
1365
+ const tokens = tokenizer.encode(text);
1366
+ return tokens.length;
1367
+ });
1368
+ };
1223
1369
 
1224
- async function createCommit(commitMsg, git) {
1225
- return await git.commit(commitMsg);
1370
+ /**
1371
+ * Creates a commit with the specified commit message.
1372
+ *
1373
+ * @param message The commit message.
1374
+ * @param git The SimpleGit instance.
1375
+ * @returns A Promise that resolves to the CommitResult.
1376
+ */
1377
+ async function createCommit(message, git) {
1378
+ return await git.commit(message);
1226
1379
  }
1227
1380
 
1228
1381
  const handler$2 = async (argv, logger) => {
1229
1382
  const git = getRepo();
1230
1383
  const options = loadConfig(argv);
1231
- const { service } = options;
1232
- const key = getApiKeyForModel(service, options);
1233
- const tokenizer = await getTokenCounter(getModelFromService(service));
1384
+ const key = getApiKeyForModel(options);
1234
1385
  if (!key) {
1235
1386
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1236
1387
  process.exit(1);
1237
1388
  }
1238
- const llm = getLlm(service, key, {
1239
- temperature: 0.4,
1240
- maxConcurrency: 10,
1241
- });
1389
+ const { provider, model } = getModelAndProviderFromConfig(options);
1390
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
1391
+ const llm = getLlm(provider, model, options);
1242
1392
  const INTERACTIVE = isInteractive(options);
1243
1393
  if (INTERACTIVE) {
1244
1394
  logger.log(LOGO);
@@ -1304,16 +1454,10 @@ const handler$2 = async (argv, logger) => {
1304
1454
  * Command line options via yargs
1305
1455
  */
1306
1456
  const options$2 = {
1307
- model: { type: 'string', description: 'LLM/Model-Name' },
1457
+ service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
1308
1458
  openAIApiKey: {
1309
1459
  type: 'string',
1310
1460
  description: 'OpenAI API Key',
1311
- conflicts: 'huggingFaceHubApiKey',
1312
- },
1313
- huggingFaceHubApiKey: {
1314
- type: 'string',
1315
- description: 'HuggingFace Hub API Key',
1316
- conflicts: 'openAIApiKey',
1317
1461
  },
1318
1462
  tokenLimit: { type: 'number', description: 'Token limit' },
1319
1463
  prompt: {
@@ -1375,6 +1519,15 @@ const CHANGELOG_PROMPT = new PromptTemplate({
1375
1519
  inputVariables,
1376
1520
  });
1377
1521
 
1522
+ /**
1523
+ * Retrieves the commit log range between two specified commits.
1524
+ *
1525
+ * @param from - The starting commit.
1526
+ * @param to - The ending commit.
1527
+ * @param options - Additional options for retrieving the commit log range.
1528
+ * @returns A promise that resolves to an array of commit log messages.
1529
+ * @throws If there is an error retrieving the commit log range.
1530
+ */
1378
1531
  async function getCommitLogRange(from, to, { noMerges, git }) {
1379
1532
  try {
1380
1533
  const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
@@ -1388,10 +1541,26 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
1388
1541
  }
1389
1542
  }
1390
1543
 
1544
+ /**
1545
+ * Retrieves the name of the current branch.
1546
+ *
1547
+ * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
1548
+ * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
1549
+ */
1391
1550
  async function getCurrentBranchName({ git }) {
1392
1551
  return await git.revparse(['--abbrev-ref', 'HEAD']);
1393
1552
  }
1394
1553
 
1554
+ /**
1555
+ * Retrieves the commit log for the current branch.
1556
+ *
1557
+ * @param {Object} options - The options for retrieving the commit log.
1558
+ * @param {SimpleGit} options.git - The SimpleGit instance.
1559
+ * @param {Logger} options.logger - The logger for logging messages.
1560
+ * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
1561
+ * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
1562
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
1563
+ */
1395
1564
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
1396
1565
  try {
1397
1566
  // Get the current branch name
@@ -1437,15 +1606,13 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
1437
1606
  const handler$1 = async (argv, logger) => {
1438
1607
  const options = loadConfig(argv);
1439
1608
  const git = getRepo();
1440
- const key = getApiKeyForModel(options.service, options);
1609
+ const key = getApiKeyForModel(options);
1441
1610
  if (!key) {
1442
1611
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1443
1612
  process.exit(1);
1444
1613
  }
1445
- const model = getLlm(options.service, key, {
1446
- temperature: 0.4,
1447
- maxConcurrency: 10,
1448
- });
1614
+ const { provider, model } = getModelAndProviderFromConfig(options);
1615
+ const llm = getLlm(provider, model, options);
1449
1616
  const INTERACTIVE = isInteractive(options);
1450
1617
  if (INTERACTIVE) {
1451
1618
  logger.log(LOGO);
@@ -1477,7 +1644,7 @@ const handler$1 = async (argv, logger) => {
1477
1644
  fallback: CHANGELOG_PROMPT,
1478
1645
  });
1479
1646
  return await executeChain({
1480
- llm: model,
1647
+ llm,
1481
1648
  prompt,
1482
1649
  variables: { summary: context },
1483
1650
  });
@@ -1634,14 +1801,10 @@ async function installNpmPackage({ name, flags = [], cwd = process.cwd(), }) {
1634
1801
  */
1635
1802
  function isPackageInstalled(packageName, projectPath) {
1636
1803
  try {
1637
- // Construct the path to the package.json file
1638
1804
  const packageJsonPath = path__default.join(projectPath, 'package.json');
1639
- // Read the package.json file
1640
1805
  const packageJson = JSON.parse(fs__default.readFileSync(packageJsonPath, 'utf8'));
1641
- // Check both dependencies and devDependencies
1642
1806
  const dependencies = packageJson.dependencies || {};
1643
1807
  const devDependencies = packageJson.devDependencies || {};
1644
- // Return true if the package is found in either
1645
1808
  return dependencies.hasOwnProperty(packageName) || devDependencies.hasOwnProperty(packageName);
1646
1809
  }
1647
1810
  catch (error) {