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.
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,35 +68,38 @@ 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
  });
101
+ const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
102
+ const COCO_CONFIG_END_COMMENT = '# -- end coco config --';
100
103
 
101
104
  async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
102
105
  const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
@@ -171,8 +174,9 @@ function loadEnvConfig(config) {
171
174
  CONFIG_KEYS.forEach((key) => {
172
175
  const envVarName = toEnvVarName(key);
173
176
  const envValue = parseEnvValue(key, process.env[envVarName]);
174
- if (envValue === undefined)
177
+ if (envValue === undefined) {
175
178
  return;
179
+ }
176
180
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
177
181
  // @ts-ignore
178
182
  envConfig[key] = envValue;
@@ -180,27 +184,28 @@ function loadEnvConfig(config) {
180
184
  return { ...config, ...removeUndefined(envConfig) };
181
185
  }
182
186
  function parseEnvValue(key, value) {
183
- if (value === undefined) {
184
- return undefined;
185
- }
186
- else if (key === 'tokenLimit' && typeof value === 'string') {
187
- return parseInt(value);
188
- }
189
- else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
190
- typeof value === 'string' &&
191
- value.includes(',')) {
192
- 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;
193
201
  }
194
- return value;
195
202
  }
196
203
  function toEnvVarName(key) {
197
204
  switch (key) {
198
205
  case 'openAIApiKey':
199
206
  return 'OPENAI_API_KEY';
200
- case 'huggingFaceHubApiKey':
201
- return 'HUGGINGFACE_HUB_API_KEY';
202
207
  default:
203
- return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
208
+ return `COCO_${key.replace(/([A-Z])/g, '_$1').toLocaleUpperCase()}`;
204
209
  }
205
210
  }
206
211
  function formatEnvValue(value) {
@@ -217,8 +222,6 @@ function formatEnvValue(value) {
217
222
  return `${value}`;
218
223
  }
219
224
  const appendToEnvFile = async (filePath, config) => {
220
- const startComment = '# -- Start coco config --';
221
- const endComment = '# -- End coco config --';
222
225
  const getNewContent = async () => {
223
226
  return Object.entries(config)
224
227
  .map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
@@ -226,8 +229,8 @@ const appendToEnvFile = async (filePath, config) => {
226
229
  };
227
230
  await updateFileSection({
228
231
  filePath,
229
- startComment,
230
- endComment,
232
+ startComment: COCO_CONFIG_START_COMMENT,
233
+ endComment: COCO_CONFIG_END_COMMENT,
231
234
  getNewContent,
232
235
  confirmMessage: CONFIG_ALREADY_EXISTS,
233
236
  });
@@ -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,
@@ -271,10 +280,7 @@ const appendToGitConfig = async (filePath, config) => {
271
280
  if (!fs__namespace.existsSync(filePath)) {
272
281
  throw new Error(`File ${filePath} does not exist.`);
273
282
  }
274
- const startComment = '# -- Start coco config --';
275
- const endComment = '# -- End coco config --';
276
283
  const header = '[coco]';
277
- // Function to generate new content for the coco section
278
284
  const getNewContent = async () => {
279
285
  const contentLines = [header];
280
286
  for (const key in config) {
@@ -292,8 +298,8 @@ const appendToGitConfig = async (filePath, config) => {
292
298
  };
293
299
  await updateFileSection({
294
300
  filePath,
295
- startComment,
296
- endComment,
301
+ startComment: COCO_CONFIG_START_COMMENT,
302
+ endComment: COCO_CONFIG_END_COMMENT,
297
303
  getNewContent,
298
304
  confirmUpdate: true,
299
305
  confirmMessage: CONFIG_ALREADY_EXISTS,
@@ -339,7 +345,7 @@ function loadIgnore(config) {
339
345
  * @param {Config} config
340
346
  * @returns {Config} Updated config
341
347
  **/
342
- function loadProjectConfig(config) {
348
+ function loadProjectJsonConfig(config) {
343
349
  // TODO: Add validation based of JSON schema?
344
350
  // @see https://github.com/acornejo/jjv
345
351
  if (fs__namespace.existsSync('.coco.config.json')) {
@@ -348,7 +354,10 @@ function loadProjectConfig(config) {
348
354
  }
349
355
  return config;
350
356
  }
351
- const appendToProjectConfig = (filePath, config) => {
357
+ const appendToProjectJsonConfig = (filePath, config) => {
358
+ if (!fs__namespace.existsSync(filePath)) {
359
+ fs__namespace.writeFileSync(filePath, '{}');
360
+ }
352
361
  fs__namespace.writeFileSync(filePath, JSON.stringify({
353
362
  $schema: 'https://git-co.co/schema.json',
354
363
  ...config,
@@ -395,7 +404,7 @@ function loadConfig(argv = {}) {
395
404
  config = loadIgnore(config);
396
405
  config = loadXDGConfig(config);
397
406
  config = loadGitConfig(config);
398
- config = loadProjectConfig(config);
407
+ config = loadProjectJsonConfig(config);
399
408
  config = loadEnvConfig(config);
400
409
  return { ...config, ...argv };
401
410
  }
@@ -682,61 +691,16 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
682
691
  };
683
692
  }
684
693
 
685
- function getModelAndProviderFromService(service) {
686
- const [provider, model] = service.split(/\/(.*)/s);
687
- if (!model || !provider) {
688
- throw new Error(`Invalid service: ${service}`);
689
- }
690
- return { provider, model };
691
- }
692
- function getModelFromService(service) {
693
- const { model } = getModelAndProviderFromService(service);
694
- return model;
695
- }
696
694
  /**
697
- * Get LLM Model Based on Configuration
698
- * @param fields
699
- * @param configuration
700
- * @returns LLM Model
701
- */
702
- function getLlm(service, key, fields) {
703
- const { provider, model } = getModelAndProviderFromService(service);
704
- if (!model) {
705
- throw new Error(`Invalid LLM Service: ${service}`);
706
- }
707
- switch (provider) {
708
- case 'huggingface':
709
- return new hf.HuggingFaceInference({
710
- model: model,
711
- apiKey: key,
712
- maxConcurrency: 4,
713
- ...fields,
714
- });
715
- case 'openai':
716
- default:
717
- return new openai.OpenAI({
718
- openAIApiKey: key,
719
- modelName: model,
720
- ...fields,
721
- });
722
- }
723
- }
724
- /**
725
- * Retrieve appropriate API key based on selected model
726
- * @param service
695
+ * Get Summarization Chain
696
+ * @param model
727
697
  * @param options
728
698
  * @returns
729
699
  */
730
- function getApiKeyForModel(service, options) {
731
- const { provider } = getModelAndProviderFromService(service);
732
- switch (provider) {
733
- case 'huggingface':
734
- return options.huggingFaceHubApiKey;
735
- case 'openai':
736
- default:
737
- return options.openAIApiKey;
738
- }
700
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
701
+ return chains.loadSummarizationChain(model, options);
739
702
  }
703
+
740
704
  /**
741
705
  * Get Recursive Character Text Splitter
742
706
  * @param options
@@ -745,41 +709,6 @@ function getApiKeyForModel(service, options) {
745
709
  function getTextSplitter(options = {}) {
746
710
  return new text_splitter.RecursiveCharacterTextSplitter(options);
747
711
  }
748
- /**
749
- * Get Summarization Chain
750
- * @param model
751
- * @param options
752
- * @returns
753
- */
754
- function getSummarizationChain(model, options = { type: 'map_reduce' }) {
755
- return chains.loadSummarizationChain(model, options);
756
- }
757
- function getPrompt({ template, variables, fallback }) {
758
- if (!template && !fallback)
759
- throw new Error('Must provide either a template or a fallback');
760
- return (template
761
- ? new prompts.PromptTemplate({
762
- template,
763
- inputVariables: variables,
764
- })
765
- : fallback);
766
- }
767
- /**
768
- * Verify template string contains all required input variables
769
- * @param text template string
770
- * @param inputVariables template variables
771
- * @returns boolean or error message
772
- */
773
- function validatePromptTemplate(text, inputVariables) {
774
- if (!text) {
775
- return 'Prompt template cannot be empty';
776
- }
777
- if (!inputVariables.some((entry) => text.includes(entry))) {
778
- return ('Prompt template must include at least one of the following input variables: ' +
779
- inputVariables.map((value) => `{${value}}`).join(', '));
780
- }
781
- return true;
782
- }
783
712
 
784
713
  async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
785
714
  if (commit !== '--staged') {
@@ -867,14 +796,14 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
867
796
  }
868
797
 
869
798
  const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
870
- 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.
799
+ 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:
871
800
 
872
- - Typically a hyphen or asterisk is used for the bullet
873
801
  - Write concisely using an informal tone
874
802
  - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
875
- - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
876
803
  - DO NOT use specific names or files from the code
804
+ - DO NOT include any diffs or file changes in the commit message
877
805
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
806
+ - ONLY respond with the resulting commit message.
878
807
 
879
808
  """{summary}"""
880
809
 
@@ -885,6 +814,143 @@ const COMMIT_PROMPT = new prompts.PromptTemplate({
885
814
  inputVariables: inputVariables$1,
886
815
  });
887
816
 
817
+ const DEFAULT_OLLAMA_LLM_SERVICE = {
818
+ provider: 'ollama',
819
+ model: 'codellama',
820
+ endpoint: 'http://localhost:11434',
821
+ maxConcurrent: 1,
822
+ tokenLimit: 1024,
823
+ };
824
+ const DEFAULT_OPENAI_LLM_SERVICE = {
825
+ provider: 'openai',
826
+ model: 'gpt-4',
827
+ authentication: {
828
+ type: 'APIKey',
829
+ credentials: {
830
+ apiKey: '',
831
+ },
832
+ },
833
+ tokenLimit: 1024,
834
+ };
835
+
836
+ /**
837
+ * Retrieves the provider and model from the given configuration object.
838
+ * @param config The configuration object.
839
+ * @returns An object containing the provider and model.
840
+ * @throws Error if the configuration is invalid or missing required properties.
841
+ */
842
+ function getModelAndProviderFromConfig(config) {
843
+ if (!config.service) {
844
+ throw new Error('Invalid service: undefined');
845
+ }
846
+ let result;
847
+ switch (typeof config.service) {
848
+ case 'string':
849
+ result = getDefaultServiceConfigFromAlias(config.service, config?.model);
850
+ break;
851
+ case 'object':
852
+ default:
853
+ result = config.service;
854
+ break;
855
+ }
856
+ const { provider, model } = result;
857
+ if (!model || !provider) {
858
+ throw new Error(`Invalid service: ${config.service}`);
859
+ }
860
+ return { provider, model };
861
+ }
862
+ /**
863
+ * Retrieve appropriate API key based on selected model
864
+ * @param service
865
+ * @param options
866
+ * @returns API Key
867
+ */
868
+ function getApiKeyForModel(config) {
869
+ const { provider } = getModelAndProviderFromConfig(config);
870
+ switch (provider) {
871
+ case 'openai':
872
+ return config.openAIApiKey || getDefaultServiceApiKey(config);
873
+ default:
874
+ return getDefaultServiceApiKey(config);
875
+ }
876
+ }
877
+ /**
878
+ * Retrieves the default service API key from the given configuration.
879
+ * @param config The configuration object.
880
+ * @returns The default service API key.
881
+ */
882
+ function getDefaultServiceApiKey(config) {
883
+ const service = config.service;
884
+ if (service.authentication.type === 'APIKey') {
885
+ return service.authentication.credentials?.apiKey;
886
+ }
887
+ else if (service.authentication.type === 'OAuth') {
888
+ return service.authentication.credentials?.token;
889
+ }
890
+ return '';
891
+ }
892
+ /**
893
+ * Retrieves the default service configuration based on the provided alias and optional model.
894
+ * @param alias - The alias of the service.
895
+ * @param model - The optional model to be used.
896
+ * @returns The default service configuration.
897
+ * @throws Error if the alias is invalid or undefined.
898
+ */
899
+ function getDefaultServiceConfigFromAlias(alias, model) {
900
+ if (!alias) {
901
+ throw new Error('Invalid alias: undefined');
902
+ }
903
+ switch (alias) {
904
+ case 'openai':
905
+ return {
906
+ ...DEFAULT_OPENAI_LLM_SERVICE,
907
+ model: model || DEFAULT_OPENAI_LLM_SERVICE.model,
908
+ };
909
+ case 'ollama':
910
+ return {
911
+ ...DEFAULT_OLLAMA_LLM_SERVICE,
912
+ model: model || DEFAULT_OLLAMA_LLM_SERVICE.model,
913
+ };
914
+ }
915
+ }
916
+
917
+ /**
918
+ * Get LLM Model Based on Configuration
919
+ *
920
+ * @param fields
921
+ * @param configuration
922
+ * @returns LLM Model
923
+ */
924
+ function getLlm(provider, model, config) {
925
+ if (!model) {
926
+ throw new Error(`Invalid LLM Service: ${provider}/${model}`);
927
+ }
928
+ switch (provider) {
929
+ case 'ollama':
930
+ return new ollama.Ollama({
931
+ baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
932
+ model,
933
+ });
934
+ case 'openai':
935
+ default:
936
+ return new openai.OpenAI({
937
+ openAIApiKey: config.openAIApiKey,
938
+ modelName: model,
939
+ });
940
+ }
941
+ }
942
+
943
+ function getPrompt({ template, variables, fallback }) {
944
+ if (!template && !fallback)
945
+ throw new Error('Must provide either a template or a fallback');
946
+ return (template
947
+ ? new prompts.PromptTemplate({
948
+ template,
949
+ inputVariables: variables,
950
+ })
951
+ : fallback);
952
+ }
953
+
888
954
  function getStatus(file, location = 'index') {
889
955
  if ('index' in file && 'working_dir' in file) {
890
956
  const statusCode = file[location];
@@ -939,9 +1005,6 @@ function getSummaryText(file, change) {
939
1005
  return `${status}: ${filePath}`;
940
1006
  }
941
1007
 
942
- const config = loadConfig();
943
- const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
944
- const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
945
1008
  async function getChanges({ git, options }) {
946
1009
  const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
947
1010
  const staged = [];
@@ -1007,13 +1070,13 @@ async function noResult({ git, logger }) {
1007
1070
  else if (hasUnstaged || hasUntracked) {
1008
1071
  logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
1009
1072
  if (hasUnstaged) {
1010
- logger.log('\nDetected unstaged changes', { color: 'yellow' });
1073
+ logger.log('\nChanges not staged for commit:', { color: 'yellow' });
1011
1074
  logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
1012
1075
  color: 'red',
1013
1076
  });
1014
1077
  }
1015
1078
  if (hasUntracked) {
1016
- logger.log('\nDetected untracked changes', { color: 'yellow' });
1079
+ logger.log('\nUntracked changes:', { color: 'yellow' });
1017
1080
  logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
1018
1081
  color: 'red',
1019
1082
  });
@@ -1087,6 +1150,23 @@ async function getUserReviewDecision({ label, descriptions, enableRetry = true,
1087
1150
  }));
1088
1151
  }
1089
1152
 
1153
+ /**
1154
+ * Verify template string contains all required input variables
1155
+ * @param text template string
1156
+ * @param inputVariables template variables
1157
+ * @returns boolean or error message
1158
+ */
1159
+ function validatePromptTemplate(text, inputVariables) {
1160
+ if (!text) {
1161
+ return 'Prompt template cannot be empty';
1162
+ }
1163
+ if (!inputVariables.some((entry) => text.includes(entry))) {
1164
+ return ('Prompt template must include at least one of the following input variables: ' +
1165
+ inputVariables.map((value) => `{${value}}`).join(', '));
1166
+ }
1167
+ return true;
1168
+ }
1169
+
1090
1170
  async function editPrompt(options) {
1091
1171
  return await prompts$1.editor({
1092
1172
  message: 'Edit the prompt',
@@ -1232,12 +1312,14 @@ const getRepo = () => {
1232
1312
  const getTikToken = async (modelName) => {
1233
1313
  return await tiktoken.encoding_for_model(modelName);
1234
1314
  };
1235
- const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1236
- // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1237
- const tokens = tokenizer.encode(text);
1238
- // console.log('Tokens', { tokenCount: tokens.length })
1239
- return tokens.length;
1240
- });
1315
+ const getTokenCounter = async (modelName) => {
1316
+ return getTikToken(modelName).then((tokenizer) => (text) => {
1317
+ // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1318
+ const tokens = tokenizer.encode(text);
1319
+ // console.log('Tokens', { tokenCount: tokens.length })
1320
+ return tokens.length;
1321
+ });
1322
+ };
1241
1323
 
1242
1324
  async function createCommit(commitMsg, git) {
1243
1325
  return await git.commit(commitMsg);
@@ -1246,17 +1328,14 @@ async function createCommit(commitMsg, git) {
1246
1328
  const handler$2 = async (argv, logger) => {
1247
1329
  const git = getRepo();
1248
1330
  const options = loadConfig(argv);
1249
- const { service } = options;
1250
- const key = getApiKeyForModel(service, options);
1251
- const tokenizer = await getTokenCounter(getModelFromService(service));
1331
+ const key = getApiKeyForModel(options);
1252
1332
  if (!key) {
1253
1333
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1254
1334
  process.exit(1);
1255
1335
  }
1256
- const llm = getLlm(service, key, {
1257
- temperature: 0.4,
1258
- maxConcurrency: 10,
1259
- });
1336
+ const { provider, model } = getModelAndProviderFromConfig(options);
1337
+ const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
1338
+ const llm = getLlm(provider, model, options);
1260
1339
  const INTERACTIVE = isInteractive(options);
1261
1340
  if (INTERACTIVE) {
1262
1341
  logger.log(LOGO);
@@ -1322,16 +1401,10 @@ const handler$2 = async (argv, logger) => {
1322
1401
  * Command line options via yargs
1323
1402
  */
1324
1403
  const options$2 = {
1325
- model: { type: 'string', description: 'LLM/Model-Name' },
1404
+ service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
1326
1405
  openAIApiKey: {
1327
1406
  type: 'string',
1328
1407
  description: 'OpenAI API Key',
1329
- conflicts: 'huggingFaceHubApiKey',
1330
- },
1331
- huggingFaceHubApiKey: {
1332
- type: 'string',
1333
- description: 'HuggingFace Hub API Key',
1334
- conflicts: 'openAIApiKey',
1335
1408
  },
1336
1409
  tokenLimit: { type: 'number', description: 'Token limit' },
1337
1410
  prompt: {
@@ -1455,15 +1528,13 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
1455
1528
  const handler$1 = async (argv, logger) => {
1456
1529
  const options = loadConfig(argv);
1457
1530
  const git = getRepo();
1458
- const key = getApiKeyForModel(options.service, options);
1531
+ const key = getApiKeyForModel(options);
1459
1532
  if (!key) {
1460
1533
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1461
1534
  process.exit(1);
1462
1535
  }
1463
- const model = getLlm(options.service, key, {
1464
- temperature: 0.4,
1465
- maxConcurrency: 10,
1466
- });
1536
+ const { provider, model } = getModelAndProviderFromConfig(options);
1537
+ const llm = getLlm(provider, model, options);
1467
1538
  const INTERACTIVE = isInteractive(options);
1468
1539
  if (INTERACTIVE) {
1469
1540
  logger.log(LOGO);
@@ -1495,7 +1566,7 @@ const handler$1 = async (argv, logger) => {
1495
1566
  fallback: CHANGELOG_PROMPT,
1496
1567
  });
1497
1568
  return await executeChain({
1498
- llm: model,
1569
+ llm,
1499
1570
  prompt,
1500
1571
  variables: { summary: context },
1501
1572
  });
@@ -1707,13 +1778,9 @@ function getPathToUsersGitConfig() {
1707
1778
  return path.join(os.homedir(), '.gitconfig');
1708
1779
  }
1709
1780
 
1710
- async function createProjectFileAndReturnPath(fileName, contents) {
1781
+ async function getProjectConfigFilePath(configFileName) {
1711
1782
  const projectRoot = findProjectRoot(process.cwd());
1712
- const configFile = `${projectRoot}/${fileName}`;
1713
- if (!fs.existsSync(configFile)) {
1714
- fs.writeFileSync(configFile, contents || '');
1715
- }
1716
- return configFile;
1783
+ return `${projectRoot}/${configFileName}`;
1717
1784
  }
1718
1785
 
1719
1786
  const handler = async (argv, logger) => {
@@ -1737,29 +1804,6 @@ const handler = async (argv, logger) => {
1737
1804
  ],
1738
1805
  });
1739
1806
  }
1740
- let configFilePath = '';
1741
- switch (level) {
1742
- case 'project':
1743
- const projectConfiguration = await prompts$1.select({
1744
- message: 'select type project level configuration:',
1745
- choices: [
1746
- {
1747
- name: '.coco.config.json',
1748
- value: '.coco.config.json',
1749
- },
1750
- {
1751
- name: '.env',
1752
- value: '.env',
1753
- },
1754
- ],
1755
- });
1756
- configFilePath = await createProjectFileAndReturnPath(projectConfiguration);
1757
- break;
1758
- case 'global':
1759
- default:
1760
- configFilePath = getPathToUsersGitConfig();
1761
- break;
1762
- }
1763
1807
  // interactive v.s stdout mode
1764
1808
  const mode = (await prompts$1.select({
1765
1809
  message: 'select mode:',
@@ -1856,15 +1900,38 @@ const handler = async (argv, logger) => {
1856
1900
  const isApproved = await prompts$1.confirm({
1857
1901
  message: 'looking good? (API key hidden for security)',
1858
1902
  });
1903
+ let configFilePath = '';
1904
+ switch (level) {
1905
+ case 'project':
1906
+ const projectConfiguration = (await prompts$1.select({
1907
+ message: 'where would you like to store the project config?',
1908
+ choices: [
1909
+ {
1910
+ name: '.coco.config.json',
1911
+ value: '.coco.config.json',
1912
+ },
1913
+ {
1914
+ name: '.env',
1915
+ value: '.env',
1916
+ },
1917
+ ],
1918
+ }));
1919
+ configFilePath = await getProjectConfigFilePath(projectConfiguration);
1920
+ break;
1921
+ case 'global':
1922
+ default:
1923
+ configFilePath = getPathToUsersGitConfig();
1924
+ break;
1925
+ }
1859
1926
  if (isApproved) {
1860
1927
  if (configFilePath.endsWith('.gitconfig')) {
1861
1928
  await appendToGitConfig(configFilePath, config);
1862
1929
  }
1863
- else if (configFilePath === '.env') {
1930
+ else if (configFilePath.endsWith('.env')) {
1864
1931
  await appendToEnvFile(configFilePath, config);
1865
1932
  }
1866
- else if (configFilePath === '.coco.config.json') {
1867
- await appendToProjectConfig(configFilePath, config);
1933
+ else if (configFilePath.endsWith('.coco.config.json')) {
1934
+ await appendToProjectJsonConfig(configFilePath, config);
1868
1935
  }
1869
1936
  // After config is written, check for package installation
1870
1937
  await checkAndHandlePackageInstallation({ global: level === 'global', logger });