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/README.md +41 -80
- package/dist/index.d.ts +119 -63
- package/dist/index.esm.mjs +258 -191
- package/dist/index.js +258 -191
- package/package.json +2 -1
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
|
|
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:
|
|
84
|
-
ignoredExtensions:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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?.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
698
|
-
* @param
|
|
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
|
|
731
|
-
|
|
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
|
|
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('\
|
|
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('\
|
|
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) =>
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1464
|
-
|
|
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
|
|
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
|
|
1781
|
+
async function getProjectConfigFilePath(configFileName) {
|
|
1711
1782
|
const projectRoot = findProjectRoot(process.cwd());
|
|
1712
|
-
|
|
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
|
|
1930
|
+
else if (configFilePath.endsWith('.env')) {
|
|
1864
1931
|
await appendToEnvFile(configFilePath, config);
|
|
1865
1932
|
}
|
|
1866
|
-
else if (configFilePath
|
|
1867
|
-
await
|
|
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 });
|