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.esm.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
63
|
-
ignoredExtensions:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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?.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
677
|
-
* @param
|
|
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
|
|
710
|
-
|
|
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
|
|
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('\
|
|
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('\
|
|
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) =>
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
|
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
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1443
|
-
|
|
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
|
|
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
|
|
1760
|
+
async function getProjectConfigFilePath(configFileName) {
|
|
1690
1761
|
const projectRoot = findProjectRoot(process.cwd());
|
|
1691
|
-
|
|
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
|
|
1909
|
+
else if (configFilePath.endsWith('.env')) {
|
|
1843
1910
|
await appendToEnvFile(configFilePath, config);
|
|
1844
1911
|
}
|
|
1845
|
-
else if (configFilePath
|
|
1846
|
-
await
|
|
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 });
|