git-coco 0.7.5 → 0.8.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.
@@ -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,35 +47,38 @@ 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
  });
80
+ const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
81
+ const COCO_CONFIG_END_COMMENT = '# -- end coco config --';
79
82
 
80
83
  async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
81
84
  const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
@@ -150,8 +153,9 @@ function loadEnvConfig(config) {
150
153
  CONFIG_KEYS.forEach((key) => {
151
154
  const envVarName = toEnvVarName(key);
152
155
  const envValue = parseEnvValue(key, process.env[envVarName]);
153
- if (envValue === undefined)
156
+ if (envValue === undefined) {
154
157
  return;
158
+ }
155
159
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
156
160
  // @ts-ignore
157
161
  envConfig[key] = envValue;
@@ -159,27 +163,28 @@ function loadEnvConfig(config) {
159
163
  return { ...config, ...removeUndefined(envConfig) };
160
164
  }
161
165
  function parseEnvValue(key, value) {
162
- if (value === undefined) {
163
- return undefined;
164
- }
165
- else if (key === 'tokenLimit' && typeof value === 'string') {
166
- return parseInt(value);
167
- }
168
- else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
169
- typeof value === 'string' &&
170
- value.includes(',')) {
171
- 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;
172
180
  }
173
- return value;
174
181
  }
175
182
  function toEnvVarName(key) {
176
183
  switch (key) {
177
184
  case 'openAIApiKey':
178
185
  return 'OPENAI_API_KEY';
179
- case 'huggingFaceHubApiKey':
180
- return 'HUGGINGFACE_HUB_API_KEY';
181
186
  default:
182
- return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
187
+ return `COCO_${key.replace(/([A-Z])/g, '_$1').toLocaleUpperCase()}`;
183
188
  }
184
189
  }
185
190
  function formatEnvValue(value) {
@@ -196,8 +201,6 @@ function formatEnvValue(value) {
196
201
  return `${value}`;
197
202
  }
198
203
  const appendToEnvFile = async (filePath, config) => {
199
- const startComment = '# -- Start coco config --';
200
- const endComment = '# -- End coco config --';
201
204
  const getNewContent = async () => {
202
205
  return Object.entries(config)
203
206
  .map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
@@ -205,8 +208,8 @@ const appendToEnvFile = async (filePath, config) => {
205
208
  };
206
209
  await updateFileSection({
207
210
  filePath,
208
- startComment,
209
- endComment,
211
+ startComment: COCO_CONFIG_START_COMMENT,
212
+ endComment: COCO_CONFIG_END_COMMENT,
210
213
  getNewContent,
211
214
  confirmMessage: CONFIG_ALREADY_EXISTS,
212
215
  });
@@ -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,
@@ -250,10 +259,7 @@ const appendToGitConfig = async (filePath, config) => {
250
259
  if (!fs.existsSync(filePath)) {
251
260
  throw new Error(`File ${filePath} does not exist.`);
252
261
  }
253
- const startComment = '# -- Start coco config --';
254
- const endComment = '# -- End coco config --';
255
262
  const header = '[coco]';
256
- // Function to generate new content for the coco section
257
263
  const getNewContent = async () => {
258
264
  const contentLines = [header];
259
265
  for (const key in config) {
@@ -271,8 +277,8 @@ const appendToGitConfig = async (filePath, config) => {
271
277
  };
272
278
  await updateFileSection({
273
279
  filePath,
274
- startComment,
275
- endComment,
280
+ startComment: COCO_CONFIG_START_COMMENT,
281
+ endComment: COCO_CONFIG_END_COMMENT,
276
282
  getNewContent,
277
283
  confirmUpdate: true,
278
284
  confirmMessage: CONFIG_ALREADY_EXISTS,
@@ -318,7 +324,7 @@ function loadIgnore(config) {
318
324
  * @param {Config} config
319
325
  * @returns {Config} Updated config
320
326
  **/
321
- function loadProjectConfig(config) {
327
+ function loadProjectJsonConfig(config) {
322
328
  // TODO: Add validation based of JSON schema?
323
329
  // @see https://github.com/acornejo/jjv
324
330
  if (fs.existsSync('.coco.config.json')) {
@@ -327,7 +333,10 @@ function loadProjectConfig(config) {
327
333
  }
328
334
  return config;
329
335
  }
330
- const appendToProjectConfig = (filePath, config) => {
336
+ const appendToProjectJsonConfig = (filePath, config) => {
337
+ if (!fs.existsSync(filePath)) {
338
+ fs.writeFileSync(filePath, '{}');
339
+ }
331
340
  fs.writeFileSync(filePath, JSON.stringify({
332
341
  $schema: 'https://git-co.co/schema.json',
333
342
  ...config,
@@ -374,7 +383,7 @@ function loadConfig(argv = {}) {
374
383
  config = loadIgnore(config);
375
384
  config = loadXDGConfig(config);
376
385
  config = loadGitConfig(config);
377
- config = loadProjectConfig(config);
386
+ config = loadProjectJsonConfig(config);
378
387
  config = loadEnvConfig(config);
379
388
  return { ...config, ...argv };
380
389
  }
@@ -661,61 +670,16 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
661
670
  };
662
671
  }
663
672
 
664
- function getModelAndProviderFromService(service) {
665
- const [provider, model] = service.split(/\/(.*)/s);
666
- if (!model || !provider) {
667
- throw new Error(`Invalid service: ${service}`);
668
- }
669
- return { provider, model };
670
- }
671
- function getModelFromService(service) {
672
- const { model } = getModelAndProviderFromService(service);
673
- return model;
674
- }
675
673
  /**
676
- * Get LLM Model Based on Configuration
677
- * @param fields
678
- * @param configuration
679
- * @returns LLM Model
680
- */
681
- function getLlm(service, key, fields) {
682
- const { provider, model } = getModelAndProviderFromService(service);
683
- if (!model) {
684
- throw new Error(`Invalid LLM Service: ${service}`);
685
- }
686
- switch (provider) {
687
- case 'huggingface':
688
- return new HuggingFaceInference({
689
- model: model,
690
- apiKey: key,
691
- maxConcurrency: 4,
692
- ...fields,
693
- });
694
- case 'openai':
695
- default:
696
- return new OpenAI({
697
- openAIApiKey: key,
698
- modelName: model,
699
- ...fields,
700
- });
701
- }
702
- }
703
- /**
704
- * Retrieve appropriate API key based on selected model
705
- * @param service
674
+ * Get Summarization Chain
675
+ * @param model
706
676
  * @param options
707
677
  * @returns
708
678
  */
709
- function getApiKeyForModel(service, options) {
710
- const { provider } = getModelAndProviderFromService(service);
711
- switch (provider) {
712
- case 'huggingface':
713
- return options.huggingFaceHubApiKey;
714
- case 'openai':
715
- default:
716
- return options.openAIApiKey;
717
- }
679
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
680
+ return loadSummarizationChain(model, options);
718
681
  }
682
+
719
683
  /**
720
684
  * Get Recursive Character Text Splitter
721
685
  * @param options
@@ -724,41 +688,6 @@ function getApiKeyForModel(service, options) {
724
688
  function getTextSplitter(options = {}) {
725
689
  return new RecursiveCharacterTextSplitter(options);
726
690
  }
727
- /**
728
- * Get Summarization Chain
729
- * @param model
730
- * @param options
731
- * @returns
732
- */
733
- function getSummarizationChain(model, options = { type: 'map_reduce' }) {
734
- return loadSummarizationChain(model, options);
735
- }
736
- function getPrompt({ template, variables, fallback }) {
737
- if (!template && !fallback)
738
- throw new Error('Must provide either a template or a fallback');
739
- return (template
740
- ? new PromptTemplate({
741
- template,
742
- inputVariables: variables,
743
- })
744
- : fallback);
745
- }
746
- /**
747
- * Verify template string contains all required input variables
748
- * @param text template string
749
- * @param inputVariables template variables
750
- * @returns boolean or error message
751
- */
752
- function validatePromptTemplate(text, inputVariables) {
753
- if (!text) {
754
- return 'Prompt template cannot be empty';
755
- }
756
- if (!inputVariables.some((entry) => text.includes(entry))) {
757
- return ('Prompt template must include at least one of the following input variables: ' +
758
- inputVariables.map((value) => `{${value}}`).join(', '));
759
- }
760
- return true;
761
- }
762
691
 
763
692
  async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
764
693
  if (commit !== '--staged') {
@@ -846,14 +775,14 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
846
775
  }
847
776
 
848
777
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
849
- 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.
778
+ 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:
850
779
 
851
- - Typically a hyphen or asterisk is used for the bullet
852
780
  - Write concisely using an informal tone
853
781
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
854
- - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
855
782
  - DO NOT use specific names or files from the code
783
+ - DO NOT include any diffs or file changes in the commit message
856
784
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
785
+ - ONLY respond with the resulting commit message.
857
786
 
858
787
  """{summary}"""
859
788
 
@@ -864,6 +793,143 @@ const COMMIT_PROMPT = new PromptTemplate({
864
793
  inputVariables: inputVariables$1,
865
794
  });
866
795
 
796
+ const DEFAULT_OLLAMA_LLM_SERVICE = {
797
+ provider: 'ollama',
798
+ model: 'codellama',
799
+ endpoint: 'http://localhost:11434',
800
+ maxConcurrent: 1,
801
+ tokenLimit: 1024,
802
+ };
803
+ const DEFAULT_OPENAI_LLM_SERVICE = {
804
+ provider: 'openai',
805
+ model: 'gpt-4',
806
+ authentication: {
807
+ type: 'APIKey',
808
+ credentials: {
809
+ apiKey: '',
810
+ },
811
+ },
812
+ tokenLimit: 1024,
813
+ };
814
+
815
+ /**
816
+ * Retrieves the provider and model from the given configuration object.
817
+ * @param config The configuration object.
818
+ * @returns An object containing the provider and model.
819
+ * @throws Error if the configuration is invalid or missing required properties.
820
+ */
821
+ function getModelAndProviderFromConfig(config) {
822
+ if (!config.service) {
823
+ throw new Error('Invalid service: undefined');
824
+ }
825
+ let result;
826
+ switch (typeof config.service) {
827
+ case 'string':
828
+ result = getDefaultServiceConfigFromAlias(config.service, config?.model);
829
+ break;
830
+ case 'object':
831
+ default:
832
+ result = config.service;
833
+ break;
834
+ }
835
+ const { provider, model } = result;
836
+ if (!model || !provider) {
837
+ throw new Error(`Invalid service: ${config.service}`);
838
+ }
839
+ return { provider, model };
840
+ }
841
+ /**
842
+ * Retrieve appropriate API key based on selected model
843
+ * @param service
844
+ * @param options
845
+ * @returns API Key
846
+ */
847
+ function getApiKeyForModel(config) {
848
+ const { provider } = getModelAndProviderFromConfig(config);
849
+ switch (provider) {
850
+ case 'openai':
851
+ return config.openAIApiKey || getDefaultServiceApiKey(config);
852
+ default:
853
+ return getDefaultServiceApiKey(config);
854
+ }
855
+ }
856
+ /**
857
+ * Retrieves the default service API key from the given configuration.
858
+ * @param config The configuration object.
859
+ * @returns The default service API key.
860
+ */
861
+ function getDefaultServiceApiKey(config) {
862
+ const service = config.service;
863
+ if (service.authentication.type === 'APIKey') {
864
+ return service.authentication.credentials?.apiKey;
865
+ }
866
+ else if (service.authentication.type === 'OAuth') {
867
+ return service.authentication.credentials?.token;
868
+ }
869
+ return '';
870
+ }
871
+ /**
872
+ * Retrieves the default service configuration based on the provided alias and optional model.
873
+ * @param alias - The alias of the service.
874
+ * @param model - The optional model to be used.
875
+ * @returns The default service configuration.
876
+ * @throws Error if the alias is invalid or undefined.
877
+ */
878
+ function getDefaultServiceConfigFromAlias(alias, model) {
879
+ if (!alias) {
880
+ throw new Error('Invalid alias: undefined');
881
+ }
882
+ switch (alias) {
883
+ case 'openai':
884
+ return {
885
+ ...DEFAULT_OPENAI_LLM_SERVICE,
886
+ model: model || DEFAULT_OPENAI_LLM_SERVICE.model,
887
+ };
888
+ case 'ollama':
889
+ return {
890
+ ...DEFAULT_OLLAMA_LLM_SERVICE,
891
+ model: model || DEFAULT_OLLAMA_LLM_SERVICE.model,
892
+ };
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Get LLM Model Based on Configuration
898
+ *
899
+ * @param fields
900
+ * @param configuration
901
+ * @returns LLM Model
902
+ */
903
+ function getLlm(provider, model, config) {
904
+ if (!model) {
905
+ throw new Error(`Invalid LLM Service: ${provider}/${model}`);
906
+ }
907
+ switch (provider) {
908
+ case 'ollama':
909
+ return new Ollama({
910
+ baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
911
+ model,
912
+ });
913
+ case 'openai':
914
+ default:
915
+ return new OpenAI({
916
+ openAIApiKey: config.openAIApiKey,
917
+ modelName: model,
918
+ });
919
+ }
920
+ }
921
+
922
+ function getPrompt({ template, variables, fallback }) {
923
+ if (!template && !fallback)
924
+ throw new Error('Must provide either a template or a fallback');
925
+ return (template
926
+ ? new PromptTemplate({
927
+ template,
928
+ inputVariables: variables,
929
+ })
930
+ : fallback);
931
+ }
932
+
867
933
  function getStatus(file, location = 'index') {
868
934
  if ('index' in file && 'working_dir' in file) {
869
935
  const statusCode = file[location];
@@ -918,9 +984,6 @@ function getSummaryText(file, change) {
918
984
  return `${status}: ${filePath}`;
919
985
  }
920
986
 
921
- const config = loadConfig();
922
- const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
923
- const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
924
987
  async function getChanges({ git, options }) {
925
988
  const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
926
989
  const staged = [];
@@ -986,13 +1049,13 @@ async function noResult({ git, logger }) {
986
1049
  else if (hasUnstaged || hasUntracked) {
987
1050
  logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
988
1051
  if (hasUnstaged) {
989
- logger.log('\nDetected unstaged changes', { color: 'yellow' });
1052
+ logger.log('\nChanges not staged for commit:', { color: 'yellow' });
990
1053
  logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
991
1054
  color: 'red',
992
1055
  });
993
1056
  }
994
1057
  if (hasUntracked) {
995
- logger.log('\nDetected untracked changes', { color: 'yellow' });
1058
+ logger.log('\nUntracked changes:', { color: 'yellow' });
996
1059
  logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
997
1060
  color: 'red',
998
1061
  });
@@ -1066,6 +1129,23 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
1066
1129
  }));
1067
1130
  }
1068
1131
 
1132
+ /**
1133
+ * Verify template string contains all required input variables
1134
+ * @param text template string
1135
+ * @param inputVariables template variables
1136
+ * @returns boolean or error message
1137
+ */
1138
+ function validatePromptTemplate(text, inputVariables) {
1139
+ if (!text) {
1140
+ return 'Prompt template cannot be empty';
1141
+ }
1142
+ if (!inputVariables.some((entry) => text.includes(entry))) {
1143
+ return ('Prompt template must include at least one of the following input variables: ' +
1144
+ inputVariables.map((value) => `{${value}}`).join(', '));
1145
+ }
1146
+ return true;
1147
+ }
1148
+
1069
1149
  async function editPrompt(options) {
1070
1150
  return await editor({
1071
1151
  message: 'Edit the prompt',
@@ -1211,12 +1291,14 @@ const getRepo = () => {
1211
1291
  const getTikToken = async (modelName) => {
1212
1292
  return await encoding_for_model(modelName);
1213
1293
  };
1214
- const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1215
- // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1216
- const tokens = tokenizer.encode(text);
1217
- // console.log('Tokens', { tokenCount: tokens.length })
1218
- return tokens.length;
1219
- });
1294
+ const getTokenCounter = async (modelName) => {
1295
+ return getTikToken(modelName).then((tokenizer) => (text) => {
1296
+ // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1297
+ const tokens = tokenizer.encode(text);
1298
+ // console.log('Tokens', { tokenCount: tokens.length })
1299
+ return tokens.length;
1300
+ });
1301
+ };
1220
1302
 
1221
1303
  async function createCommit(commitMsg, git) {
1222
1304
  return await git.commit(commitMsg);
@@ -1225,17 +1307,14 @@ async function createCommit(commitMsg, git) {
1225
1307
  const handler$2 = async (argv, logger) => {
1226
1308
  const git = getRepo();
1227
1309
  const options = loadConfig(argv);
1228
- const { service } = options;
1229
- const key = getApiKeyForModel(service, options);
1230
- const tokenizer = await getTokenCounter(getModelFromService(service));
1310
+ const key = getApiKeyForModel(options);
1231
1311
  if (!key) {
1232
1312
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1233
1313
  process.exit(1);
1234
1314
  }
1235
- const llm = getLlm(service, key, {
1236
- temperature: 0.4,
1237
- maxConcurrency: 10,
1238
- });
1315
+ const { provider, model } = getModelAndProviderFromConfig(options);
1316
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
1317
+ const llm = getLlm(provider, model, options);
1239
1318
  const INTERACTIVE = isInteractive(options);
1240
1319
  if (INTERACTIVE) {
1241
1320
  logger.log(LOGO);
@@ -1301,16 +1380,10 @@ const handler$2 = async (argv, logger) => {
1301
1380
  * Command line options via yargs
1302
1381
  */
1303
1382
  const options$2 = {
1304
- model: { type: 'string', description: 'LLM/Model-Name' },
1383
+ service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
1305
1384
  openAIApiKey: {
1306
1385
  type: 'string',
1307
1386
  description: 'OpenAI API Key',
1308
- conflicts: 'huggingFaceHubApiKey',
1309
- },
1310
- huggingFaceHubApiKey: {
1311
- type: 'string',
1312
- description: 'HuggingFace Hub API Key',
1313
- conflicts: 'openAIApiKey',
1314
1387
  },
1315
1388
  tokenLimit: { type: 'number', description: 'Token limit' },
1316
1389
  prompt: {
@@ -1434,15 +1507,13 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
1434
1507
  const handler$1 = async (argv, logger) => {
1435
1508
  const options = loadConfig(argv);
1436
1509
  const git = getRepo();
1437
- const key = getApiKeyForModel(options.service, options);
1510
+ const key = getApiKeyForModel(options);
1438
1511
  if (!key) {
1439
1512
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1440
1513
  process.exit(1);
1441
1514
  }
1442
- const model = getLlm(options.service, key, {
1443
- temperature: 0.4,
1444
- maxConcurrency: 10,
1445
- });
1515
+ const { provider, model } = getModelAndProviderFromConfig(options);
1516
+ const llm = getLlm(provider, model, options);
1446
1517
  const INTERACTIVE = isInteractive(options);
1447
1518
  if (INTERACTIVE) {
1448
1519
  logger.log(LOGO);
@@ -1474,7 +1545,7 @@ const handler$1 = async (argv, logger) => {
1474
1545
  fallback: CHANGELOG_PROMPT,
1475
1546
  });
1476
1547
  return await executeChain({
1477
- llm: model,
1548
+ llm,
1478
1549
  prompt,
1479
1550
  variables: { summary: context },
1480
1551
  });
@@ -1686,13 +1757,9 @@ function getPathToUsersGitConfig() {
1686
1757
  return path__default.join(os__default.homedir(), '.gitconfig');
1687
1758
  }
1688
1759
 
1689
- async function createProjectFileAndReturnPath(fileName, contents) {
1760
+ async function getProjectConfigFilePath(configFileName) {
1690
1761
  const projectRoot = findProjectRoot(process.cwd());
1691
- const configFile = `${projectRoot}/${fileName}`;
1692
- if (!fs__default.existsSync(configFile)) {
1693
- fs__default.writeFileSync(configFile, contents || '');
1694
- }
1695
- return configFile;
1762
+ return `${projectRoot}/${configFileName}`;
1696
1763
  }
1697
1764
 
1698
1765
  const handler = async (argv, logger) => {
@@ -1716,29 +1783,6 @@ const handler = async (argv, logger) => {
1716
1783
  ],
1717
1784
  });
1718
1785
  }
1719
- let configFilePath = '';
1720
- switch (level) {
1721
- case 'project':
1722
- const projectConfiguration = await select({
1723
- message: 'select type project level configuration:',
1724
- choices: [
1725
- {
1726
- name: '.coco.config.json',
1727
- value: '.coco.config.json',
1728
- },
1729
- {
1730
- name: '.env',
1731
- value: '.env',
1732
- },
1733
- ],
1734
- });
1735
- configFilePath = await createProjectFileAndReturnPath(projectConfiguration);
1736
- break;
1737
- case 'global':
1738
- default:
1739
- configFilePath = getPathToUsersGitConfig();
1740
- break;
1741
- }
1742
1786
  // interactive v.s stdout mode
1743
1787
  const mode = (await select({
1744
1788
  message: 'select mode:',
@@ -1835,15 +1879,38 @@ const handler = async (argv, logger) => {
1835
1879
  const isApproved = await confirm({
1836
1880
  message: 'looking good? (API key hidden for security)',
1837
1881
  });
1882
+ let configFilePath = '';
1883
+ switch (level) {
1884
+ case 'project':
1885
+ const projectConfiguration = (await select({
1886
+ message: 'where would you like to store the project config?',
1887
+ choices: [
1888
+ {
1889
+ name: '.coco.config.json',
1890
+ value: '.coco.config.json',
1891
+ },
1892
+ {
1893
+ name: '.env',
1894
+ value: '.env',
1895
+ },
1896
+ ],
1897
+ }));
1898
+ configFilePath = await getProjectConfigFilePath(projectConfiguration);
1899
+ break;
1900
+ case 'global':
1901
+ default:
1902
+ configFilePath = getPathToUsersGitConfig();
1903
+ break;
1904
+ }
1838
1905
  if (isApproved) {
1839
1906
  if (configFilePath.endsWith('.gitconfig')) {
1840
1907
  await appendToGitConfig(configFilePath, config);
1841
1908
  }
1842
- else if (configFilePath === '.env') {
1909
+ else if (configFilePath.endsWith('.env')) {
1843
1910
  await appendToEnvFile(configFilePath, config);
1844
1911
  }
1845
- else if (configFilePath === '.coco.config.json') {
1846
- await appendToProjectConfig(configFilePath, config);
1912
+ else if (configFilePath.endsWith('.coco.config.json')) {
1913
+ await appendToProjectJsonConfig(configFilePath, config);
1847
1914
  }
1848
1915
  // After config is written, check for package installation
1849
1916
  await checkAndHandlePackageInstallation({ global: level === 'global', logger });