git-coco 0.6.2 → 0.6.3

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.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  /// <reference types="yargs" />
2
2
  import * as yargs from 'yargs';
3
3
  import { Argv } from 'yargs';
4
+ import { TiktokenModel } from 'langchain/dist/types/openai-types';
4
5
  import { HuggingFaceInference } from 'langchain/llms/hf';
5
6
  import { BaseLLMParams } from 'langchain/llms/base';
6
7
  import { OpenAIInput, AzureOpenAIInput, OpenAI } from 'langchain/llms/openai';
7
8
  import { SimpleGit } from 'simple-git';
8
9
  import { Color } from 'chalk';
9
- import GPT3NodeTokenizer from 'gpt3-tokenizer';
10
10
 
11
+ type ServiceProvider = "openai" | "huggingface";
12
+ type ServiceModel = TiktokenModel;
13
+ type Service = `${ServiceProvider}/${ServiceModel}`;
11
14
  interface Config$1 {
12
15
  /**
13
16
  * The LLM model to use for generating results.
@@ -18,7 +21,7 @@ interface Config$1 {
18
21
  * @example 'openai/gpt-3.5-turbo'
19
22
  * @example 'huggingface/bigscience/bloom'
20
23
  **/
21
- model: string;
24
+ service: Service;
22
25
  /**
23
26
  * The OpenAI API key.
24
27
  */
@@ -99,10 +102,10 @@ interface BaseArgvOptions {
99
102
  }
100
103
  interface BaseCommandOptions extends BaseArgvOptions {
101
104
  [x: string]: unknown;
102
- model: string;
103
- openAIApiKey: string;
104
- huggingFaceHubApiKey: string;
105
- tokenLimit: number;
105
+ service: Config$1['service'];
106
+ openAIApiKey: Config$1['openAIApiKey'];
107
+ huggingFaceHubApiKey: Config$1['huggingFaceHubApiKey'];
108
+ tokenLimit: Config$1['tokenLimit'];
106
109
  }
107
110
 
108
111
  interface CommitOptions$1 extends BaseCommandOptions {
@@ -169,7 +172,7 @@ declare const _default: {
169
172
  * @param configuration
170
173
  * @returns LLM Model
171
174
  */
172
- declare function getModel(name: string, key: string, fields?: (Partial<OpenAIInput> & Partial<AzureOpenAIInput> & BaseLLMParams) | undefined): OpenAI | HuggingFaceInference;
175
+ declare function getLlm(service: Config$1['service'], key: string, fields?: (Partial<OpenAIInput> & Partial<AzureOpenAIInput> & BaseLLMParams) | undefined): OpenAI | HuggingFaceInference;
173
176
 
174
177
  interface LoggerOptions {
175
178
  color?: typeof Color;
@@ -194,14 +197,8 @@ declare class Logger {
194
197
  stopSpinner(message?: string | undefined, options?: SpinnerOptions): Logger;
195
198
  }
196
199
 
197
- /**
198
- * Wrapper around GPT3NodeTokenizer to handle default export.
199
- *
200
- * @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
201
- *
202
- * @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
203
- */
204
- declare const getTokenizer: () => GPT3NodeTokenizer;
200
+ type TokenCounter = Awaited<ReturnType<typeof getTokenCounter>>;
201
+ declare const getTokenCounter: (modelName: ServiceModel) => Promise<(text: string) => number>;
205
202
 
206
203
  type FileChangeStatus = 'modified' | 'renamed' | 'added' | 'deleted' | 'untracked' | 'unknown';
207
204
  interface FileChange {
@@ -228,8 +225,8 @@ interface DirectoryDiff {
228
225
  tokenCount: number;
229
226
  }
230
227
  interface BaseParserOptions {
231
- tokenizer: ReturnType<typeof getTokenizer>;
232
- model: ReturnType<typeof getModel>;
228
+ tokenizer: TokenCounter;
229
+ llm: ReturnType<typeof getLlm>;
233
230
  git: SimpleGit;
234
231
  logger: Logger;
235
232
  }
@@ -8,7 +8,6 @@ import { loadSummarizationChain, LLMChain } from 'langchain/chains';
8
8
  import { OpenAI } from 'langchain/llms/openai';
9
9
  import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
10
10
  import { createTwoFilesPatch } from 'diff';
11
- import GPT3NodeTokenizer from 'gpt3-tokenizer';
12
11
  import chalk from 'chalk';
13
12
  import ora from 'ora';
14
13
  import now from 'performance-now';
@@ -23,6 +22,7 @@ import * as os from 'os';
23
22
  import os__default from 'os';
24
23
  import * as ini from 'ini';
25
24
  import { simpleGit } from 'simple-git';
25
+ import { encoding_for_model } from 'tiktoken';
26
26
 
27
27
  /**
28
28
  * Extract the path from a file path string.
@@ -84,7 +84,7 @@ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenize
84
84
  returnIntermediateSteps: true,
85
85
  },
86
86
  });
87
- const newTokenTotal = tokenizer.encode(directorySummary).text.length;
87
+ const newTokenTotal = tokenizer(directorySummary);
88
88
  return {
89
89
  diffs: directory.diffs,
90
90
  path: directory.path,
@@ -215,9 +215,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
215
215
  // Collect diffs for the files of the current node
216
216
  const diffPromises = node.files.map(async (nodeFile) => {
217
217
  const diff = await getFileDiff(nodeFile);
218
- // TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
219
- const tokenizedDiff = tokenizer.encode(diff).text;
220
- const tokenCount = tokenizedDiff.length;
218
+ const tokenCount = tokenizer(diff);
221
219
  logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
222
220
  color: 'magenta',
223
221
  });
@@ -241,18 +239,29 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
241
239
  };
242
240
  }
243
241
 
242
+ function getModelAndProviderFromService(service) {
243
+ const [provider, model] = service.split(/\/(.*)/s);
244
+ if (!model || !provider) {
245
+ throw new Error(`Invalid service: ${service}`);
246
+ }
247
+ return { provider, model };
248
+ }
249
+ function getModelFromService(service) {
250
+ const { model } = getModelAndProviderFromService(service);
251
+ return model;
252
+ }
244
253
  /**
245
254
  * Get LLM Model Based on Configuration
246
255
  * @param fields
247
256
  * @param configuration
248
257
  * @returns LLM Model
249
258
  */
250
- function getModel(name, key, fields) {
251
- const [llm, model] = name.split(/\/(.*)/s);
259
+ function getLlm(service, key, fields) {
260
+ const { provider, model } = getModelAndProviderFromService(service);
252
261
  if (!model) {
253
- throw new Error(`Invalid model: ${name}`);
262
+ throw new Error(`Invalid LLM Service: ${service}`);
254
263
  }
255
- switch (llm) {
264
+ switch (provider) {
256
265
  case 'huggingface':
257
266
  return new HuggingFaceInference({
258
267
  model: model,
@@ -271,16 +280,13 @@ function getModel(name, key, fields) {
271
280
  }
272
281
  /**
273
282
  * Retrieve appropriate API key based on selected model
274
- * @param name
283
+ * @param service
275
284
  * @param options
276
285
  * @returns
277
286
  */
278
- function getApiKeyForModel(name, options) {
279
- const [llm, model] = name.split(/\/(.*)/s);
280
- if (!model) {
281
- throw new Error(`Invalid model: ${name}`);
282
- }
283
- switch (llm) {
287
+ function getApiKeyForModel(service, options) {
288
+ const { provider } = getModelAndProviderFromService(service);
289
+ switch (provider) {
284
290
  case 'huggingface':
285
291
  return options.huggingFaceHubApiKey;
286
292
  case 'openai':
@@ -302,7 +308,7 @@ function getTextSplitter(options = {}) {
302
308
  * @param options
303
309
  * @returns
304
310
  */
305
- function getChain(model, options = { type: 'map_reduce' }) {
311
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
306
312
  return loadSummarizationChain(model, options);
307
313
  }
308
314
  function getPrompt({ template, variables, fallback }) {
@@ -402,9 +408,9 @@ async function getDiff(nodeFile, commit, { git, logger, }) {
402
408
  }
403
409
 
404
410
  const MAX_TOKENS_PER_SUMMARY = 2048;
405
- async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
411
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
406
412
  const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
407
- const summarizationChain = getChain(model, {
413
+ const summarizationChain = getSummarizationChain(model, {
408
414
  type: 'map_reduce',
409
415
  combineMapPrompt: SUMMARIZE_PROMPT,
410
416
  combinePrompt: SUMMARIZE_PROMPT,
@@ -429,28 +435,6 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, mo
429
435
  return summary;
430
436
  }
431
437
 
432
- /**
433
- * Wrapper around GPT3NodeTokenizer to handle default export.
434
- *
435
- * @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
436
- *
437
- * @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
438
- */
439
- const getTokenizer = () => {
440
- let tokenizer;
441
- // eslint-disable-next-line
442
- // @ts-ignore
443
- if (GPT3NodeTokenizer.default) {
444
- // eslint-disable-next-line
445
- // @ts-ignore
446
- tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
447
- }
448
- else {
449
- tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
450
- }
451
- return tokenizer;
452
- };
453
-
454
438
  class Logger {
455
439
  constructor(config) {
456
440
  this.config = config;
@@ -591,7 +575,7 @@ function removeUndefined(obj) {
591
575
  * @type {Config}
592
576
  */
593
577
  const DEFAULT_CONFIG = {
594
- model: 'openai/gpt-4',
578
+ service: 'openai/gpt-4',
595
579
  verbose: false,
596
580
  tokenLimit: 1024,
597
581
  summarizePrompt: SUMMARIZE_PROMPT.template,
@@ -602,7 +586,9 @@ const DEFAULT_CONFIG = {
602
586
  defaultBranch: 'main',
603
587
  };
604
588
  /**
605
- * Config keys
589
+ * Create a named export of all config keys for use in other modules.
590
+ *
591
+ * @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
606
592
  *
607
593
  * @type {string[]}
608
594
  */
@@ -741,7 +727,7 @@ function loadGitConfig(config) {
741
727
  const gitConfigParsed = ini.parse(gitConfigRaw);
742
728
  config = {
743
729
  ...config,
744
- model: gitConfigParsed.coco?.model || config.model,
730
+ service: gitConfigParsed.coco?.model || config.service,
745
731
  openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
746
732
  huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
747
733
  tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
@@ -995,41 +981,51 @@ async function editResult(result, options) {
995
981
  return result;
996
982
  }
997
983
 
998
- async function getUserReviewDecision() {
999
- return await select({
1000
- message: 'Would you like to make any changes to the commit message?',
1001
- choices: [
1002
- {
1003
- name: '✨ Looks good!',
1004
- value: 'approve',
1005
- description: 'Commit staged changes with generated commit message',
1006
- },
1007
- {
1008
- name: '📝 Edit',
1009
- value: 'edit',
1010
- description: 'Edit the commit message before proceeding',
1011
- },
1012
- {
1013
- name: '🪶 Modify Prompt',
1014
- value: 'modifyPrompt',
1015
- description: 'Modify the prompt template and regenerate the commit message',
1016
- },
1017
- {
1018
- name: '🔄 Retry - Message Only',
1019
- value: 'retryMessageOnly',
1020
- description: 'Restart the function execution from generating the commit message',
1021
- },
1022
- {
1023
- name: '🔄 Retry - Full',
1024
- value: 'retryFull',
1025
- description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
1026
- },
1027
- {
1028
- name: '💣 Cancel',
1029
- value: 'cancel',
1030
- },
1031
- ],
984
+ async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
985
+ const choices = [
986
+ {
987
+ name: '✨ Looks good!',
988
+ value: 'approve',
989
+ description: descriptions?.approve || `Continue with the generated ${label}`,
990
+ },
991
+ {
992
+ name: '📝 Edit',
993
+ value: 'edit',
994
+ description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
995
+ },
996
+ ];
997
+ if (enableModifyPrompt) {
998
+ choices.push({
999
+ name: '🪶 Modify Prompt',
1000
+ value: 'modifyPrompt',
1001
+ description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
1002
+ });
1003
+ }
1004
+ if (enableRetry) {
1005
+ choices.push({
1006
+ name: '🔄 Retry',
1007
+ value: 'retryMessageOnly',
1008
+ description: descriptions?.retryMessageOnly ||
1009
+ `Restart the function execution from generating the ${label}`,
1010
+ });
1011
+ }
1012
+ if (enableFullRetry) {
1013
+ choices.push({
1014
+ name: '🔄 Retry Full',
1015
+ value: 'retryFull',
1016
+ description: descriptions?.retryFull ||
1017
+ `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
1018
+ });
1019
+ }
1020
+ choices.push({
1021
+ name: '💣 Cancel',
1022
+ value: 'cancel',
1023
+ description: descriptions?.cancel || `Cancel the ${label}`,
1032
1024
  });
1025
+ return (await select({
1026
+ message: `Would you like to make any changes to the ${label}?`,
1027
+ choices,
1028
+ }));
1033
1029
  }
1034
1030
 
1035
1031
  async function editPrompt(options) {
@@ -1082,7 +1078,10 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
1082
1078
  .stopTimer();
1083
1079
  if (options?.interactive) {
1084
1080
  logResult(label, result);
1085
- const reviewAnswer = await getUserReviewDecision();
1081
+ const reviewAnswer = await getUserReviewDecision({
1082
+ label,
1083
+ ...options?.review || {},
1084
+ });
1086
1085
  if (reviewAnswer === 'cancel') {
1087
1086
  process.exit(0);
1088
1087
  }
@@ -1136,20 +1135,20 @@ const executeChain = async ({ llm, prompt, variables }) => {
1136
1135
  return res.text.trim();
1137
1136
  };
1138
1137
 
1139
- async function createCommit(commitMsg, git) {
1140
- return await git.commit(commitMsg);
1141
- }
1142
-
1143
1138
  const logSuccess = () => {
1144
1139
  console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
1145
1140
  };
1146
1141
 
1147
- const handleResult = async (result, { mode, git }) => {
1148
- // Handle resulting commit message
1142
+ async function handleResult({ result, mode, interactiveHandler }) {
1149
1143
  switch (mode) {
1150
1144
  case 'interactive':
1151
- await createCommit(result, git);
1152
- logSuccess();
1145
+ if (interactiveHandler) {
1146
+ await interactiveHandler(result);
1147
+ }
1148
+ else {
1149
+ console.error('No result handler provided for interactive mode');
1150
+ logSuccess();
1151
+ }
1153
1152
  break;
1154
1153
  case 'stdout':
1155
1154
  default:
@@ -1157,7 +1156,7 @@ const handleResult = async (result, { mode, git }) => {
1157
1156
  break;
1158
1157
  }
1159
1158
  process.exit(0);
1160
- };
1159
+ }
1161
1160
 
1162
1161
  const getRepo = () => {
1163
1162
  let git;
@@ -1171,17 +1170,32 @@ const getRepo = () => {
1171
1170
  return git;
1172
1171
  };
1173
1172
 
1173
+ const getTikToken = async (modelName) => {
1174
+ return await encoding_for_model(modelName);
1175
+ };
1176
+ const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1177
+ // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1178
+ const tokens = tokenizer.encode(text);
1179
+ // console.log('Tokens', { tokenCount: tokens.length })
1180
+ return tokens.length;
1181
+ });
1182
+
1183
+ async function createCommit(commitMsg, git) {
1184
+ return await git.commit(commitMsg);
1185
+ }
1186
+
1174
1187
  async function handler$2(argv) {
1175
- const tokenizer = getTokenizer();
1176
1188
  const git = getRepo();
1177
1189
  const options = loadConfig(argv);
1190
+ const { service } = options;
1178
1191
  const logger = new Logger(options);
1179
- const key = getApiKeyForModel(options.model, options);
1192
+ const key = getApiKeyForModel(service, options);
1193
+ const tokenizer = await getTokenCounter(getModelFromService(service));
1180
1194
  if (!key) {
1181
1195
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1182
1196
  process.exit(1);
1183
1197
  }
1184
- const model = getModel(options.model, key, {
1198
+ const llm = getLlm(service, key, {
1185
1199
  temperature: 0.4,
1186
1200
  maxConcurrency: 10,
1187
1201
  });
@@ -1194,16 +1208,16 @@ async function handler$2(argv) {
1194
1208
  return await fileChangeParser({
1195
1209
  changes,
1196
1210
  commit: '--staged',
1197
- options: { tokenizer, git, model, logger },
1211
+ options: { tokenizer, git, llm, logger },
1198
1212
  });
1199
1213
  }
1200
1214
  const commitMsg = await generateAndReviewLoop({
1201
- label: 'Commit Message',
1215
+ label: 'commit message',
1202
1216
  factory,
1203
1217
  parser,
1204
1218
  agent: async (context, options) => {
1205
1219
  return await executeChain({
1206
- llm: model,
1220
+ llm,
1207
1221
  prompt: getPrompt({
1208
1222
  template: options.prompt,
1209
1223
  variables: COMMIT_PROMPT.inputVariables,
@@ -1221,12 +1235,25 @@ async function handler$2(argv) {
1221
1235
  prompt: options.prompt || COMMIT_PROMPT.template,
1222
1236
  logger,
1223
1237
  interactive: INTERACTIVE,
1238
+ review: {
1239
+ descriptions: {
1240
+ approve: `Commit staged changes with generated commit message`,
1241
+ edit: 'Edit the commit message before proceeding',
1242
+ modifyPrompt: 'Modify the prompt template and regenerate the commit message',
1243
+ retryMessageOnly: 'Restart the function execution from generating the commit message',
1244
+ retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
1245
+ },
1246
+ },
1224
1247
  },
1225
1248
  });
1226
1249
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1227
- handleResult(commitMsg, {
1250
+ handleResult({
1251
+ result: commitMsg,
1252
+ interactiveHandler: async (result) => {
1253
+ await createCommit(result, git);
1254
+ logSuccess();
1255
+ },
1228
1256
  mode: MODE,
1229
- git,
1230
1257
  });
1231
1258
  }
1232
1259
 
@@ -1368,12 +1395,12 @@ async function handler$1(argv) {
1368
1395
  const options = loadConfig(argv);
1369
1396
  const logger = new Logger(options);
1370
1397
  const git = getRepo();
1371
- const key = getApiKeyForModel(options.model, options);
1398
+ const key = getApiKeyForModel(options.service, options);
1372
1399
  if (!key) {
1373
1400
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1374
1401
  process.exit(1);
1375
1402
  }
1376
- const model = getModel(options.model, key, {
1403
+ const model = getLlm(options.service, key, {
1377
1404
  temperature: 0.4,
1378
1405
  maxConcurrency: 10,
1379
1406
  });
@@ -1395,7 +1422,7 @@ async function handler$1(argv) {
1395
1422
  return result;
1396
1423
  }
1397
1424
  const changelogMsg = await generateAndReviewLoop({
1398
- label: 'Changelog',
1425
+ label: 'changelog',
1399
1426
  factory,
1400
1427
  parser,
1401
1428
  agent: async (context, options) => {
@@ -1423,12 +1450,18 @@ async function handler$1(argv) {
1423
1450
  prompt: options.prompt || CHANGELOG_PROMPT.template,
1424
1451
  logger,
1425
1452
  interactive: INTERACTIVE,
1453
+ review: {
1454
+ enableFullRetry: false,
1455
+ }
1426
1456
  },
1427
1457
  });
1428
1458
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1429
- handleResult(changelogMsg, {
1459
+ handleResult({
1460
+ result: changelogMsg,
1461
+ interactiveHandler: async () => {
1462
+ logSuccess();
1463
+ },
1430
1464
  mode: MODE,
1431
- git,
1432
1465
  });
1433
1466
  }
1434
1467
 
@@ -1724,25 +1757,23 @@ var types = /*#__PURE__*/Object.freeze({
1724
1757
  __proto__: null
1725
1758
  });
1726
1759
 
1727
- yargs
1728
- .scriptName('coco')
1729
- .usage('$0 <cmd> [args]')
1730
- .command([commit.command, '$0'], commit.desc,
1760
+ const y = yargs();
1761
+ y.scriptName('coco').usage('$0 <cmd> [args]');
1762
+ y.command([commit.command, '$0'], commit.desc,
1731
1763
  // TODO: fix type on builder
1732
1764
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1733
1765
  // @ts-ignore
1734
- commit.builder, commit.handler)
1735
- .command(changelog.command, changelog.desc,
1766
+ commit.builder, commit.handler);
1767
+ y.command(changelog.command, changelog.desc,
1736
1768
  // TODO: fix type on builder
1737
1769
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1738
1770
  // @ts-ignore
1739
- changelog.builder, changelog.handler)
1740
- .command(init.command, init.desc,
1771
+ changelog.builder, changelog.handler);
1772
+ y.command(init.command, init.desc,
1741
1773
  // TODO: fix type on builder
1742
1774
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1743
1775
  // @ts-ignore
1744
- init.builder, init.handler)
1745
- .demandCommand()
1746
- .help().argv;
1776
+ init.builder, init.handler);
1777
+ y.parse(process.argv.slice(2));
1747
1778
 
1748
1779
  export { changelog, commit, init, types };
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ var chains = require('langchain/chains');
10
10
  var openai = require('langchain/llms/openai');
11
11
  var text_splitter = require('langchain/text_splitter');
12
12
  var diff = require('diff');
13
- var GPT3NodeTokenizer = require('gpt3-tokenizer');
14
13
  var chalk = require('chalk');
15
14
  var ora = require('ora');
16
15
  var now = require('performance-now');
@@ -22,6 +21,7 @@ var prompts$1 = require('@inquirer/prompts');
22
21
  var os = require('os');
23
22
  var ini = require('ini');
24
23
  var simpleGit = require('simple-git');
24
+ var tiktoken = require('tiktoken');
25
25
 
26
26
  function _interopNamespaceDefault(e) {
27
27
  var n = Object.create(null);
@@ -105,7 +105,7 @@ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenize
105
105
  returnIntermediateSteps: true,
106
106
  },
107
107
  });
108
- const newTokenTotal = tokenizer.encode(directorySummary).text.length;
108
+ const newTokenTotal = tokenizer(directorySummary);
109
109
  return {
110
110
  diffs: directory.diffs,
111
111
  path: directory.path,
@@ -236,9 +236,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
236
236
  // Collect diffs for the files of the current node
237
237
  const diffPromises = node.files.map(async (nodeFile) => {
238
238
  const diff = await getFileDiff(nodeFile);
239
- // TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
240
- const tokenizedDiff = tokenizer.encode(diff).text;
241
- const tokenCount = tokenizedDiff.length;
239
+ const tokenCount = tokenizer(diff);
242
240
  logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
243
241
  color: 'magenta',
244
242
  });
@@ -262,18 +260,29 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
262
260
  };
263
261
  }
264
262
 
263
+ function getModelAndProviderFromService(service) {
264
+ const [provider, model] = service.split(/\/(.*)/s);
265
+ if (!model || !provider) {
266
+ throw new Error(`Invalid service: ${service}`);
267
+ }
268
+ return { provider, model };
269
+ }
270
+ function getModelFromService(service) {
271
+ const { model } = getModelAndProviderFromService(service);
272
+ return model;
273
+ }
265
274
  /**
266
275
  * Get LLM Model Based on Configuration
267
276
  * @param fields
268
277
  * @param configuration
269
278
  * @returns LLM Model
270
279
  */
271
- function getModel(name, key, fields) {
272
- const [llm, model] = name.split(/\/(.*)/s);
280
+ function getLlm(service, key, fields) {
281
+ const { provider, model } = getModelAndProviderFromService(service);
273
282
  if (!model) {
274
- throw new Error(`Invalid model: ${name}`);
283
+ throw new Error(`Invalid LLM Service: ${service}`);
275
284
  }
276
- switch (llm) {
285
+ switch (provider) {
277
286
  case 'huggingface':
278
287
  return new hf.HuggingFaceInference({
279
288
  model: model,
@@ -292,16 +301,13 @@ function getModel(name, key, fields) {
292
301
  }
293
302
  /**
294
303
  * Retrieve appropriate API key based on selected model
295
- * @param name
304
+ * @param service
296
305
  * @param options
297
306
  * @returns
298
307
  */
299
- function getApiKeyForModel(name, options) {
300
- const [llm, model] = name.split(/\/(.*)/s);
301
- if (!model) {
302
- throw new Error(`Invalid model: ${name}`);
303
- }
304
- switch (llm) {
308
+ function getApiKeyForModel(service, options) {
309
+ const { provider } = getModelAndProviderFromService(service);
310
+ switch (provider) {
305
311
  case 'huggingface':
306
312
  return options.huggingFaceHubApiKey;
307
313
  case 'openai':
@@ -323,7 +329,7 @@ function getTextSplitter(options = {}) {
323
329
  * @param options
324
330
  * @returns
325
331
  */
326
- function getChain(model, options = { type: 'map_reduce' }) {
332
+ function getSummarizationChain(model, options = { type: 'map_reduce' }) {
327
333
  return chains.loadSummarizationChain(model, options);
328
334
  }
329
335
  function getPrompt({ template, variables, fallback }) {
@@ -423,9 +429,9 @@ async function getDiff(nodeFile, commit, { git, logger, }) {
423
429
  }
424
430
 
425
431
  const MAX_TOKENS_PER_SUMMARY = 2048;
426
- async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
432
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
427
433
  const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
428
- const summarizationChain = getChain(model, {
434
+ const summarizationChain = getSummarizationChain(model, {
429
435
  type: 'map_reduce',
430
436
  combineMapPrompt: SUMMARIZE_PROMPT,
431
437
  combinePrompt: SUMMARIZE_PROMPT,
@@ -450,28 +456,6 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, mo
450
456
  return summary;
451
457
  }
452
458
 
453
- /**
454
- * Wrapper around GPT3NodeTokenizer to handle default export.
455
- *
456
- * @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
457
- *
458
- * @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
459
- */
460
- const getTokenizer = () => {
461
- let tokenizer;
462
- // eslint-disable-next-line
463
- // @ts-ignore
464
- if (GPT3NodeTokenizer.default) {
465
- // eslint-disable-next-line
466
- // @ts-ignore
467
- tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
468
- }
469
- else {
470
- tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
471
- }
472
- return tokenizer;
473
- };
474
-
475
459
  class Logger {
476
460
  constructor(config) {
477
461
  this.config = config;
@@ -612,7 +596,7 @@ function removeUndefined(obj) {
612
596
  * @type {Config}
613
597
  */
614
598
  const DEFAULT_CONFIG = {
615
- model: 'openai/gpt-4',
599
+ service: 'openai/gpt-4',
616
600
  verbose: false,
617
601
  tokenLimit: 1024,
618
602
  summarizePrompt: SUMMARIZE_PROMPT.template,
@@ -623,7 +607,9 @@ const DEFAULT_CONFIG = {
623
607
  defaultBranch: 'main',
624
608
  };
625
609
  /**
626
- * Config keys
610
+ * Create a named export of all config keys for use in other modules.
611
+ *
612
+ * @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
627
613
  *
628
614
  * @type {string[]}
629
615
  */
@@ -762,7 +748,7 @@ function loadGitConfig(config) {
762
748
  const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
763
749
  config = {
764
750
  ...config,
765
- model: gitConfigParsed.coco?.model || config.model,
751
+ service: gitConfigParsed.coco?.model || config.service,
766
752
  openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
767
753
  huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
768
754
  tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
@@ -1016,41 +1002,51 @@ async function editResult(result, options) {
1016
1002
  return result;
1017
1003
  }
1018
1004
 
1019
- async function getUserReviewDecision() {
1020
- return await prompts$1.select({
1021
- message: 'Would you like to make any changes to the commit message?',
1022
- choices: [
1023
- {
1024
- name: '✨ Looks good!',
1025
- value: 'approve',
1026
- description: 'Commit staged changes with generated commit message',
1027
- },
1028
- {
1029
- name: '📝 Edit',
1030
- value: 'edit',
1031
- description: 'Edit the commit message before proceeding',
1032
- },
1033
- {
1034
- name: '🪶 Modify Prompt',
1035
- value: 'modifyPrompt',
1036
- description: 'Modify the prompt template and regenerate the commit message',
1037
- },
1038
- {
1039
- name: '🔄 Retry - Message Only',
1040
- value: 'retryMessageOnly',
1041
- description: 'Restart the function execution from generating the commit message',
1042
- },
1043
- {
1044
- name: '🔄 Retry - Full',
1045
- value: 'retryFull',
1046
- description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
1047
- },
1048
- {
1049
- name: '💣 Cancel',
1050
- value: 'cancel',
1051
- },
1052
- ],
1005
+ async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
1006
+ const choices = [
1007
+ {
1008
+ name: '✨ Looks good!',
1009
+ value: 'approve',
1010
+ description: descriptions?.approve || `Continue with the generated ${label}`,
1011
+ },
1012
+ {
1013
+ name: '📝 Edit',
1014
+ value: 'edit',
1015
+ description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
1016
+ },
1017
+ ];
1018
+ if (enableModifyPrompt) {
1019
+ choices.push({
1020
+ name: '🪶 Modify Prompt',
1021
+ value: 'modifyPrompt',
1022
+ description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
1023
+ });
1024
+ }
1025
+ if (enableRetry) {
1026
+ choices.push({
1027
+ name: '🔄 Retry',
1028
+ value: 'retryMessageOnly',
1029
+ description: descriptions?.retryMessageOnly ||
1030
+ `Restart the function execution from generating the ${label}`,
1031
+ });
1032
+ }
1033
+ if (enableFullRetry) {
1034
+ choices.push({
1035
+ name: '🔄 Retry Full',
1036
+ value: 'retryFull',
1037
+ description: descriptions?.retryFull ||
1038
+ `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
1039
+ });
1040
+ }
1041
+ choices.push({
1042
+ name: '💣 Cancel',
1043
+ value: 'cancel',
1044
+ description: descriptions?.cancel || `Cancel the ${label}`,
1053
1045
  });
1046
+ return (await prompts$1.select({
1047
+ message: `Would you like to make any changes to the ${label}?`,
1048
+ choices,
1049
+ }));
1054
1050
  }
1055
1051
 
1056
1052
  async function editPrompt(options) {
@@ -1103,7 +1099,10 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
1103
1099
  .stopTimer();
1104
1100
  if (options?.interactive) {
1105
1101
  logResult(label, result);
1106
- const reviewAnswer = await getUserReviewDecision();
1102
+ const reviewAnswer = await getUserReviewDecision({
1103
+ label,
1104
+ ...options?.review || {},
1105
+ });
1107
1106
  if (reviewAnswer === 'cancel') {
1108
1107
  process.exit(0);
1109
1108
  }
@@ -1157,20 +1156,20 @@ const executeChain = async ({ llm, prompt, variables }) => {
1157
1156
  return res.text.trim();
1158
1157
  };
1159
1158
 
1160
- async function createCommit(commitMsg, git) {
1161
- return await git.commit(commitMsg);
1162
- }
1163
-
1164
1159
  const logSuccess = () => {
1165
1160
  console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
1166
1161
  };
1167
1162
 
1168
- const handleResult = async (result, { mode, git }) => {
1169
- // Handle resulting commit message
1163
+ async function handleResult({ result, mode, interactiveHandler }) {
1170
1164
  switch (mode) {
1171
1165
  case 'interactive':
1172
- await createCommit(result, git);
1173
- logSuccess();
1166
+ if (interactiveHandler) {
1167
+ await interactiveHandler(result);
1168
+ }
1169
+ else {
1170
+ console.error('No result handler provided for interactive mode');
1171
+ logSuccess();
1172
+ }
1174
1173
  break;
1175
1174
  case 'stdout':
1176
1175
  default:
@@ -1178,7 +1177,7 @@ const handleResult = async (result, { mode, git }) => {
1178
1177
  break;
1179
1178
  }
1180
1179
  process.exit(0);
1181
- };
1180
+ }
1182
1181
 
1183
1182
  const getRepo = () => {
1184
1183
  let git;
@@ -1192,17 +1191,32 @@ const getRepo = () => {
1192
1191
  return git;
1193
1192
  };
1194
1193
 
1194
+ const getTikToken = async (modelName) => {
1195
+ return await tiktoken.encoding_for_model(modelName);
1196
+ };
1197
+ const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
1198
+ // console.log('Running GetTokenCount', { tokenizer, length: text.length })
1199
+ const tokens = tokenizer.encode(text);
1200
+ // console.log('Tokens', { tokenCount: tokens.length })
1201
+ return tokens.length;
1202
+ });
1203
+
1204
+ async function createCommit(commitMsg, git) {
1205
+ return await git.commit(commitMsg);
1206
+ }
1207
+
1195
1208
  async function handler$2(argv) {
1196
- const tokenizer = getTokenizer();
1197
1209
  const git = getRepo();
1198
1210
  const options = loadConfig(argv);
1211
+ const { service } = options;
1199
1212
  const logger = new Logger(options);
1200
- const key = getApiKeyForModel(options.model, options);
1213
+ const key = getApiKeyForModel(service, options);
1214
+ const tokenizer = await getTokenCounter(getModelFromService(service));
1201
1215
  if (!key) {
1202
1216
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1203
1217
  process.exit(1);
1204
1218
  }
1205
- const model = getModel(options.model, key, {
1219
+ const llm = getLlm(service, key, {
1206
1220
  temperature: 0.4,
1207
1221
  maxConcurrency: 10,
1208
1222
  });
@@ -1215,16 +1229,16 @@ async function handler$2(argv) {
1215
1229
  return await fileChangeParser({
1216
1230
  changes,
1217
1231
  commit: '--staged',
1218
- options: { tokenizer, git, model, logger },
1232
+ options: { tokenizer, git, llm, logger },
1219
1233
  });
1220
1234
  }
1221
1235
  const commitMsg = await generateAndReviewLoop({
1222
- label: 'Commit Message',
1236
+ label: 'commit message',
1223
1237
  factory,
1224
1238
  parser,
1225
1239
  agent: async (context, options) => {
1226
1240
  return await executeChain({
1227
- llm: model,
1241
+ llm,
1228
1242
  prompt: getPrompt({
1229
1243
  template: options.prompt,
1230
1244
  variables: COMMIT_PROMPT.inputVariables,
@@ -1242,12 +1256,25 @@ async function handler$2(argv) {
1242
1256
  prompt: options.prompt || COMMIT_PROMPT.template,
1243
1257
  logger,
1244
1258
  interactive: INTERACTIVE,
1259
+ review: {
1260
+ descriptions: {
1261
+ approve: `Commit staged changes with generated commit message`,
1262
+ edit: 'Edit the commit message before proceeding',
1263
+ modifyPrompt: 'Modify the prompt template and regenerate the commit message',
1264
+ retryMessageOnly: 'Restart the function execution from generating the commit message',
1265
+ retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
1266
+ },
1267
+ },
1245
1268
  },
1246
1269
  });
1247
1270
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1248
- handleResult(commitMsg, {
1271
+ handleResult({
1272
+ result: commitMsg,
1273
+ interactiveHandler: async (result) => {
1274
+ await createCommit(result, git);
1275
+ logSuccess();
1276
+ },
1249
1277
  mode: MODE,
1250
- git,
1251
1278
  });
1252
1279
  }
1253
1280
 
@@ -1389,12 +1416,12 @@ async function handler$1(argv) {
1389
1416
  const options = loadConfig(argv);
1390
1417
  const logger = new Logger(options);
1391
1418
  const git = getRepo();
1392
- const key = getApiKeyForModel(options.model, options);
1419
+ const key = getApiKeyForModel(options.service, options);
1393
1420
  if (!key) {
1394
1421
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1395
1422
  process.exit(1);
1396
1423
  }
1397
- const model = getModel(options.model, key, {
1424
+ const model = getLlm(options.service, key, {
1398
1425
  temperature: 0.4,
1399
1426
  maxConcurrency: 10,
1400
1427
  });
@@ -1416,7 +1443,7 @@ async function handler$1(argv) {
1416
1443
  return result;
1417
1444
  }
1418
1445
  const changelogMsg = await generateAndReviewLoop({
1419
- label: 'Changelog',
1446
+ label: 'changelog',
1420
1447
  factory,
1421
1448
  parser,
1422
1449
  agent: async (context, options) => {
@@ -1444,12 +1471,18 @@ async function handler$1(argv) {
1444
1471
  prompt: options.prompt || CHANGELOG_PROMPT.template,
1445
1472
  logger,
1446
1473
  interactive: INTERACTIVE,
1474
+ review: {
1475
+ enableFullRetry: false,
1476
+ }
1447
1477
  },
1448
1478
  });
1449
1479
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1450
- handleResult(changelogMsg, {
1480
+ handleResult({
1481
+ result: changelogMsg,
1482
+ interactiveHandler: async () => {
1483
+ logSuccess();
1484
+ },
1451
1485
  mode: MODE,
1452
- git,
1453
1486
  });
1454
1487
  }
1455
1488
 
@@ -1745,26 +1778,24 @@ var types = /*#__PURE__*/Object.freeze({
1745
1778
  __proto__: null
1746
1779
  });
1747
1780
 
1748
- yargs
1749
- .scriptName('coco')
1750
- .usage('$0 <cmd> [args]')
1751
- .command([commit.command, '$0'], commit.desc,
1781
+ const y = yargs();
1782
+ y.scriptName('coco').usage('$0 <cmd> [args]');
1783
+ y.command([commit.command, '$0'], commit.desc,
1752
1784
  // TODO: fix type on builder
1753
1785
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1754
1786
  // @ts-ignore
1755
- commit.builder, commit.handler)
1756
- .command(changelog.command, changelog.desc,
1787
+ commit.builder, commit.handler);
1788
+ y.command(changelog.command, changelog.desc,
1757
1789
  // TODO: fix type on builder
1758
1790
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1759
1791
  // @ts-ignore
1760
- changelog.builder, changelog.handler)
1761
- .command(init.command, init.desc,
1792
+ changelog.builder, changelog.handler);
1793
+ y.command(init.command, init.desc,
1762
1794
  // TODO: fix type on builder
1763
1795
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1764
1796
  // @ts-ignore
1765
- init.builder, init.handler)
1766
- .demandCommand()
1767
- .help().argv;
1797
+ init.builder, init.handler);
1798
+ y.parse(process.argv.slice(2));
1768
1799
 
1769
1800
  exports.changelog = changelog;
1770
1801
  exports.commit = commit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -82,7 +82,6 @@
82
82
  "@inquirer/prompts": "3.3.0",
83
83
  "chalk": "4.1.2",
84
84
  "diff": "5.1.0",
85
- "gpt3-tokenizer": "1.1.5",
86
85
  "ini": "4.1.1",
87
86
  "langchain": "0.0.196",
88
87
  "minimatch": "9.0.3",
@@ -91,6 +90,7 @@
91
90
  "performance-now": "2.1.0",
92
91
  "pretty-ms": "7.0.1",
93
92
  "simple-git": "3.21.0",
93
+ "tiktoken": "1.0.11",
94
94
  "yargs": "17.7.2"
95
95
  }
96
96
  }