git-coco 0.18.2 → 0.20.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.
@@ -18,13 +18,13 @@ import prettyMilliseconds from 'pretty-ms';
18
18
  import { ChatAnthropic } from '@langchain/anthropic';
19
19
  import { ChatOllama } from '@langchain/ollama';
20
20
  import { ChatOpenAI } from '@langchain/openai';
21
- import { StructuredOutputParser, BaseOutputParser, OutputParserException, StringOutputParser } from '@langchain/core/output_parsers';
22
- import { simpleGit } from 'simple-git';
21
+ import { BaseOutputParser, OutputParserException, StructuredOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
23
22
  import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
24
23
  import { ensureConfig, Runnable } from '@langchain/core/runnables';
25
24
  import { RUN_KEY } from '@langchain/core/outputs';
26
25
  import { CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager';
27
26
  import '@langchain/core/utils/json_patch';
27
+ import { simpleGit } from 'simple-git';
28
28
  import pQueue from 'p-queue';
29
29
  import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
30
30
  import { createTwoFilesPatch } from 'diff';
@@ -46,7 +46,7 @@ import * as readline from 'readline';
46
46
  /**
47
47
  * Current build version from package.json
48
48
  */
49
- const BUILD_VERSION = "0.18.2";
49
+ const BUILD_VERSION = "0.20.0";
50
50
 
51
51
  const isInteractive = (config) => {
52
52
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -984,7 +984,7 @@ const schema$1 = {
984
984
  "deprecated": "Use \"model\" instead."
985
985
  },
986
986
  "model": {
987
- "type": "string",
987
+ "$ref": "#/definitions/OpenAIChatModelId",
988
988
  "description": "Model name to use"
989
989
  },
990
990
  "modelKwargs": {
@@ -1462,6 +1462,79 @@ const schema$1 = {
1462
1462
  "additionalProperties": false,
1463
1463
  "description": "Base class for all caches. All caches should extend this class."
1464
1464
  },
1465
+ "OpenAIChatModelId": {
1466
+ "anyOf": [
1467
+ {
1468
+ "$ref": "#/definitions/OpenAI.ChatModel"
1469
+ },
1470
+ {
1471
+ "type": "string"
1472
+ }
1473
+ ]
1474
+ },
1475
+ "OpenAI.ChatModel": {
1476
+ "$ref": "#/definitions/ChatModel"
1477
+ },
1478
+ "ChatModel": {
1479
+ "type": "string",
1480
+ "enum": [
1481
+ "gpt-4.1",
1482
+ "gpt-4.1-mini",
1483
+ "gpt-4.1-nano",
1484
+ "gpt-4.1-2025-04-14",
1485
+ "gpt-4.1-mini-2025-04-14",
1486
+ "gpt-4.1-nano-2025-04-14",
1487
+ "o4-mini",
1488
+ "o4-mini-2025-04-16",
1489
+ "o3",
1490
+ "o3-2025-04-16",
1491
+ "o3-mini",
1492
+ "o3-mini-2025-01-31",
1493
+ "o1",
1494
+ "o1-2024-12-17",
1495
+ "o1-preview",
1496
+ "o1-preview-2024-09-12",
1497
+ "o1-mini",
1498
+ "o1-mini-2024-09-12",
1499
+ "gpt-4o",
1500
+ "gpt-4o-2024-11-20",
1501
+ "gpt-4o-2024-08-06",
1502
+ "gpt-4o-2024-05-13",
1503
+ "gpt-4o-audio-preview",
1504
+ "gpt-4o-audio-preview-2024-10-01",
1505
+ "gpt-4o-audio-preview-2024-12-17",
1506
+ "gpt-4o-audio-preview-2025-06-03",
1507
+ "gpt-4o-mini-audio-preview",
1508
+ "gpt-4o-mini-audio-preview-2024-12-17",
1509
+ "gpt-4o-search-preview",
1510
+ "gpt-4o-mini-search-preview",
1511
+ "gpt-4o-search-preview-2025-03-11",
1512
+ "gpt-4o-mini-search-preview-2025-03-11",
1513
+ "chatgpt-4o-latest",
1514
+ "codex-mini-latest",
1515
+ "gpt-4o-mini",
1516
+ "gpt-4o-mini-2024-07-18",
1517
+ "gpt-4-turbo",
1518
+ "gpt-4-turbo-2024-04-09",
1519
+ "gpt-4-0125-preview",
1520
+ "gpt-4-turbo-preview",
1521
+ "gpt-4-1106-preview",
1522
+ "gpt-4-vision-preview",
1523
+ "gpt-4",
1524
+ "gpt-4-0314",
1525
+ "gpt-4-0613",
1526
+ "gpt-4-32k",
1527
+ "gpt-4-32k-0314",
1528
+ "gpt-4-32k-0613",
1529
+ "gpt-3.5-turbo",
1530
+ "gpt-3.5-turbo-16k",
1531
+ "gpt-3.5-turbo-0301",
1532
+ "gpt-3.5-turbo-0613",
1533
+ "gpt-3.5-turbo-1106",
1534
+ "gpt-3.5-turbo-0125",
1535
+ "gpt-3.5-turbo-16k-0613"
1536
+ ]
1537
+ },
1465
1538
  "OllamaLLMService": {
1466
1539
  "type": "object",
1467
1540
  "additionalProperties": false,
@@ -6128,1416 +6201,1424 @@ function getPrompt({ template, variables, fallback }) {
6128
6201
  throw new LangChainExecutionError('getPrompt: Unexpected execution path - neither template nor fallback available', { template, fallback, variables });
6129
6202
  }
6130
6203
 
6204
+ new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
6205
+
6131
6206
  /**
6132
- * Executes a LangChain pipeline with the provided LLM, prompt, variables, and parser.
6133
- * @param params - The execution parameters
6134
- * @returns The parsed result from the LLM chain
6135
- * @throws LangChainExecutionError if the chain execution fails or returns empty results
6207
+ * Base interface that all chains must implement.
6136
6208
  */
6137
- const executeChain = async ({ llm, prompt, variables, parser }) => {
6138
- validateRequired(llm, 'llm', 'executeChain');
6139
- validateRequired(prompt, 'prompt', 'executeChain');
6140
- validateRequired(variables, 'variables', 'executeChain');
6141
- validateRequired(parser, 'parser', 'executeChain');
6142
- // Validate that variables is an object
6143
- if (typeof variables !== 'object' || Array.isArray(variables)) {
6144
- throw new LangChainExecutionError('executeChain: Variables must be a non-array object', { variables, type: typeof variables, isArray: Array.isArray(variables) });
6209
+ class BaseChain extends BaseLangChain {
6210
+ get lc_namespace() {
6211
+ return ["langchain", "chains", this._chainType()];
6145
6212
  }
6146
- try {
6147
- const chain = prompt.pipe(llm).pipe(parser);
6148
- const result = await chain.invoke(variables);
6149
- if (result === null || result === undefined) {
6150
- throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
6213
+ constructor(fields,
6214
+ /** @deprecated */
6215
+ verbose,
6216
+ /** @deprecated */
6217
+ callbacks) {
6218
+ if (arguments.length === 1 &&
6219
+ typeof fields === "object" &&
6220
+ !("saveContext" in fields)) {
6221
+ // fields is not a BaseMemory
6222
+ const { memory, callbackManager, ...rest } = fields;
6223
+ super({ ...rest, callbacks: callbackManager ?? rest.callbacks });
6224
+ this.memory = memory;
6225
+ }
6226
+ else {
6227
+ // fields is a BaseMemory
6228
+ super({ verbose, callbacks });
6229
+ this.memory = fields;
6151
6230
  }
6152
- return result;
6153
6231
  }
6154
- catch (error) {
6155
- // Re-throw LangChain errors as-is
6156
- if (error instanceof LangChainExecutionError) {
6157
- throw error;
6232
+ /** @ignore */
6233
+ _selectMemoryInputs(values) {
6234
+ const valuesForMemory = { ...values };
6235
+ if ("signal" in valuesForMemory) {
6236
+ delete valuesForMemory.signal;
6158
6237
  }
6159
- // Wrap other errors with context
6160
- handleLangChainError(error, 'executeChain: Chain execution failed', {
6161
- promptInputVariables: prompt.inputVariables,
6162
- variableKeys: Object.keys(variables),
6163
- parserType: parser.constructor.name
6238
+ if ("timeout" in valuesForMemory) {
6239
+ delete valuesForMemory.timeout;
6240
+ }
6241
+ return valuesForMemory;
6242
+ }
6243
+ /**
6244
+ * Invoke the chain with the provided input and returns the output.
6245
+ * @param input Input values for the chain run.
6246
+ * @param config Optional configuration for the Runnable.
6247
+ * @returns Promise that resolves with the output of the chain run.
6248
+ */
6249
+ async invoke(input, options) {
6250
+ const config = ensureConfig(options);
6251
+ const fullValues = await this._formatValues(input);
6252
+ const callbackManager_ = await CallbackManager.configure(config?.callbacks, this.callbacks, config?.tags, this.tags, config?.metadata, this.metadata, { verbose: this.verbose });
6253
+ const runManager = await callbackManager_?.handleChainStart(this.toJSON(), fullValues, undefined, undefined, undefined, undefined, config?.runName);
6254
+ let outputValues;
6255
+ try {
6256
+ outputValues = await (fullValues.signal
6257
+ ? Promise.race([
6258
+ this._call(fullValues, runManager, config),
6259
+ new Promise((_, reject) => {
6260
+ fullValues.signal?.addEventListener("abort", () => {
6261
+ reject(new Error("AbortError"));
6262
+ });
6263
+ }),
6264
+ ])
6265
+ : this._call(fullValues, runManager, config));
6266
+ }
6267
+ catch (e) {
6268
+ await runManager?.handleChainError(e);
6269
+ throw e;
6270
+ }
6271
+ if (!(this.memory == null)) {
6272
+ await this.memory.saveContext(this._selectMemoryInputs(input), outputValues);
6273
+ }
6274
+ await runManager?.handleChainEnd(outputValues);
6275
+ // add the runManager's currentRunId to the outputValues
6276
+ Object.defineProperty(outputValues, RUN_KEY, {
6277
+ value: runManager ? { runId: runManager?.runId } : undefined,
6278
+ configurable: true,
6164
6279
  });
6280
+ return outputValues;
6165
6281
  }
6166
- };
6167
-
6168
- function extractTicketIdFromBranchName(branchName) {
6169
- const regex = /((?<!([A-Z]+)-?)[A-Z]+-\d+)/;
6170
- const match = branchName.match(regex);
6171
- return match ? match[0] : null;
6172
- }
6173
-
6174
- /**
6175
- * Formats a commit log into a readable string format.
6176
- *
6177
- * @param commitLog - The commit log result containing an array of commit details.
6178
- * @returns An array of formatted commit log strings.
6179
- *
6180
- * Each formatted string includes:
6181
- * - The date of the commit in square brackets.
6182
- * - The commit message.
6183
- * - The commit body.
6184
- * - The commit hash in parentheses.
6185
- * - The author's name and email in angle brackets.
6186
- */
6187
- const formatCommitLog = (commitLog) => {
6188
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6189
- };
6190
-
6191
- const getChangesSinceLastTag = async ({ git }) => {
6192
- const tags = await git.tags();
6193
- if (tags.all.length > 0) {
6194
- const lastTag = tags.latest;
6195
- const commitLog = await git.log({ from: lastTag });
6196
- return formatCommitLog(commitLog);
6282
+ _validateOutputs(outputs) {
6283
+ const missingKeys = this.outputKeys.filter((k) => !(k in outputs));
6284
+ if (missingKeys.length) {
6285
+ throw new Error(`Missing output keys: ${missingKeys.join(", ")} from chain ${this._chainType()}`);
6286
+ }
6197
6287
  }
6198
- else {
6199
- return ['No tags found in the repository.'];
6288
+ async prepOutputs(inputs, outputs, returnOnlyOutputs = false) {
6289
+ this._validateOutputs(outputs);
6290
+ if (this.memory) {
6291
+ await this.memory.saveContext(inputs, outputs);
6292
+ }
6293
+ if (returnOnlyOutputs) {
6294
+ return outputs;
6295
+ }
6296
+ return { ...inputs, ...outputs };
6200
6297
  }
6201
- };
6202
-
6203
- /**
6204
- * Retrieves the commit log range between two specified commits (inclusive of both commits).
6205
- *
6206
- * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6207
- * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6208
- * @param options - Additional options for retrieving the commit log range.
6209
- * @returns A promise that resolves to an array of commit log messages, including both the 'from' and 'to' commits.
6210
- * @throws If there is an error retrieving the commit log range.
6211
- */
6212
- async function getCommitLogRange(from, to, { noMerges, git }) {
6213
- try {
6214
- // Use from^..to to include the 'from' commit in the range
6215
- // This works because from^..to means "commits reachable from 'to' but not from the parent of 'from'"
6216
- const logOptions = {
6217
- from: `${from}^`,
6218
- to,
6219
- '--no-merges': noMerges
6220
- };
6221
- const commitLog = await git.log(logOptions);
6222
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6298
+ /**
6299
+ * Return a json-like object representing this chain.
6300
+ */
6301
+ serialize() {
6302
+ throw new Error("Method not implemented.");
6223
6303
  }
6224
- catch (error) {
6225
- // If from^ fails (e.g., 'from' is the first commit), fall back to using from..to and manually adding the 'from' commit
6226
- if (error instanceof Error && error.message.includes('unknown revision')) {
6227
- try {
6228
- // Get the 'from' commit separately
6229
- const fromCommitLog = await git.log({ from: from, maxCount: 1 });
6230
- const fromCommit = fromCommitLog.latest;
6231
- // Get the range from..to (excluding 'from')
6232
- const rangeLogOptions = {
6233
- from,
6234
- to,
6235
- '--no-merges': noMerges
6236
- };
6237
- const rangeCommitLog = await git.log(rangeLogOptions);
6238
- // Combine the 'from' commit with the range commits
6239
- const allCommits = fromCommit
6240
- ? [fromCommit, ...rangeCommitLog.all]
6241
- : rangeCommitLog.all;
6242
- return allCommits.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6243
- }
6244
- catch (fallbackError) {
6245
- throw fallbackError;
6246
- }
6304
+ /** @deprecated Use .invoke() instead. Will be removed in 0.2.0. */
6305
+ async run(
6306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6307
+ input, config) {
6308
+ const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k) ?? true);
6309
+ const isKeylessInput = inputKeys.length <= 1;
6310
+ if (!isKeylessInput) {
6311
+ throw new Error(`Chain ${this._chainType()} expects multiple inputs, cannot use 'run' `);
6247
6312
  }
6248
- throw error;
6313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6314
+ const values = inputKeys.length ? { [inputKeys[0]]: input } : {};
6315
+ const returnValues = await this.call(values, config);
6316
+ const keys = Object.keys(returnValues);
6317
+ if (keys.length === 1) {
6318
+ return returnValues[keys[0]];
6319
+ }
6320
+ throw new Error("return values have multiple keys, `run` only supported when one key currently");
6249
6321
  }
6250
- }
6251
-
6252
- /**
6253
- * Retrieves the name of the current branch.
6254
- *
6255
- * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
6256
- * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
6257
- */
6258
- async function getCurrentBranchName({ git }) {
6259
- return await git.revparse(['--abbrev-ref', 'HEAD']);
6260
- }
6261
-
6262
- /**
6263
- * Retrieves the commit log between the current branch and a specified target branch.
6264
- *
6265
- * @param {Object} options - The options for retrieving the commit log.
6266
- * @param {SimpleGit} options.git - The SimpleGit instance.
6267
- * @param {Logger} options.logger - The logger for logging messages.
6268
- * @param {string} options.targetBranch - The target branch to compare against.
6269
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
6270
- */
6271
- async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
6272
- try {
6273
- // Get the current branch name
6274
- const currentBranch = await getCurrentBranchName({ git });
6275
- // Get the list of commits that are unique to the current branch compared to the target branch
6276
- const uniqueCommits = (await git.raw(['rev-list', `${targetBranch}..${currentBranch}`]))
6277
- .split('\n')
6278
- .filter(Boolean)
6279
- .reverse();
6280
- logger?.verbose(`Found ${uniqueCommits.length} unique commits between "${currentBranch}" and "${targetBranch}"`, { color: 'blue' });
6281
- const firstCommit = uniqueCommits[0];
6282
- const lastCommit = uniqueCommits[uniqueCommits.length - 1];
6283
- if (!firstCommit || !lastCommit) {
6284
- logger?.log('Unable to determine first and last commit between branches', { color: 'yellow' });
6285
- return [];
6322
+ async _formatValues(values) {
6323
+ const fullValues = { ...values };
6324
+ if (fullValues.timeout && !fullValues.signal) {
6325
+ fullValues.signal = AbortSignal.timeout(fullValues.timeout);
6326
+ delete fullValues.timeout;
6286
6327
  }
6287
- // Retrieve commit log with messages
6288
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
6328
+ if (!(this.memory == null)) {
6329
+ const newValues = await this.memory.loadMemoryVariables(this._selectMemoryInputs(values));
6330
+ for (const [key, value] of Object.entries(newValues)) {
6331
+ fullValues[key] = value;
6332
+ }
6333
+ }
6334
+ return fullValues;
6289
6335
  }
6290
- catch (error) {
6291
- logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
6336
+ /**
6337
+ * @deprecated Use .invoke() instead. Will be removed in 0.2.0.
6338
+ *
6339
+ * Run the core logic of this chain and add to output if desired.
6340
+ *
6341
+ * Wraps _call and handles memory.
6342
+ */
6343
+ async call(values, config,
6344
+ /** @deprecated */
6345
+ tags) {
6346
+ const parsedConfig = { tags, ...parseCallbackConfigArg(config) };
6347
+ return this.invoke(values, parsedConfig);
6348
+ }
6349
+ /**
6350
+ * @deprecated Use .batch() instead. Will be removed in 0.2.0.
6351
+ *
6352
+ * Call the chain on all inputs in the list
6353
+ */
6354
+ async apply(inputs, config) {
6355
+ return Promise.all(inputs.map(async (i, idx) => this.call(i, config?.[idx])));
6356
+ }
6357
+ /**
6358
+ * Load a chain from a json-like object describing it.
6359
+ */
6360
+ static async deserialize(data, values = {}) {
6361
+ switch (data._type) {
6362
+ case "llm_chain": {
6363
+ const { LLMChain } = await Promise.resolve().then(function () { return llm_chain; });
6364
+ return LLMChain.deserialize(data);
6365
+ }
6366
+ case "sequential_chain": {
6367
+ const { SequentialChain } = await Promise.resolve().then(function () { return sequential_chain; });
6368
+ return SequentialChain.deserialize(data);
6369
+ }
6370
+ case "simple_sequential_chain": {
6371
+ const { SimpleSequentialChain } = await Promise.resolve().then(function () { return sequential_chain; });
6372
+ return SimpleSequentialChain.deserialize(data);
6373
+ }
6374
+ case "stuff_documents_chain": {
6375
+ const { StuffDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
6376
+ return StuffDocumentsChain.deserialize(data);
6377
+ }
6378
+ case "map_reduce_documents_chain": {
6379
+ const { MapReduceDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
6380
+ return MapReduceDocumentsChain.deserialize(data);
6381
+ }
6382
+ case "refine_documents_chain": {
6383
+ const { RefineDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
6384
+ return RefineDocumentsChain.deserialize(data);
6385
+ }
6386
+ case "vector_db_qa": {
6387
+ const { VectorDBQAChain } = await Promise.resolve().then(function () { return vector_db_qa; });
6388
+ return VectorDBQAChain.deserialize(data, values);
6389
+ }
6390
+ case "api_chain": {
6391
+ const { APIChain } = await Promise.resolve().then(function () { return api_chain; });
6392
+ return APIChain.deserialize(data);
6393
+ }
6394
+ default:
6395
+ throw new Error(`Invalid prompt type in config: ${data._type}`);
6396
+ }
6292
6397
  }
6293
- return [];
6294
6398
  }
6295
6399
 
6296
6400
  /**
6297
- * Retrieves the commit log for the current branch.
6298
- *
6299
- * @param {Object} options - The options for retrieving the commit log.
6300
- * @param {SimpleGit} options.git - The SimpleGit instance.
6301
- * @param {Logger} options.logger - The logger for logging messages.
6302
- * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
6303
- * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
6304
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
6401
+ * The NoOpOutputParser class is a type of output parser that does not
6402
+ * perform any operations on the output. It extends the BaseOutputParser
6403
+ * class and is part of the LangChain's output parsers module. This class
6404
+ * is useful in scenarios where the raw output of the Large Language
6405
+ * Models (LLMs) is required.
6305
6406
  */
6306
- async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
6307
- try {
6308
- const branchName = await getCurrentBranchName({ git });
6309
- const hasCommits = (await git.raw(['rev-list', '--count', branchName])) !== '0';
6310
- if (!hasCommits) {
6311
- logger?.log('No commits on the current branch.');
6312
- return [];
6313
- }
6314
- let uniqueCommits;
6315
- if (comparisonBranch === branchName) {
6316
- // If the comparison branch is the same as the current branch, we compare against the remote.
6317
- uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branchName}`]))
6318
- .split('\n')
6319
- .filter(Boolean)
6320
- .reverse();
6321
- }
6322
- else {
6323
- // Your existing code for different branches
6324
- uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branchName}`]))
6325
- .split('\n')
6326
- .filter(Boolean)
6327
- .reverse();
6328
- }
6329
- logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branchName}"`, {
6330
- color: 'blue',
6407
+ class NoOpOutputParser extends BaseOutputParser {
6408
+ constructor() {
6409
+ super(...arguments);
6410
+ Object.defineProperty(this, "lc_namespace", {
6411
+ enumerable: true,
6412
+ configurable: true,
6413
+ writable: true,
6414
+ value: ["langchain", "output_parsers", "default"]
6415
+ });
6416
+ Object.defineProperty(this, "lc_serializable", {
6417
+ enumerable: true,
6418
+ configurable: true,
6419
+ writable: true,
6420
+ value: true
6331
6421
  });
6332
- const firstCommit = uniqueCommits[0];
6333
- const lastCommit = uniqueCommits[uniqueCommits.length - 1];
6334
- if (!firstCommit || !lastCommit) {
6335
- logger?.log('Unable to determine first and last commit on the current branch', {
6336
- color: 'yellow',
6337
- });
6338
- return [];
6339
- }
6340
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
6341
6422
  }
6342
- catch (error) {
6343
- logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
6423
+ static lc_name() {
6424
+ return "NoOpOutputParser";
6425
+ }
6426
+ /**
6427
+ * This method takes a string as input and returns the same string as
6428
+ * output. It does not perform any operations on the input string.
6429
+ * @param text The input string to be parsed.
6430
+ * @returns The same input string without any operations performed on it.
6431
+ */
6432
+ parse(text) {
6433
+ return Promise.resolve(text);
6434
+ }
6435
+ /**
6436
+ * This method returns an empty string. It does not provide any formatting
6437
+ * instructions.
6438
+ * @returns An empty string, indicating no formatting instructions.
6439
+ */
6440
+ getFormatInstructions() {
6441
+ return "";
6344
6442
  }
6345
- return [];
6346
6443
  }
6347
6444
 
6348
- /**
6349
- * Retrieves the SimpleGit instance for the repository.
6350
- * @returns {SimpleGit} The SimpleGit instance.
6351
- */
6352
- const getRepo = () => {
6353
- let git;
6354
- try {
6355
- git = simpleGit();
6445
+ function isBaseLanguageModel(llmLike) {
6446
+ return typeof llmLike._llmType === "function";
6447
+ }
6448
+ function _getLanguageModel(llmLike) {
6449
+ if (isBaseLanguageModel(llmLike)) {
6450
+ return llmLike;
6356
6451
  }
6357
- catch (e) {
6358
- console.log('Error initializing git repo', e);
6359
- process.exit(1);
6452
+ else if ("bound" in llmLike && Runnable.isRunnable(llmLike.bound)) {
6453
+ return _getLanguageModel(llmLike.bound);
6360
6454
  }
6361
- return git;
6362
- };
6363
-
6455
+ else if ("runnable" in llmLike &&
6456
+ "fallbacks" in llmLike &&
6457
+ Runnable.isRunnable(llmLike.runnable)) {
6458
+ return _getLanguageModel(llmLike.runnable);
6459
+ }
6460
+ else if ("default" in llmLike && Runnable.isRunnable(llmLike.default)) {
6461
+ return _getLanguageModel(llmLike.default);
6462
+ }
6463
+ else {
6464
+ throw new Error("Unable to extract BaseLanguageModel from llmLike object.");
6465
+ }
6466
+ }
6364
6467
  /**
6365
- * Template for generating git commit messages based on code changes
6468
+ * @deprecated This class will be removed in 1.0.0. Use the LangChain Expression Language (LCEL) instead.
6469
+ * See the example below for how to use LCEL with the LLMChain class:
6366
6470
  *
6367
- * Variables:
6368
- * - summary: Contains the diff summary of staged changes
6369
- * - format_instructions: Instructions for the output format (JSON with title and body)
6370
- * - additional_context: Optional user-provided context to guide the commit message generation
6371
- * - commit_history: Optional history of previous commits for context
6372
- * - branch_name_context: String containing formatted branch name (or empty if disabled)
6471
+ * Chain to run queries against LLMs.
6472
+ *
6473
+ * @example
6474
+ * ```ts
6475
+ * import { ChatPromptTemplate } from "@langchain/core/prompts";
6476
+ * import { ChatOpenAI } from "@langchain/openai";
6477
+ *
6478
+ * const prompt = ChatPromptTemplate.fromTemplate("Tell me a {adjective} joke");
6479
+ * const llm = new ChatOpenAI();
6480
+ * const chain = prompt.pipe(llm);
6481
+ *
6482
+ * const response = await chain.invoke({ adjective: "funny" });
6483
+ * ```
6373
6484
  */
6374
- const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
6375
- Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
6376
-
6377
- Please follow the guidelines below when writing your commit message:
6378
-
6379
- - Write concisely using an informal tone
6380
- - Avoid phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
6381
- - Avoid referencing specific files names or long paths in the commit message
6382
- - DO NOT include any diffs or file changes in the commit message
6383
- - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
6384
-
6385
- """"""
6386
- {{summary}}
6387
- """"""
6388
-
6389
- {{branch_name_context}}
6390
-
6391
- {{format_instructions}}
6392
-
6393
- {{commit_history}}
6394
-
6395
- {{additional_context}}
6396
- `;
6397
- // Define the variables that will be passed to the prompt template
6398
- const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context'];
6399
- const COMMIT_PROMPT = new PromptTemplate({
6400
- template: template$4,
6401
- inputVariables: inputVariables$3,
6402
- });
6403
- const CONVENTIONAL_TEMPLATE = `Generate a commit message that strictly adheres to the Conventional Commits specification. Follow these rules precisely:
6404
-
6405
- 1. Type Selection:
6406
- - Choose ONE of these types based on the changes:
6407
- * feat: A new feature
6408
- * fix: A bug fix
6409
- * docs: Documentation only changes
6410
- * style: Changes that don't affect the code's meaning (white-space, formatting, etc)
6411
- * refactor: Code changes that neither fix a bug nor add a feature
6412
- * perf: Code changes that improve performance
6413
- * test: Adding missing tests or correcting existing tests
6414
- * build: Changes that affect the build system or external dependencies
6415
- * ci: Changes to CI configuration files and scripts
6416
- * chore: Other changes that don't modify src or test files
6417
- * revert: Reverts a previous commit
6418
-
6419
- 2. Format Requirements:
6420
- - Title format: <type>(<optional-scope>): <description>
6421
- - Title must be 50 characters or less
6422
- - Description should be in imperative mood (e.g., "add" not "adds/added")
6423
- - Body MUST be 280 characters or less
6424
- - Separate body from title with a blank line
6425
- - Body should explain the motivation for the change and contrast it with previous behavior
6426
-
6427
- 3. Scope Guidelines:
6428
- - If the change affects a specific component/area, include it as a scope
6429
- - Scope should be a noun in parentheses (e.g., (parser), (ui), (config))
6430
- - Omit scope if the change is broad or affects multiple areas
6431
-
6432
- Based on the following diff summary, generate a conventional commit message that follows these rules exactly:
6433
-
6434
- """"""
6435
- {{summary}}
6436
- """"""
6437
-
6438
- {{branch_name_context}}
6439
-
6440
- {{format_instructions}}
6441
-
6442
- {{commit_history}}
6443
-
6444
- {{additional_context}}
6445
-
6446
- Remember:
6447
- - Be concise and precise
6448
- - Focus on WHAT and WHY, not HOW
6449
- - Use imperative mood in both title and body
6450
- - Ensure the title alone provides enough context to understand the change`;
6451
- const conventionalInputVariables = [
6452
- 'summary',
6453
- 'additional_context',
6454
- 'commit_history',
6455
- 'format_instructions',
6456
- 'branch_name_context',
6457
- ];
6458
- const CONVENTIONAL_COMMIT_PROMPT = new PromptTemplate({
6459
- template: CONVENTIONAL_TEMPLATE,
6460
- inputVariables: conventionalInputVariables,
6461
- });
6462
-
6463
- /**
6464
- * Verify template string contains all required input variables
6465
- *
6466
- * @param text template string
6467
- * @param inputVariables template variables
6468
- * @throws Error if validation fails
6469
- */
6470
- function validatePromptTemplate(text, inputVariables) {
6471
- if (!text || text.trim() === '') {
6472
- throw new Error('Prompt template cannot be empty');
6473
- }
6474
- if (!inputVariables || inputVariables.length === 0) {
6475
- return; // No variables to validate
6476
- }
6477
- // Extract variables from template using regex to find {variable} patterns
6478
- // This regex matches {variable_name} with no whitespace inside braces
6479
- // Excludes JSON-like patterns with quotes, colons, or whitespace
6480
- const templateVariableRegex = /\{([^}\s:"']+)\}/g;
6481
- const foundVariables = new Set();
6482
- let match;
6483
- while ((match = templateVariableRegex.exec(text)) !== null) {
6484
- foundVariables.add(match[1]);
6485
+ class LLMChain extends BaseChain {
6486
+ static lc_name() {
6487
+ return "LLMChain";
6485
6488
  }
6486
- // Check if all required variables are present in template
6487
- const missingVariables = inputVariables.filter(variable => !foundVariables.has(variable));
6488
- if (missingVariables.length > 0) {
6489
- throw new Error(`Prompt template is missing required variables: ${missingVariables.map(v => `{${v}}`).join(', ')}. ` +
6490
- `Found variables: ${Array.from(foundVariables).map(v => `{${v}}`).join(', ') || 'none'}`);
6489
+ get inputKeys() {
6490
+ return this.prompt.inputVariables;
6491
6491
  }
6492
- // Warn about unused variables in template (optional check)
6493
- const unusedVariables = Array.from(foundVariables).filter(variable => !inputVariables.includes(variable));
6494
- if (unusedVariables.length > 0) {
6495
- console.warn(`Prompt template contains undefined variables: ${unusedVariables.map(v => `{${v}}`).join(', ')}`);
6492
+ get outputKeys() {
6493
+ return [this.outputKey];
6496
6494
  }
6497
- }
6498
-
6499
- async function editPrompt(options) {
6500
- return await editor({
6501
- message: 'Edit the prompt',
6502
- default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
6503
- waitForUseInput: false,
6504
- postfix: 'Press ENTER to continue',
6505
- validate: (text) => {
6506
- try {
6507
- validatePromptTemplate(text, COMMIT_PROMPT.inputVariables);
6508
- return true;
6509
- }
6510
- catch (error) {
6511
- return error instanceof Error ? error.message : 'Invalid prompt template';
6512
- }
6513
- },
6514
- });
6515
- }
6516
-
6517
- async function editResult(result, options) {
6518
- if (options.openInEditor) {
6519
- return await editor({
6520
- message: 'Edit the commit message',
6521
- default: result,
6522
- waitForUseInput: false,
6523
- validate: (text) => (text ? true : 'Commit message cannot be empty'),
6495
+ constructor(fields) {
6496
+ super(fields);
6497
+ Object.defineProperty(this, "lc_serializable", {
6498
+ enumerable: true,
6499
+ configurable: true,
6500
+ writable: true,
6501
+ value: true
6524
6502
  });
6525
- }
6526
- return result;
6527
- }
6528
-
6529
- async function getUserReviewDecision({ label, descriptions, labels, enableEdit = true, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, selectLabel, }) {
6530
- const choices = [
6531
- {
6532
- name: labels?.approve || '✨ Looks good!',
6533
- value: 'approve',
6534
- description: descriptions?.approve || `Continue with the generated ${label}`,
6535
- },
6536
- ];
6537
- if (enableEdit) {
6538
- choices.push({
6539
- name: '📝 Edit',
6540
- value: 'edit',
6541
- description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
6503
+ Object.defineProperty(this, "prompt", {
6504
+ enumerable: true,
6505
+ configurable: true,
6506
+ writable: true,
6507
+ value: void 0
6542
6508
  });
6543
- }
6544
- if (enableModifyPrompt) {
6545
- choices.push({
6546
- name: '🪶 Modify Prompt',
6547
- value: 'modifyPrompt',
6548
- description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
6509
+ Object.defineProperty(this, "llm", {
6510
+ enumerable: true,
6511
+ configurable: true,
6512
+ writable: true,
6513
+ value: void 0
6549
6514
  });
6550
- }
6551
- if (enableRetry) {
6552
- choices.push({
6553
- name: labels?.retryMessageOnly || '🔄 Retry',
6554
- value: 'retryMessageOnly',
6555
- description: descriptions?.retryMessageOnly ||
6556
- `Restart the function execution from generating the ${label}`,
6515
+ Object.defineProperty(this, "llmKwargs", {
6516
+ enumerable: true,
6517
+ configurable: true,
6518
+ writable: true,
6519
+ value: void 0
6557
6520
  });
6558
- }
6559
- if (enableFullRetry) {
6560
- choices.push({
6561
- name: labels?.retryFull || '🔄 Retry Full',
6562
- value: 'retryFull',
6563
- description: descriptions?.retryFull ||
6564
- `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
6521
+ Object.defineProperty(this, "outputKey", {
6522
+ enumerable: true,
6523
+ configurable: true,
6524
+ writable: true,
6525
+ value: "text"
6565
6526
  });
6566
- }
6567
- choices.push({
6568
- name: labels?.cancel || '💣 Cancel',
6569
- value: 'cancel',
6570
- description: descriptions?.cancel || `Cancel the ${label}`,
6571
- });
6572
- return (await select({
6573
- message: selectLabel || `Would you like to make any changes to the ${label}?`,
6574
- choices,
6575
- }));
6576
- }
6577
-
6578
- function logResult(label, result) {
6579
- console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
6580
- }
6581
-
6582
- async function generateAndReviewLoop({ label, factory, parser, noResult, agent, reviewParser, options, }) {
6583
- const { logger } = options;
6584
- let continueLoop = true;
6585
- let modifyPrompt = false;
6586
- let context = '';
6587
- let result = '';
6588
- const changes = await factory();
6589
- // if we don't have any changes, bail.
6590
- if (!changes || !Object.keys(changes).length) {
6591
- await noResult(options);
6592
- }
6593
- while (continueLoop) {
6594
- if (!context.length) {
6595
- context = await parser(changes, result, options);
6596
- }
6597
- // if we still don't have a context, bail.
6598
- if (!context.length) {
6599
- await noResult(options);
6600
- }
6601
- if (modifyPrompt) {
6602
- options.prompt = await editPrompt(options);
6603
- }
6604
- logger.startTimer().startSpinner(`Generating ${label}\n`, {
6605
- color: 'blue',
6527
+ Object.defineProperty(this, "outputParser", {
6528
+ enumerable: true,
6529
+ configurable: true,
6530
+ writable: true,
6531
+ value: void 0
6606
6532
  });
6607
- try {
6608
- result = await agent(context, options);
6609
- if (!result) {
6610
- logger.stopSpinner('💀 Agent failed to return content.', {
6611
- mode: 'fail',
6612
- color: 'red',
6613
- });
6614
- process.exit(0);
6615
- }
6616
- }
6617
- catch (error) {
6618
- // Handle special regeneration request from validation
6619
- if (error.message === 'REGENERATE_COMMIT_MESSAGE') {
6620
- logger.stopSpinner('Regenerating commit message...', {
6621
- mode: 'stop',
6622
- color: 'blue',
6623
- });
6624
- result = '';
6625
- continue;
6626
- }
6627
- // Re-throw other errors
6628
- throw error;
6629
- }
6630
- logger
6631
- .stopSpinner(`Generated ${label}`, {
6632
- color: 'green',
6633
- mode: 'succeed',
6634
- })
6635
- .stopTimer();
6636
- if (options?.interactive) {
6637
- logResult(label, reviewParser ? reviewParser(result, options) : result);
6638
- const reviewAnswer = await getUserReviewDecision({
6639
- label,
6640
- ...(options?.review || {}),
6641
- });
6642
- if (reviewAnswer === 'cancel') {
6643
- process.exit(0);
6644
- }
6645
- if (reviewAnswer === 'edit') {
6646
- options.openInEditor = true;
6647
- }
6648
- if (reviewAnswer === 'retryFull') {
6649
- context = '';
6650
- result = '';
6651
- options.prompt = '';
6652
- continue;
6653
- }
6654
- if (reviewAnswer === 'retryMessageOnly') {
6655
- modifyPrompt = false;
6656
- result = '';
6657
- continue;
6658
- }
6659
- if (reviewAnswer === 'modifyPrompt') {
6660
- modifyPrompt = true;
6661
- result = '';
6662
- continue;
6533
+ this.prompt = fields.prompt;
6534
+ this.llm = fields.llm;
6535
+ this.llmKwargs = fields.llmKwargs;
6536
+ this.outputKey = fields.outputKey ?? this.outputKey;
6537
+ this.outputParser =
6538
+ fields.outputParser ?? new NoOpOutputParser();
6539
+ if (this.prompt.outputParser) {
6540
+ if (fields.outputParser) {
6541
+ throw new Error("Cannot set both outputParser and prompt.outputParser");
6663
6542
  }
6664
- // Only edit the result in interactive mode if approved
6665
- result = await editResult(result, options);
6666
- }
6667
- else {
6668
- // In non-interactive mode, we return the result as is to be output to stdout by the caller.
6669
- const displayResult = reviewParser ? reviewParser(result, options) : result;
6670
- // In non-interactive mode, ensure we return the properly formatted result
6671
- result = displayResult;
6543
+ this.outputParser = this.prompt.outputParser;
6672
6544
  }
6673
- // if we're here, we're done.
6674
- continueLoop = false;
6675
6545
  }
6676
- return result;
6677
- }
6678
-
6679
- const logSuccess = () => {
6680
- console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
6681
- };
6682
-
6683
- async function handleResult({ result, mode, interactiveModeCallback }) {
6684
- switch (mode) {
6685
- case 'interactive':
6686
- if (interactiveModeCallback) {
6687
- await interactiveModeCallback(result);
6688
- }
6689
- else {
6690
- console.warn('No result handler provided for interactive mode.');
6691
- logSuccess();
6692
- }
6693
- break;
6694
- case 'stdout':
6695
- default:
6696
- // Ensure we write the result to stdout in non-interactive mode
6697
- process.stdout.write(result + '\n', 'utf8');
6698
- break;
6699
- }
6700
- process.exit(0);
6701
- }
6702
-
6703
- const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
6704
-
6705
- - Annotate each change with the git commit hash as reference, including just the first 7 characters
6706
- - Logically group changes, and if necessary, summarize dependency updates
6707
- - Include a descriptive title for the changelog, to give a high-level overview of the changes
6708
- - Depending on the size of the changes, consider breaking the changelog into sections
6709
- - Avoid generlizations like "various bug fixes" or "improvements" or "enhancements"
6710
-
6711
- {{format_instructions}}
6712
-
6713
- """{{summary}}"""`;
6714
- const inputVariables$2 = ['format_instructions', 'summary'];
6715
- const CHANGELOG_PROMPT = new PromptTemplate({
6716
- template: template$3,
6717
- inputVariables: inputVariables$2,
6718
- });
6719
-
6720
- const handler$4 = async (argv, logger) => {
6721
- const config = loadConfig(argv);
6722
- const git = getRepo();
6723
- const key = getApiKeyForModel(config);
6724
- const { provider, model } = getModelAndProviderFromConfig(config);
6725
- if (config.service.authentication.type !== 'None' && !key) {
6726
- logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
6727
- process.exit(1);
6546
+ getCallKeys() {
6547
+ const callKeys = "callKeys" in this.llm ? this.llm.callKeys : [];
6548
+ return callKeys;
6728
6549
  }
6729
- const llm = getLlm(provider, model, config);
6730
- const INTERACTIVE = isInteractive(config);
6731
- if (INTERACTIVE) {
6732
- if (!config.hideCocoBanner) {
6733
- logger.log(LOGO);
6550
+ /** @ignore */
6551
+ _selectMemoryInputs(values) {
6552
+ const valuesForMemory = super._selectMemoryInputs(values);
6553
+ const callKeys = this.getCallKeys();
6554
+ for (const key of callKeys) {
6555
+ if (key in values) {
6556
+ delete valuesForMemory[key];
6557
+ }
6734
6558
  }
6559
+ return valuesForMemory;
6735
6560
  }
6736
- async function factory() {
6737
- const branchName = await getCurrentBranchName({ git });
6738
- if (config.sinceLastTag) {
6739
- logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
6740
- return {
6741
- branch: branchName,
6742
- commits: await getChangesSinceLastTag({ git, logger }),
6743
- };
6561
+ /** @ignore */
6562
+ async _getFinalOutput(generations, promptValue, runManager) {
6563
+ let finalCompletion;
6564
+ if (this.outputParser) {
6565
+ finalCompletion = await this.outputParser.parseResultWithPrompt(generations, promptValue, runManager?.getChild());
6744
6566
  }
6745
- if (config.range && config.range.includes(':')) {
6746
- const [from, to] = config.range.split(':');
6747
- if (!from || !to) {
6748
- logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
6749
- process.exit(1);
6567
+ else {
6568
+ finalCompletion = generations[0].text;
6569
+ }
6570
+ return finalCompletion;
6571
+ }
6572
+ /**
6573
+ * Run the core logic of this chain and add to output if desired.
6574
+ *
6575
+ * Wraps _call and handles memory.
6576
+ */
6577
+ call(values, config) {
6578
+ return super.call(values, config);
6579
+ }
6580
+ /** @ignore */
6581
+ async _call(values, runManager) {
6582
+ const valuesForPrompt = { ...values };
6583
+ const valuesForLLM = {
6584
+ ...this.llmKwargs,
6585
+ };
6586
+ const callKeys = this.getCallKeys();
6587
+ for (const key of callKeys) {
6588
+ if (key in values) {
6589
+ if (valuesForLLM) {
6590
+ valuesForLLM[key] =
6591
+ values[key];
6592
+ delete valuesForPrompt[key];
6593
+ }
6750
6594
  }
6751
- return {
6752
- branch: branchName,
6753
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
6754
- };
6755
6595
  }
6756
- if (argv.branch) {
6757
- logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
6596
+ const promptValue = await this.prompt.formatPromptValue(valuesForPrompt);
6597
+ if ("generatePrompt" in this.llm) {
6598
+ const { generations } = await this.llm.generatePrompt([promptValue], valuesForLLM, runManager?.getChild());
6758
6599
  return {
6759
- branch: branchName,
6760
- commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
6600
+ [this.outputKey]: await this._getFinalOutput(generations[0], promptValue, runManager),
6761
6601
  };
6762
6602
  }
6763
- logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
6764
- color: 'yellow',
6765
- });
6766
- const commits = await getCommitLogCurrentBranch({ git, logger });
6603
+ const modelWithParser = this.outputParser
6604
+ ? this.llm.pipe(this.outputParser)
6605
+ : this.llm;
6606
+ const response = await modelWithParser.invoke(promptValue, runManager?.getChild());
6767
6607
  return {
6768
- branch: branchName,
6769
- commits,
6608
+ [this.outputKey]: response,
6770
6609
  };
6771
6610
  }
6772
- async function parser({ branch, commits }) {
6773
- let result;
6774
- if (!commits || commits.length === 0) {
6775
- result = `## ${branch}\n\nNo commits found.`;
6611
+ /**
6612
+ * Format prompt with values and pass to LLM
6613
+ *
6614
+ * @param values - keys to pass to prompt template
6615
+ * @param callbackManager - CallbackManager to use
6616
+ * @returns Completion from LLM.
6617
+ *
6618
+ * @example
6619
+ * ```ts
6620
+ * llm.predict({ adjective: "funny" })
6621
+ * ```
6622
+ */
6623
+ async predict(values, callbackManager) {
6624
+ const output = await this.call(values, callbackManager);
6625
+ return output[this.outputKey];
6626
+ }
6627
+ _chainType() {
6628
+ return "llm";
6629
+ }
6630
+ static async deserialize(data) {
6631
+ const { llm, prompt } = data;
6632
+ if (!llm) {
6633
+ throw new Error("LLMChain must have llm");
6776
6634
  }
6777
- else {
6778
- result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
6635
+ if (!prompt) {
6636
+ throw new Error("LLMChain must have prompt");
6779
6637
  }
6780
- return result;
6638
+ return new LLMChain({
6639
+ llm: await BaseLanguageModel.deserialize(llm),
6640
+ prompt: await BasePromptTemplate.deserialize(prompt),
6641
+ });
6781
6642
  }
6782
- const changelogMsg = await generateAndReviewLoop({
6783
- label: 'changelog',
6784
- options: {
6785
- ...config,
6786
- prompt: config.prompt || CHANGELOG_PROMPT.template,
6787
- logger,
6788
- interactive: INTERACTIVE,
6789
- review: {
6790
- enableFullRetry: false,
6791
- },
6792
- },
6793
- factory,
6794
- parser,
6795
- agent: async (context, options) => {
6796
- const parser = new StructuredOutputParser(ChangelogResponseSchema);
6797
- const prompt = getPrompt({
6798
- template: options.prompt,
6799
- variables: CHANGELOG_PROMPT.inputVariables,
6800
- fallback: CHANGELOG_PROMPT,
6801
- });
6802
- const formatInstructions = "Only respond with a valid JSON object, containing two fields: 'title' an escaped string, no more than 65 characters, and 'content' also an escaped string.";
6803
- const changelog = await executeChain({
6804
- llm,
6805
- prompt,
6806
- variables: {
6807
- summary: context,
6808
- format_instructions: formatInstructions,
6809
- },
6810
- parser,
6811
- });
6812
- const branchName = await getCurrentBranchName({ git });
6813
- const ticketId = extractTicketIdFromBranchName(branchName);
6814
- const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
6815
- return `${changelog.title}\n\n${changelog.content}${footer}`;
6816
- },
6817
- noResult: async () => {
6818
- if (config.range) {
6819
- logger.log(`No commits found in the provided range.`, { color: 'red' });
6820
- process.exit(0);
6821
- }
6822
- logger.log(`No commits found in the current branch.`, { color: 'red' });
6823
- process.exit(0);
6824
- },
6825
- });
6826
- const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
6827
- handleResult({
6828
- result: changelogMsg,
6829
- interactiveModeCallback: async () => {
6830
- logSuccess();
6831
- },
6832
- mode: MODE,
6833
- });
6834
- };
6835
-
6836
- var changelog = {
6837
- command: command$4,
6838
- desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
6839
- builder: builder$4,
6840
- handler: commandExecutor(handler$4),
6841
- options: options$4,
6842
- };
6643
+ /** @deprecated */
6644
+ serialize() {
6645
+ const serialize = "serialize" in this.llm ? this.llm.serialize() : undefined;
6646
+ return {
6647
+ _type: `${this._chainType()}_chain`,
6648
+ llm: serialize,
6649
+ prompt: this.prompt.serialize(),
6650
+ };
6651
+ }
6652
+ _getNumTokens(text) {
6653
+ return _getLanguageModel(this.llm).getNumTokens(text);
6654
+ }
6655
+ }
6843
6656
 
6844
- const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/;
6845
- // Regular commit message schema with basic validation
6846
- const CommitMessageResponseSchema = objectType({
6847
- title: stringType(),
6848
- body: stringType(),
6849
- });
6850
- // Conventional commit message schema with strict formatting rules
6851
- const ConventionalCommitMessageResponseSchema = objectType({
6852
- title: stringType()
6853
- .max(50, "Title must be 50 characters or less")
6854
- .refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')"),
6855
- body: stringType()
6856
- // .max(280, "Body must be 280 characters or less"),
6657
+ var llm_chain = /*#__PURE__*/Object.freeze({
6658
+ __proto__: null,
6659
+ LLMChain: LLMChain
6857
6660
  });
6858
- const command$3 = 'commit';
6859
- /**
6860
- * Command line options via yargs
6861
- */
6862
- const options$3 = {
6863
- i: {
6864
- alias: 'interactive',
6865
- description: 'Toggle interactive mode',
6866
- type: 'boolean',
6867
- },
6868
- ignoredFiles: {
6869
- description: 'Ignored files',
6870
- type: 'array',
6871
- },
6872
- ignoredExtensions: {
6873
- description: 'Ignored extensions',
6874
- type: 'array',
6875
- },
6876
- append: {
6877
- description: 'Add content to the end of the generated commit message',
6878
- type: 'string',
6879
- },
6880
- appendTicket: {
6881
- description: 'Append ticket ID from branch name to the commit message',
6882
- type: 'boolean',
6883
- alias: 't',
6884
- },
6885
- additional: {
6886
- description: 'Add extra contextual information to the prompt',
6887
- type: 'string',
6888
- alias: 'a',
6889
- },
6890
- withPreviousCommits: {
6891
- description: 'Include previous commits as context (specify number of commits, 0 for none)',
6892
- type: 'number',
6893
- default: 0,
6894
- alias: 'p',
6895
- },
6896
- conventional: {
6897
- description: 'Generate commit message in Conventional Commits format',
6898
- type: 'boolean',
6899
- default: false,
6900
- alias: 'c',
6901
- },
6902
- includeBranchName: {
6903
- description: 'Include the current branch name in the commit prompt for context',
6904
- type: 'boolean',
6905
- default: true,
6906
- },
6907
- };
6908
- const builder$3 = (yargs) => {
6909
- return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
6910
- };
6911
6661
 
6912
- new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
6662
+ const NAIVE_FIX_TEMPLATE = `Instructions:
6663
+ --------------
6664
+ {instructions}
6665
+ --------------
6666
+ Completion:
6667
+ --------------
6668
+ {completion}
6669
+ --------------
6670
+
6671
+ Above, the Completion did not satisfy the constraints given in the Instructions.
6672
+ Error:
6673
+ --------------
6674
+ {error}
6675
+ --------------
6676
+
6677
+ Please try again. Please only respond with an answer that satisfies the constraints laid out in the Instructions:`;
6678
+ const NAIVE_FIX_PROMPT =
6679
+ /* #__PURE__ */ PromptTemplate.fromTemplate(NAIVE_FIX_TEMPLATE);
6913
6680
 
6681
+ function isLLMChain(x) {
6682
+ return (x.prompt !== undefined && x.llm !== undefined);
6683
+ }
6914
6684
  /**
6915
- * Base interface that all chains must implement.
6685
+ * Class that extends the BaseOutputParser to handle situations where the
6686
+ * initial parsing attempt fails. It contains a retryChain for retrying
6687
+ * the parsing process in case of a failure.
6916
6688
  */
6917
- class BaseChain extends BaseLangChain {
6918
- get lc_namespace() {
6919
- return ["langchain", "chains", this._chainType()];
6689
+ class OutputFixingParser extends BaseOutputParser {
6690
+ static lc_name() {
6691
+ return "OutputFixingParser";
6920
6692
  }
6921
- constructor(fields,
6922
- /** @deprecated */
6923
- verbose,
6924
- /** @deprecated */
6925
- callbacks) {
6926
- if (arguments.length === 1 &&
6927
- typeof fields === "object" &&
6928
- !("saveContext" in fields)) {
6929
- // fields is not a BaseMemory
6930
- const { memory, callbackManager, ...rest } = fields;
6931
- super({ ...rest, callbacks: callbackManager ?? rest.callbacks });
6932
- this.memory = memory;
6933
- }
6934
- else {
6935
- // fields is a BaseMemory
6936
- super({ verbose, callbacks });
6937
- this.memory = fields;
6938
- }
6693
+ /**
6694
+ * Static method to create a new instance of OutputFixingParser using a
6695
+ * given language model, parser, and optional fields.
6696
+ * @param llm The language model to be used.
6697
+ * @param parser The parser to be used.
6698
+ * @param fields Optional fields which may contain a prompt.
6699
+ * @returns A new instance of OutputFixingParser.
6700
+ */
6701
+ static fromLLM(llm, parser, fields) {
6702
+ const prompt = fields?.prompt ?? NAIVE_FIX_PROMPT;
6703
+ const chain = new LLMChain({ llm, prompt });
6704
+ return new OutputFixingParser({ parser, retryChain: chain });
6939
6705
  }
6940
- /** @ignore */
6941
- _selectMemoryInputs(values) {
6942
- const valuesForMemory = { ...values };
6943
- if ("signal" in valuesForMemory) {
6944
- delete valuesForMemory.signal;
6945
- }
6946
- if ("timeout" in valuesForMemory) {
6947
- delete valuesForMemory.timeout;
6948
- }
6949
- return valuesForMemory;
6706
+ constructor({ parser, retryChain, }) {
6707
+ super(...arguments);
6708
+ Object.defineProperty(this, "lc_namespace", {
6709
+ enumerable: true,
6710
+ configurable: true,
6711
+ writable: true,
6712
+ value: ["langchain", "output_parsers", "fix"]
6713
+ });
6714
+ Object.defineProperty(this, "lc_serializable", {
6715
+ enumerable: true,
6716
+ configurable: true,
6717
+ writable: true,
6718
+ value: true
6719
+ });
6720
+ Object.defineProperty(this, "parser", {
6721
+ enumerable: true,
6722
+ configurable: true,
6723
+ writable: true,
6724
+ value: void 0
6725
+ });
6726
+ Object.defineProperty(this, "retryChain", {
6727
+ enumerable: true,
6728
+ configurable: true,
6729
+ writable: true,
6730
+ value: void 0
6731
+ });
6732
+ this.parser = parser;
6733
+ this.retryChain = retryChain;
6950
6734
  }
6951
6735
  /**
6952
- * Invoke the chain with the provided input and returns the output.
6953
- * @param input Input values for the chain run.
6954
- * @param config Optional configuration for the Runnable.
6955
- * @returns Promise that resolves with the output of the chain run.
6736
+ * Method to parse the completion using the parser. If the initial parsing
6737
+ * fails, it uses the retryChain to attempt to fix the output and retry
6738
+ * the parsing process.
6739
+ * @param completion The completion to be parsed.
6740
+ * @param callbacks Optional callbacks to be used during parsing.
6741
+ * @returns The parsed output.
6956
6742
  */
6957
- async invoke(input, options) {
6958
- const config = ensureConfig(options);
6959
- const fullValues = await this._formatValues(input);
6960
- const callbackManager_ = await CallbackManager.configure(config?.callbacks, this.callbacks, config?.tags, this.tags, config?.metadata, this.metadata, { verbose: this.verbose });
6961
- const runManager = await callbackManager_?.handleChainStart(this.toJSON(), fullValues, undefined, undefined, undefined, undefined, config?.runName);
6962
- let outputValues;
6743
+ async parse(completion, callbacks) {
6963
6744
  try {
6964
- outputValues = await (fullValues.signal
6965
- ? Promise.race([
6966
- this._call(fullValues, runManager, config),
6967
- new Promise((_, reject) => {
6968
- fullValues.signal?.addEventListener("abort", () => {
6969
- reject(new Error("AbortError"));
6970
- });
6971
- }),
6972
- ])
6973
- : this._call(fullValues, runManager, config));
6745
+ return await this.parser.parse(completion, callbacks);
6974
6746
  }
6975
6747
  catch (e) {
6976
- await runManager?.handleChainError(e);
6748
+ // eslint-disable-next-line no-instanceof/no-instanceof
6749
+ if (e instanceof OutputParserException) {
6750
+ const retryInput = {
6751
+ instructions: this.parser.getFormatInstructions(),
6752
+ completion,
6753
+ error: e,
6754
+ };
6755
+ if (isLLMChain(this.retryChain)) {
6756
+ const result = await this.retryChain.call(retryInput, callbacks);
6757
+ const newCompletion = result[this.retryChain.outputKey];
6758
+ return this.parser.parse(newCompletion, callbacks);
6759
+ }
6760
+ else {
6761
+ const result = await this.retryChain.invoke(retryInput, {
6762
+ callbacks,
6763
+ });
6764
+ return result;
6765
+ }
6766
+ }
6977
6767
  throw e;
6978
6768
  }
6979
- if (!(this.memory == null)) {
6980
- await this.memory.saveContext(this._selectMemoryInputs(input), outputValues);
6981
- }
6982
- await runManager?.handleChainEnd(outputValues);
6983
- // add the runManager's currentRunId to the outputValues
6984
- Object.defineProperty(outputValues, RUN_KEY, {
6985
- value: runManager ? { runId: runManager?.runId } : undefined,
6986
- configurable: true,
6987
- });
6988
- return outputValues;
6989
- }
6990
- _validateOutputs(outputs) {
6991
- const missingKeys = this.outputKeys.filter((k) => !(k in outputs));
6992
- if (missingKeys.length) {
6993
- throw new Error(`Missing output keys: ${missingKeys.join(", ")} from chain ${this._chainType()}`);
6994
- }
6995
- }
6996
- async prepOutputs(inputs, outputs, returnOnlyOutputs = false) {
6997
- this._validateOutputs(outputs);
6998
- if (this.memory) {
6999
- await this.memory.saveContext(inputs, outputs);
7000
- }
7001
- if (returnOnlyOutputs) {
7002
- return outputs;
7003
- }
7004
- return { ...inputs, ...outputs };
7005
6769
  }
7006
6770
  /**
7007
- * Return a json-like object representing this chain.
6771
+ * Method to get the format instructions for the parser.
6772
+ * @returns The format instructions for the parser.
7008
6773
  */
7009
- serialize() {
7010
- throw new Error("Method not implemented.");
6774
+ getFormatInstructions() {
6775
+ return this.parser.getFormatInstructions();
7011
6776
  }
7012
- /** @deprecated Use .invoke() instead. Will be removed in 0.2.0. */
7013
- async run(
7014
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7015
- input, config) {
7016
- const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k) ?? true);
7017
- const isKeylessInput = inputKeys.length <= 1;
7018
- if (!isKeylessInput) {
7019
- throw new Error(`Chain ${this._chainType()} expects multiple inputs, cannot use 'run' `);
7020
- }
7021
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7022
- const values = inputKeys.length ? { [inputKeys[0]]: input } : {};
7023
- const returnValues = await this.call(values, config);
7024
- const keys = Object.keys(returnValues);
7025
- if (keys.length === 1) {
7026
- return returnValues[keys[0]];
7027
- }
7028
- throw new Error("return values have multiple keys, `run` only supported when one key currently");
6777
+ }
6778
+
6779
+ /**
6780
+ * Creates a parser with built-in retry logic for schema-based generation
6781
+ * @param schema - Zod schema for the expected output structure
6782
+ * @param llm - LLM instance for retry attempts
6783
+ * @param options - Configuration options for retry behavior
6784
+ * @returns OutputFixingParser configured with retry logic
6785
+ * @throws LangChainExecutionError if parser creation fails
6786
+ */
6787
+ function createSchemaParser(schema, llm, options = {}) {
6788
+ validateRequired(schema, 'schema', 'createSchemaParser');
6789
+ validateRequired(llm, 'llm', 'createSchemaParser');
6790
+ validateRequired(options, 'options', 'createSchemaParser');
6791
+ // Validate schema is actually a Zod schema
6792
+ if (typeof schema.parse !== 'function') {
6793
+ throw new LangChainExecutionError('createSchemaParser: Schema must be a valid Zod schema with a parse method', { schemaType: typeof schema, hasParseMethod: typeof schema.parse });
6794
+ }
6795
+ // Validate options structure
6796
+ if (typeof options !== 'object' || Array.isArray(options)) {
6797
+ throw new LangChainExecutionError('createSchemaParser: Options must be a non-array object', { options, type: typeof options, isArray: Array.isArray(options) });
6798
+ }
6799
+ const { retryTemplate } = options;
6800
+ // Validate retryTemplate if provided
6801
+ if (retryTemplate !== undefined && typeof retryTemplate !== 'string') {
6802
+ throw new LangChainExecutionError('createSchemaParser: retryTemplate must be a string when provided', { retryTemplate, type: typeof retryTemplate });
6803
+ }
6804
+ try {
6805
+ // @ts-expect-error - StructuredOutputParser constructor type issue with Zod schema
6806
+ const baseParser = new StructuredOutputParser(schema);
6807
+ const defaultRetryTemplate = `The following text failed to parse as valid JSON. Please convert it into a valid JSON object that matches the required schema.
6808
+
6809
+ ## Text to fix:
6810
+ {completion}
6811
+
6812
+ ## Instructions:
6813
+ {instructions}
6814
+
6815
+ You must return ONLY valid JSON that matches the schema exactly. Do not include any additional text, explanations, or markdown formatting:`;
6816
+ const retryPromptTemplate = new PromptTemplate({
6817
+ template: retryTemplate || defaultRetryTemplate,
6818
+ inputVariables: ['completion', 'instructions'],
6819
+ });
6820
+ const retryChain = retryPromptTemplate.pipe(llm).pipe(baseParser);
6821
+ return new OutputFixingParser({
6822
+ parser: baseParser,
6823
+ retryChain: retryChain,
6824
+ });
6825
+ }
6826
+ catch (error) {
6827
+ handleLangChainError(error, 'createSchemaParser: Failed to create schema parser', {
6828
+ schemaName: schema.constructor.name,
6829
+ llmType: llm.constructor.name,
6830
+ hasRetryTemplate: !!retryTemplate
6831
+ });
6832
+ }
6833
+ }
6834
+
6835
+ /**
6836
+ * Executes a LangChain pipeline with the provided LLM, prompt, variables, and parser.
6837
+ * @param params - The execution parameters
6838
+ * @returns The parsed result from the LLM chain
6839
+ * @throws LangChainExecutionError if the chain execution fails or returns empty results
6840
+ */
6841
+ const executeChain = async ({ llm, prompt, variables, parser }) => {
6842
+ validateRequired(llm, 'llm', 'executeChain');
6843
+ validateRequired(prompt, 'prompt', 'executeChain');
6844
+ validateRequired(variables, 'variables', 'executeChain');
6845
+ validateRequired(parser, 'parser', 'executeChain');
6846
+ // Validate that variables is an object
6847
+ if (typeof variables !== 'object' || Array.isArray(variables)) {
6848
+ throw new LangChainExecutionError('executeChain: Variables must be a non-array object', { variables, type: typeof variables, isArray: Array.isArray(variables) });
7029
6849
  }
7030
- async _formatValues(values) {
7031
- const fullValues = { ...values };
7032
- if (fullValues.timeout && !fullValues.signal) {
7033
- fullValues.signal = AbortSignal.timeout(fullValues.timeout);
7034
- delete fullValues.timeout;
6850
+ try {
6851
+ const chain = prompt.pipe(llm).pipe(parser);
6852
+ const result = await chain.invoke(variables);
6853
+ if (result === null || result === undefined) {
6854
+ throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
7035
6855
  }
7036
- if (!(this.memory == null)) {
7037
- const newValues = await this.memory.loadMemoryVariables(this._selectMemoryInputs(values));
7038
- for (const [key, value] of Object.entries(newValues)) {
7039
- fullValues[key] = value;
7040
- }
6856
+ return result;
6857
+ }
6858
+ catch (error) {
6859
+ // Re-throw LangChain errors as-is
6860
+ if (error instanceof LangChainExecutionError) {
6861
+ throw error;
7041
6862
  }
7042
- return fullValues;
6863
+ // Wrap other errors with context
6864
+ handleLangChainError(error, 'executeChain: Chain execution failed', {
6865
+ promptInputVariables: prompt.inputVariables,
6866
+ variableKeys: Object.keys(variables),
6867
+ parserType: parser.constructor.name
6868
+ });
7043
6869
  }
7044
- /**
7045
- * @deprecated Use .invoke() instead. Will be removed in 0.2.0.
7046
- *
7047
- * Run the core logic of this chain and add to output if desired.
7048
- *
7049
- * Wraps _call and handles memory.
7050
- */
7051
- async call(values, config,
7052
- /** @deprecated */
7053
- tags) {
7054
- const parsedConfig = { tags, ...parseCallbackConfigArg(config) };
7055
- return this.invoke(values, parsedConfig);
6870
+ };
6871
+
6872
+ function extractTicketIdFromBranchName(branchName) {
6873
+ const regex = /((?<!([A-Z]+)-?)[A-Z]+-\d+)/;
6874
+ const match = branchName.match(regex);
6875
+ return match ? match[0] : null;
6876
+ }
6877
+
6878
+ /**
6879
+ * Formats a commit log into a readable string format.
6880
+ *
6881
+ * @param commitLog - The commit log result containing an array of commit details.
6882
+ * @returns An array of formatted commit log strings.
6883
+ *
6884
+ * Each formatted string includes:
6885
+ * - The date of the commit in square brackets.
6886
+ * - The commit message.
6887
+ * - The commit body.
6888
+ * - The commit hash in parentheses.
6889
+ * - The author's name and email in angle brackets.
6890
+ */
6891
+ const formatCommitLog = (commitLog) => {
6892
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6893
+ };
6894
+
6895
+ const getChangesSinceLastTag = async ({ git }) => {
6896
+ const tags = await git.tags();
6897
+ if (tags.all.length > 0) {
6898
+ const lastTag = tags.latest;
6899
+ const commitLog = await git.log({ from: lastTag });
6900
+ return formatCommitLog(commitLog);
7056
6901
  }
7057
- /**
7058
- * @deprecated Use .batch() instead. Will be removed in 0.2.0.
7059
- *
7060
- * Call the chain on all inputs in the list
7061
- */
7062
- async apply(inputs, config) {
7063
- return Promise.all(inputs.map(async (i, idx) => this.call(i, config?.[idx])));
6902
+ else {
6903
+ return ['No tags found in the repository.'];
7064
6904
  }
7065
- /**
7066
- * Load a chain from a json-like object describing it.
7067
- */
7068
- static async deserialize(data, values = {}) {
7069
- switch (data._type) {
7070
- case "llm_chain": {
7071
- const { LLMChain } = await Promise.resolve().then(function () { return llm_chain; });
7072
- return LLMChain.deserialize(data);
7073
- }
7074
- case "sequential_chain": {
7075
- const { SequentialChain } = await Promise.resolve().then(function () { return sequential_chain; });
7076
- return SequentialChain.deserialize(data);
7077
- }
7078
- case "simple_sequential_chain": {
7079
- const { SimpleSequentialChain } = await Promise.resolve().then(function () { return sequential_chain; });
7080
- return SimpleSequentialChain.deserialize(data);
7081
- }
7082
- case "stuff_documents_chain": {
7083
- const { StuffDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
7084
- return StuffDocumentsChain.deserialize(data);
7085
- }
7086
- case "map_reduce_documents_chain": {
7087
- const { MapReduceDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
7088
- return MapReduceDocumentsChain.deserialize(data);
7089
- }
7090
- case "refine_documents_chain": {
7091
- const { RefineDocumentsChain } = await Promise.resolve().then(function () { return combine_docs_chain; });
7092
- return RefineDocumentsChain.deserialize(data);
7093
- }
7094
- case "vector_db_qa": {
7095
- const { VectorDBQAChain } = await Promise.resolve().then(function () { return vector_db_qa; });
7096
- return VectorDBQAChain.deserialize(data, values);
6905
+ };
6906
+
6907
+ /**
6908
+ * Retrieves the commit log range between two specified commits (inclusive of both commits).
6909
+ *
6910
+ * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6911
+ * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6912
+ * @param options - Additional options for retrieving the commit log range.
6913
+ * @returns A promise that resolves to an array of commit log messages, including both the 'from' and 'to' commits.
6914
+ * @throws If there is an error retrieving the commit log range.
6915
+ */
6916
+ async function getCommitLogRange(from, to, { noMerges, git }) {
6917
+ try {
6918
+ // Use from^..to to include the 'from' commit in the range
6919
+ // This works because from^..to means "commits reachable from 'to' but not from the parent of 'from'"
6920
+ const logOptions = {
6921
+ from: `${from}^`,
6922
+ to,
6923
+ '--no-merges': noMerges
6924
+ };
6925
+ const commitLog = await git.log(logOptions);
6926
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6927
+ }
6928
+ catch (error) {
6929
+ // If from^ fails (e.g., 'from' is the first commit), fall back to using from..to and manually adding the 'from' commit
6930
+ if (error instanceof Error && error.message.includes('unknown revision')) {
6931
+ try {
6932
+ // Get the 'from' commit separately
6933
+ const fromCommitLog = await git.log({ from: from, maxCount: 1 });
6934
+ const fromCommit = fromCommitLog.latest;
6935
+ // Get the range from..to (excluding 'from')
6936
+ const rangeLogOptions = {
6937
+ from,
6938
+ to,
6939
+ '--no-merges': noMerges
6940
+ };
6941
+ const rangeCommitLog = await git.log(rangeLogOptions);
6942
+ // Combine the 'from' commit with the range commits
6943
+ const allCommits = fromCommit
6944
+ ? [fromCommit, ...rangeCommitLog.all]
6945
+ : rangeCommitLog.all;
6946
+ return allCommits.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
7097
6947
  }
7098
- case "api_chain": {
7099
- const { APIChain } = await Promise.resolve().then(function () { return api_chain; });
7100
- return APIChain.deserialize(data);
6948
+ catch (fallbackError) {
6949
+ throw fallbackError;
7101
6950
  }
7102
- default:
7103
- throw new Error(`Invalid prompt type in config: ${data._type}`);
7104
6951
  }
6952
+ throw error;
7105
6953
  }
7106
6954
  }
7107
6955
 
7108
6956
  /**
7109
- * The NoOpOutputParser class is a type of output parser that does not
7110
- * perform any operations on the output. It extends the BaseOutputParser
7111
- * class and is part of the LangChain's output parsers module. This class
7112
- * is useful in scenarios where the raw output of the Large Language
7113
- * Models (LLMs) is required.
6957
+ * Retrieves the name of the current branch.
6958
+ *
6959
+ * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
6960
+ * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
7114
6961
  */
7115
- class NoOpOutputParser extends BaseOutputParser {
7116
- constructor() {
7117
- super(...arguments);
7118
- Object.defineProperty(this, "lc_namespace", {
7119
- enumerable: true,
7120
- configurable: true,
7121
- writable: true,
7122
- value: ["langchain", "output_parsers", "default"]
7123
- });
7124
- Object.defineProperty(this, "lc_serializable", {
7125
- enumerable: true,
7126
- configurable: true,
7127
- writable: true,
7128
- value: true
6962
+ async function getCurrentBranchName({ git }) {
6963
+ return await git.revparse(['--abbrev-ref', 'HEAD']);
6964
+ }
6965
+
6966
+ /**
6967
+ * Retrieves the commit log between the current branch and a specified target branch.
6968
+ *
6969
+ * @param {Object} options - The options for retrieving the commit log.
6970
+ * @param {SimpleGit} options.git - The SimpleGit instance.
6971
+ * @param {Logger} options.logger - The logger for logging messages.
6972
+ * @param {string} options.targetBranch - The target branch to compare against.
6973
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
6974
+ */
6975
+ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
6976
+ try {
6977
+ // Get the current branch name
6978
+ const currentBranch = await getCurrentBranchName({ git });
6979
+ // Get the list of commits that are unique to the current branch compared to the target branch
6980
+ const uniqueCommits = (await git.raw(['rev-list', `${targetBranch}..${currentBranch}`]))
6981
+ .split('\n')
6982
+ .filter(Boolean)
6983
+ .reverse();
6984
+ logger?.verbose(`Found ${uniqueCommits.length} unique commits between "${currentBranch}" and "${targetBranch}"`, { color: 'blue' });
6985
+ const firstCommit = uniqueCommits[0];
6986
+ const lastCommit = uniqueCommits[uniqueCommits.length - 1];
6987
+ if (!firstCommit || !lastCommit) {
6988
+ logger?.log('Unable to determine first and last commit between branches', { color: 'yellow' });
6989
+ return [];
6990
+ }
6991
+ // Retrieve commit log with messages
6992
+ return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
6993
+ }
6994
+ catch (error) {
6995
+ logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
6996
+ }
6997
+ return [];
6998
+ }
6999
+
7000
+ /**
7001
+ * Retrieves the commit log for the current branch.
7002
+ *
7003
+ * @param {Object} options - The options for retrieving the commit log.
7004
+ * @param {SimpleGit} options.git - The SimpleGit instance.
7005
+ * @param {Logger} options.logger - The logger for logging messages.
7006
+ * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
7007
+ * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
7008
+ * @returns {Promise<string[]>} The array of commit messages in the commit log.
7009
+ */
7010
+ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
7011
+ try {
7012
+ const branchName = await getCurrentBranchName({ git });
7013
+ const hasCommits = (await git.raw(['rev-list', '--count', branchName])) !== '0';
7014
+ if (!hasCommits) {
7015
+ logger?.log('No commits on the current branch.');
7016
+ return [];
7017
+ }
7018
+ let uniqueCommits;
7019
+ if (comparisonBranch === branchName) {
7020
+ // If the comparison branch is the same as the current branch, we compare against the remote.
7021
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branchName}`]))
7022
+ .split('\n')
7023
+ .filter(Boolean)
7024
+ .reverse();
7025
+ }
7026
+ else {
7027
+ // Your existing code for different branches
7028
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branchName}`]))
7029
+ .split('\n')
7030
+ .filter(Boolean)
7031
+ .reverse();
7032
+ }
7033
+ logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branchName}"`, {
7034
+ color: 'blue',
7129
7035
  });
7036
+ const firstCommit = uniqueCommits[0];
7037
+ const lastCommit = uniqueCommits[uniqueCommits.length - 1];
7038
+ if (!firstCommit || !lastCommit) {
7039
+ logger?.log('Unable to determine first and last commit on the current branch', {
7040
+ color: 'yellow',
7041
+ });
7042
+ return [];
7043
+ }
7044
+ return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7130
7045
  }
7131
- static lc_name() {
7132
- return "NoOpOutputParser";
7133
- }
7134
- /**
7135
- * This method takes a string as input and returns the same string as
7136
- * output. It does not perform any operations on the input string.
7137
- * @param text The input string to be parsed.
7138
- * @returns The same input string without any operations performed on it.
7139
- */
7140
- parse(text) {
7141
- return Promise.resolve(text);
7142
- }
7143
- /**
7144
- * This method returns an empty string. It does not provide any formatting
7145
- * instructions.
7146
- * @returns An empty string, indicating no formatting instructions.
7147
- */
7148
- getFormatInstructions() {
7149
- return "";
7046
+ catch (error) {
7047
+ logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
7150
7048
  }
7049
+ return [];
7151
7050
  }
7152
7051
 
7153
- function isBaseLanguageModel(llmLike) {
7154
- return typeof llmLike._llmType === "function";
7155
- }
7156
- function _getLanguageModel(llmLike) {
7157
- if (isBaseLanguageModel(llmLike)) {
7158
- return llmLike;
7159
- }
7160
- else if ("bound" in llmLike && Runnable.isRunnable(llmLike.bound)) {
7161
- return _getLanguageModel(llmLike.bound);
7162
- }
7163
- else if ("runnable" in llmLike &&
7164
- "fallbacks" in llmLike &&
7165
- Runnable.isRunnable(llmLike.runnable)) {
7166
- return _getLanguageModel(llmLike.runnable);
7167
- }
7168
- else if ("default" in llmLike && Runnable.isRunnable(llmLike.default)) {
7169
- return _getLanguageModel(llmLike.default);
7052
+ /**
7053
+ * Retrieves the SimpleGit instance for the repository.
7054
+ * @returns {SimpleGit} The SimpleGit instance.
7055
+ */
7056
+ const getRepo = () => {
7057
+ let git;
7058
+ try {
7059
+ git = simpleGit();
7170
7060
  }
7171
- else {
7172
- throw new Error("Unable to extract BaseLanguageModel from llmLike object.");
7061
+ catch (e) {
7062
+ console.log('Error initializing git repo', e);
7063
+ process.exit(1);
7173
7064
  }
7174
- }
7065
+ return git;
7066
+ };
7067
+
7175
7068
  /**
7176
- * @deprecated This class will be removed in 1.0.0. Use the LangChain Expression Language (LCEL) instead.
7177
- * See the example below for how to use LCEL with the LLMChain class:
7178
- *
7179
- * Chain to run queries against LLMs.
7180
- *
7181
- * @example
7182
- * ```ts
7183
- * import { ChatPromptTemplate } from "@langchain/core/prompts";
7184
- * import { ChatOpenAI } from "@langchain/openai";
7069
+ * Template for generating git commit messages based on code changes
7185
7070
  *
7186
- * const prompt = ChatPromptTemplate.fromTemplate("Tell me a {adjective} joke");
7187
- * const llm = new ChatOpenAI();
7188
- * const chain = prompt.pipe(llm);
7071
+ * Variables:
7072
+ * - summary: Contains the diff summary of staged changes
7073
+ * - format_instructions: Instructions for the output format (JSON with title and body)
7074
+ * - additional_context: Optional user-provided context to guide the commit message generation
7075
+ * - commit_history: Optional history of previous commits for context
7076
+ * - branch_name_context: String containing formatted branch name (or empty if disabled)
7077
+ */
7078
+ const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
7079
+ Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
7080
+
7081
+ Please follow the guidelines below when writing your commit message:
7082
+
7083
+ - Write concisely using an informal tone
7084
+ - Avoid phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
7085
+ - Avoid referencing specific files names or long paths in the commit message
7086
+ - DO NOT include any diffs or file changes in the commit message
7087
+ - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
7088
+
7089
+ """"""
7090
+ {{summary}}
7091
+ """"""
7092
+
7093
+ {{branch_name_context}}
7094
+
7095
+ {{format_instructions}}
7096
+
7097
+ {{commit_history}}
7098
+
7099
+ {{additional_context}}
7100
+ `;
7101
+ // Define the variables that will be passed to the prompt template
7102
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context'];
7103
+ const COMMIT_PROMPT = new PromptTemplate({
7104
+ template: template$4,
7105
+ inputVariables: inputVariables$3,
7106
+ });
7107
+ const CONVENTIONAL_TEMPLATE = `Generate a commit message that strictly adheres to the Conventional Commits specification. Follow these rules precisely:
7108
+
7109
+ 1. Type Selection:
7110
+ - Choose ONE of these types based on the changes:
7111
+ * feat: A new feature
7112
+ * fix: A bug fix
7113
+ * docs: Documentation only changes
7114
+ * style: Changes that don't affect the code's meaning (white-space, formatting, etc)
7115
+ * refactor: Code changes that neither fix a bug nor add a feature
7116
+ * perf: Code changes that improve performance
7117
+ * test: Adding missing tests or correcting existing tests
7118
+ * build: Changes that affect the build system or external dependencies
7119
+ * ci: Changes to CI configuration files and scripts
7120
+ * chore: Other changes that don't modify src or test files
7121
+ * revert: Reverts a previous commit
7122
+
7123
+ 2. Format Requirements:
7124
+ - Title format: <type>(<optional-scope>): <description>
7125
+ - Title must be 50 characters or less
7126
+ - Description should be in imperative mood (e.g., "add" not "adds/added")
7127
+ - Body MUST be 280 characters or less
7128
+ - Separate body from title with a blank line
7129
+ - Body should explain the motivation for the change and contrast it with previous behavior
7130
+
7131
+ 3. Scope Guidelines:
7132
+ - If the change affects a specific component/area, include it as a scope
7133
+ - Scope should be a noun in parentheses (e.g., (parser), (ui), (config))
7134
+ - Omit scope if the change is broad or affects multiple areas
7135
+
7136
+ Based on the following diff summary, generate a conventional commit message that follows these rules exactly:
7137
+
7138
+ """"""
7139
+ {{summary}}
7140
+ """"""
7141
+
7142
+ {{branch_name_context}}
7143
+
7144
+ {{format_instructions}}
7145
+
7146
+ {{commit_history}}
7147
+
7148
+ {{additional_context}}
7149
+
7150
+ Remember:
7151
+ - Be concise and precise
7152
+ - Focus on WHAT and WHY, not HOW
7153
+ - Use imperative mood in both title and body
7154
+ - Ensure the title alone provides enough context to understand the change`;
7155
+ const conventionalInputVariables = [
7156
+ 'summary',
7157
+ 'additional_context',
7158
+ 'commit_history',
7159
+ 'format_instructions',
7160
+ 'branch_name_context',
7161
+ ];
7162
+ const CONVENTIONAL_COMMIT_PROMPT = new PromptTemplate({
7163
+ template: CONVENTIONAL_TEMPLATE,
7164
+ inputVariables: conventionalInputVariables,
7165
+ });
7166
+
7167
+ /**
7168
+ * Verify template string contains all required input variables
7189
7169
  *
7190
- * const response = await chain.invoke({ adjective: "funny" });
7191
- * ```
7170
+ * @param text template string
7171
+ * @param inputVariables template variables
7172
+ * @throws Error if validation fails
7192
7173
  */
7193
- class LLMChain extends BaseChain {
7194
- static lc_name() {
7195
- return "LLMChain";
7174
+ function validatePromptTemplate(text, inputVariables) {
7175
+ if (!text || text.trim() === '') {
7176
+ throw new Error('Prompt template cannot be empty');
7196
7177
  }
7197
- get inputKeys() {
7198
- return this.prompt.inputVariables;
7178
+ if (!inputVariables || inputVariables.length === 0) {
7179
+ return; // No variables to validate
7199
7180
  }
7200
- get outputKeys() {
7201
- return [this.outputKey];
7181
+ // Extract variables from template using regex to find {variable} patterns
7182
+ // This regex matches {variable_name} with no whitespace inside braces
7183
+ // Excludes JSON-like patterns with quotes, colons, or whitespace
7184
+ const templateVariableRegex = /\{([^}\s:"']+)\}/g;
7185
+ const foundVariables = new Set();
7186
+ let match;
7187
+ while ((match = templateVariableRegex.exec(text)) !== null) {
7188
+ foundVariables.add(match[1]);
7202
7189
  }
7203
- constructor(fields) {
7204
- super(fields);
7205
- Object.defineProperty(this, "lc_serializable", {
7206
- enumerable: true,
7207
- configurable: true,
7208
- writable: true,
7209
- value: true
7210
- });
7211
- Object.defineProperty(this, "prompt", {
7212
- enumerable: true,
7213
- configurable: true,
7214
- writable: true,
7215
- value: void 0
7190
+ // Check if all required variables are present in template
7191
+ const missingVariables = inputVariables.filter(variable => !foundVariables.has(variable));
7192
+ if (missingVariables.length > 0) {
7193
+ throw new Error(`Prompt template is missing required variables: ${missingVariables.map(v => `{${v}}`).join(', ')}. ` +
7194
+ `Found variables: ${Array.from(foundVariables).map(v => `{${v}}`).join(', ') || 'none'}`);
7195
+ }
7196
+ // Warn about unused variables in template (optional check)
7197
+ const unusedVariables = Array.from(foundVariables).filter(variable => !inputVariables.includes(variable));
7198
+ if (unusedVariables.length > 0) {
7199
+ console.warn(`Prompt template contains undefined variables: ${unusedVariables.map(v => `{${v}}`).join(', ')}`);
7200
+ }
7201
+ }
7202
+
7203
+ async function editPrompt(options) {
7204
+ return await editor({
7205
+ message: 'Edit the prompt',
7206
+ default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
7207
+ waitForUseInput: false,
7208
+ postfix: 'Press ENTER to continue',
7209
+ validate: (text) => {
7210
+ try {
7211
+ validatePromptTemplate(text, COMMIT_PROMPT.inputVariables);
7212
+ return true;
7213
+ }
7214
+ catch (error) {
7215
+ return error instanceof Error ? error.message : 'Invalid prompt template';
7216
+ }
7217
+ },
7218
+ });
7219
+ }
7220
+
7221
+ async function editResult(result, options) {
7222
+ if (options.openInEditor) {
7223
+ return await editor({
7224
+ message: 'Edit the commit message',
7225
+ default: result,
7226
+ waitForUseInput: false,
7227
+ validate: (text) => (text ? true : 'Commit message cannot be empty'),
7216
7228
  });
7217
- Object.defineProperty(this, "llm", {
7218
- enumerable: true,
7219
- configurable: true,
7220
- writable: true,
7221
- value: void 0
7229
+ }
7230
+ return result;
7231
+ }
7232
+
7233
+ async function getUserReviewDecision({ label, descriptions, labels, enableEdit = true, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, selectLabel, }) {
7234
+ const choices = [
7235
+ {
7236
+ name: labels?.approve || '✨ Looks good!',
7237
+ value: 'approve',
7238
+ description: descriptions?.approve || `Continue with the generated ${label}`,
7239
+ },
7240
+ ];
7241
+ if (enableEdit) {
7242
+ choices.push({
7243
+ name: '📝 Edit',
7244
+ value: 'edit',
7245
+ description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
7222
7246
  });
7223
- Object.defineProperty(this, "llmKwargs", {
7224
- enumerable: true,
7225
- configurable: true,
7226
- writable: true,
7227
- value: void 0
7247
+ }
7248
+ if (enableModifyPrompt) {
7249
+ choices.push({
7250
+ name: '🪶 Modify Prompt',
7251
+ value: 'modifyPrompt',
7252
+ description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
7228
7253
  });
7229
- Object.defineProperty(this, "outputKey", {
7230
- enumerable: true,
7231
- configurable: true,
7232
- writable: true,
7233
- value: "text"
7254
+ }
7255
+ if (enableRetry) {
7256
+ choices.push({
7257
+ name: labels?.retryMessageOnly || '🔄 Retry',
7258
+ value: 'retryMessageOnly',
7259
+ description: descriptions?.retryMessageOnly ||
7260
+ `Restart the function execution from generating the ${label}`,
7234
7261
  });
7235
- Object.defineProperty(this, "outputParser", {
7236
- enumerable: true,
7237
- configurable: true,
7238
- writable: true,
7239
- value: void 0
7262
+ }
7263
+ if (enableFullRetry) {
7264
+ choices.push({
7265
+ name: labels?.retryFull || '🔄 Retry Full',
7266
+ value: 'retryFull',
7267
+ description: descriptions?.retryFull ||
7268
+ `Restart the function execution from the beginning, regenerating both the summary and ${label}`,
7240
7269
  });
7241
- this.prompt = fields.prompt;
7242
- this.llm = fields.llm;
7243
- this.llmKwargs = fields.llmKwargs;
7244
- this.outputKey = fields.outputKey ?? this.outputKey;
7245
- this.outputParser =
7246
- fields.outputParser ?? new NoOpOutputParser();
7247
- if (this.prompt.outputParser) {
7248
- if (fields.outputParser) {
7249
- throw new Error("Cannot set both outputParser and prompt.outputParser");
7250
- }
7251
- this.outputParser = this.prompt.outputParser;
7252
- }
7253
7270
  }
7254
- getCallKeys() {
7255
- const callKeys = "callKeys" in this.llm ? this.llm.callKeys : [];
7256
- return callKeys;
7271
+ choices.push({
7272
+ name: labels?.cancel || '💣 Cancel',
7273
+ value: 'cancel',
7274
+ description: descriptions?.cancel || `Cancel the ${label}`,
7275
+ });
7276
+ return (await select({
7277
+ message: selectLabel || `Would you like to make any changes to the ${label}?`,
7278
+ choices,
7279
+ }));
7280
+ }
7281
+
7282
+ function logResult(label, result) {
7283
+ console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
7284
+ }
7285
+
7286
+ async function generateAndReviewLoop({ label, factory, parser, noResult, agent, reviewParser, options, }) {
7287
+ const { logger } = options;
7288
+ let continueLoop = true;
7289
+ let modifyPrompt = false;
7290
+ let context = '';
7291
+ let result = '';
7292
+ const changes = await factory();
7293
+ // if we don't have any changes, bail.
7294
+ if (!changes || !Object.keys(changes).length) {
7295
+ await noResult(options);
7257
7296
  }
7258
- /** @ignore */
7259
- _selectMemoryInputs(values) {
7260
- const valuesForMemory = super._selectMemoryInputs(values);
7261
- const callKeys = this.getCallKeys();
7262
- for (const key of callKeys) {
7263
- if (key in values) {
7264
- delete valuesForMemory[key];
7265
- }
7297
+ while (continueLoop) {
7298
+ if (!context.length) {
7299
+ context = await parser(changes, result, options);
7266
7300
  }
7267
- return valuesForMemory;
7268
- }
7269
- /** @ignore */
7270
- async _getFinalOutput(generations, promptValue, runManager) {
7271
- let finalCompletion;
7272
- if (this.outputParser) {
7273
- finalCompletion = await this.outputParser.parseResultWithPrompt(generations, promptValue, runManager?.getChild());
7301
+ // if we still don't have a context, bail.
7302
+ if (!context.length) {
7303
+ await noResult(options);
7274
7304
  }
7275
- else {
7276
- finalCompletion = generations[0].text;
7305
+ if (modifyPrompt) {
7306
+ options.prompt = await editPrompt(options);
7277
7307
  }
7278
- return finalCompletion;
7279
- }
7280
- /**
7281
- * Run the core logic of this chain and add to output if desired.
7282
- *
7283
- * Wraps _call and handles memory.
7284
- */
7285
- call(values, config) {
7286
- return super.call(values, config);
7287
- }
7288
- /** @ignore */
7289
- async _call(values, runManager) {
7290
- const valuesForPrompt = { ...values };
7291
- const valuesForLLM = {
7292
- ...this.llmKwargs,
7293
- };
7294
- const callKeys = this.getCallKeys();
7295
- for (const key of callKeys) {
7296
- if (key in values) {
7297
- if (valuesForLLM) {
7298
- valuesForLLM[key] =
7299
- values[key];
7300
- delete valuesForPrompt[key];
7301
- }
7308
+ logger.startTimer().startSpinner(`Generating ${label}\n`, {
7309
+ color: 'blue',
7310
+ });
7311
+ try {
7312
+ result = await agent(context, options);
7313
+ if (!result) {
7314
+ logger.stopSpinner('💀 Agent failed to return content.', {
7315
+ mode: 'fail',
7316
+ color: 'red',
7317
+ });
7318
+ process.exit(0);
7302
7319
  }
7303
7320
  }
7304
- const promptValue = await this.prompt.formatPromptValue(valuesForPrompt);
7305
- if ("generatePrompt" in this.llm) {
7306
- const { generations } = await this.llm.generatePrompt([promptValue], valuesForLLM, runManager?.getChild());
7307
- return {
7308
- [this.outputKey]: await this._getFinalOutput(generations[0], promptValue, runManager),
7309
- };
7310
- }
7311
- const modelWithParser = this.outputParser
7312
- ? this.llm.pipe(this.outputParser)
7313
- : this.llm;
7314
- const response = await modelWithParser.invoke(promptValue, runManager?.getChild());
7315
- return {
7316
- [this.outputKey]: response,
7317
- };
7318
- }
7319
- /**
7320
- * Format prompt with values and pass to LLM
7321
- *
7322
- * @param values - keys to pass to prompt template
7323
- * @param callbackManager - CallbackManager to use
7324
- * @returns Completion from LLM.
7325
- *
7326
- * @example
7327
- * ```ts
7328
- * llm.predict({ adjective: "funny" })
7329
- * ```
7330
- */
7331
- async predict(values, callbackManager) {
7332
- const output = await this.call(values, callbackManager);
7333
- return output[this.outputKey];
7334
- }
7335
- _chainType() {
7336
- return "llm";
7337
- }
7338
- static async deserialize(data) {
7339
- const { llm, prompt } = data;
7340
- if (!llm) {
7341
- throw new Error("LLMChain must have llm");
7321
+ catch (error) {
7322
+ // Handle special regeneration request from validation
7323
+ if (error.message === 'REGENERATE_COMMIT_MESSAGE') {
7324
+ logger.stopSpinner('Regenerating commit message...', {
7325
+ mode: 'stop',
7326
+ color: 'blue',
7327
+ });
7328
+ result = '';
7329
+ continue;
7330
+ }
7331
+ // Re-throw other errors
7332
+ throw error;
7342
7333
  }
7343
- if (!prompt) {
7344
- throw new Error("LLMChain must have prompt");
7334
+ logger
7335
+ .stopSpinner(`Generated ${label}`, {
7336
+ color: 'green',
7337
+ mode: 'succeed',
7338
+ })
7339
+ .stopTimer();
7340
+ if (options?.interactive) {
7341
+ logResult(label, reviewParser ? reviewParser(result, options) : result);
7342
+ const reviewAnswer = await getUserReviewDecision({
7343
+ label,
7344
+ ...(options?.review || {}),
7345
+ });
7346
+ if (reviewAnswer === 'cancel') {
7347
+ process.exit(0);
7348
+ }
7349
+ if (reviewAnswer === 'edit') {
7350
+ options.openInEditor = true;
7351
+ }
7352
+ if (reviewAnswer === 'retryFull') {
7353
+ context = '';
7354
+ result = '';
7355
+ options.prompt = '';
7356
+ continue;
7357
+ }
7358
+ if (reviewAnswer === 'retryMessageOnly') {
7359
+ modifyPrompt = false;
7360
+ result = '';
7361
+ continue;
7362
+ }
7363
+ if (reviewAnswer === 'modifyPrompt') {
7364
+ modifyPrompt = true;
7365
+ result = '';
7366
+ continue;
7367
+ }
7368
+ // Only edit the result in interactive mode if approved
7369
+ result = await editResult(result, options);
7345
7370
  }
7346
- return new LLMChain({
7347
- llm: await BaseLanguageModel.deserialize(llm),
7348
- prompt: await BasePromptTemplate.deserialize(prompt),
7349
- });
7371
+ else {
7372
+ // In non-interactive mode, we return the result as is to be output to stdout by the caller.
7373
+ const displayResult = reviewParser ? reviewParser(result, options) : result;
7374
+ // In non-interactive mode, ensure we return the properly formatted result
7375
+ result = displayResult;
7376
+ }
7377
+ // if we're here, we're done.
7378
+ continueLoop = false;
7350
7379
  }
7351
- /** @deprecated */
7352
- serialize() {
7353
- const serialize = "serialize" in this.llm ? this.llm.serialize() : undefined;
7354
- return {
7355
- _type: `${this._chainType()}_chain`,
7356
- llm: serialize,
7357
- prompt: this.prompt.serialize(),
7358
- };
7380
+ return result;
7381
+ }
7382
+
7383
+ const logSuccess = () => {
7384
+ console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
7385
+ };
7386
+
7387
+ async function handleResult({ result, mode, interactiveModeCallback }) {
7388
+ switch (mode) {
7389
+ case 'interactive':
7390
+ if (interactiveModeCallback) {
7391
+ await interactiveModeCallback(result);
7392
+ }
7393
+ else {
7394
+ console.warn('No result handler provided for interactive mode.');
7395
+ logSuccess();
7396
+ }
7397
+ break;
7398
+ case 'stdout':
7399
+ default:
7400
+ // Ensure we write the result to stdout in non-interactive mode
7401
+ process.stdout.write(result + '\n', 'utf8');
7402
+ break;
7359
7403
  }
7360
- _getNumTokens(text) {
7361
- return _getLanguageModel(this.llm).getNumTokens(text);
7404
+ if (process.env.NODE_ENV !== 'test') {
7405
+ process.exit(0);
7362
7406
  }
7363
7407
  }
7364
7408
 
7365
- var llm_chain = /*#__PURE__*/Object.freeze({
7366
- __proto__: null,
7367
- LLMChain: LLMChain
7368
- });
7409
+ const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
7369
7410
 
7370
- const NAIVE_FIX_TEMPLATE = `Instructions:
7371
- --------------
7372
- {instructions}
7373
- --------------
7374
- Completion:
7375
- --------------
7376
- {completion}
7377
- --------------
7411
+ - Annotate each change with the git commit hash as reference, including just the first 7 characters
7412
+ - Logically group changes, and if necessary, summarize dependency updates
7413
+ - Include a descriptive title for the changelog, to give a high-level overview of the changes
7414
+ - Depending on the size of the changes, consider breaking the changelog into sections
7415
+ - Avoid generlizations like "various bug fixes" or "improvements" or "enhancements"
7378
7416
 
7379
- Above, the Completion did not satisfy the constraints given in the Instructions.
7380
- Error:
7381
- --------------
7382
- {error}
7383
- --------------
7417
+ {{format_instructions}}
7384
7418
 
7385
- Please try again. Please only respond with an answer that satisfies the constraints laid out in the Instructions:`;
7386
- const NAIVE_FIX_PROMPT =
7387
- /* #__PURE__ */ PromptTemplate.fromTemplate(NAIVE_FIX_TEMPLATE);
7419
+ """{{summary}}"""`;
7420
+ const inputVariables$2 = ['format_instructions', 'summary'];
7421
+ const CHANGELOG_PROMPT = new PromptTemplate({
7422
+ template: template$3,
7423
+ inputVariables: inputVariables$2,
7424
+ });
7388
7425
 
7389
- function isLLMChain(x) {
7390
- return (x.prompt !== undefined && x.llm !== undefined);
7391
- }
7392
- /**
7393
- * Class that extends the BaseOutputParser to handle situations where the
7394
- * initial parsing attempt fails. It contains a retryChain for retrying
7395
- * the parsing process in case of a failure.
7396
- */
7397
- class OutputFixingParser extends BaseOutputParser {
7398
- static lc_name() {
7399
- return "OutputFixingParser";
7400
- }
7401
- /**
7402
- * Static method to create a new instance of OutputFixingParser using a
7403
- * given language model, parser, and optional fields.
7404
- * @param llm The language model to be used.
7405
- * @param parser The parser to be used.
7406
- * @param fields Optional fields which may contain a prompt.
7407
- * @returns A new instance of OutputFixingParser.
7408
- */
7409
- static fromLLM(llm, parser, fields) {
7410
- const prompt = fields?.prompt ?? NAIVE_FIX_PROMPT;
7411
- const chain = new LLMChain({ llm, prompt });
7412
- return new OutputFixingParser({ parser, retryChain: chain });
7426
+ const handler$4 = async (argv, logger) => {
7427
+ const config = loadConfig(argv);
7428
+ const git = getRepo();
7429
+ const key = getApiKeyForModel(config);
7430
+ const { provider, model } = getModelAndProviderFromConfig(config);
7431
+ if (config.service.authentication.type !== 'None' && !key) {
7432
+ logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
7433
+ process.exit(1);
7413
7434
  }
7414
- constructor({ parser, retryChain, }) {
7415
- super(...arguments);
7416
- Object.defineProperty(this, "lc_namespace", {
7417
- enumerable: true,
7418
- configurable: true,
7419
- writable: true,
7420
- value: ["langchain", "output_parsers", "fix"]
7421
- });
7422
- Object.defineProperty(this, "lc_serializable", {
7423
- enumerable: true,
7424
- configurable: true,
7425
- writable: true,
7426
- value: true
7427
- });
7428
- Object.defineProperty(this, "parser", {
7429
- enumerable: true,
7430
- configurable: true,
7431
- writable: true,
7432
- value: void 0
7433
- });
7434
- Object.defineProperty(this, "retryChain", {
7435
- enumerable: true,
7436
- configurable: true,
7437
- writable: true,
7438
- value: void 0
7439
- });
7440
- this.parser = parser;
7441
- this.retryChain = retryChain;
7435
+ const llm = getLlm(provider, model, config);
7436
+ const INTERACTIVE = isInteractive(config);
7437
+ if (INTERACTIVE) {
7438
+ if (!config.hideCocoBanner) {
7439
+ logger.log(LOGO);
7440
+ }
7442
7441
  }
7443
- /**
7444
- * Method to parse the completion using the parser. If the initial parsing
7445
- * fails, it uses the retryChain to attempt to fix the output and retry
7446
- * the parsing process.
7447
- * @param completion The completion to be parsed.
7448
- * @param callbacks Optional callbacks to be used during parsing.
7449
- * @returns The parsed output.
7450
- */
7451
- async parse(completion, callbacks) {
7452
- try {
7453
- return await this.parser.parse(completion, callbacks);
7442
+ async function factory() {
7443
+ const branchName = await getCurrentBranchName({ git });
7444
+ if (config.sinceLastTag) {
7445
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7446
+ return {
7447
+ branch: branchName,
7448
+ commits: await getChangesSinceLastTag({ git, logger }),
7449
+ };
7454
7450
  }
7455
- catch (e) {
7456
- // eslint-disable-next-line no-instanceof/no-instanceof
7457
- if (e instanceof OutputParserException) {
7458
- const retryInput = {
7459
- instructions: this.parser.getFormatInstructions(),
7460
- completion,
7461
- error: e,
7462
- };
7463
- if (isLLMChain(this.retryChain)) {
7464
- const result = await this.retryChain.call(retryInput, callbacks);
7465
- const newCompletion = result[this.retryChain.outputKey];
7466
- return this.parser.parse(newCompletion, callbacks);
7467
- }
7468
- else {
7469
- const result = await this.retryChain.invoke(retryInput, {
7470
- callbacks,
7471
- });
7472
- return result;
7473
- }
7451
+ if (config.range && config.range.includes(':')) {
7452
+ const [from, to] = config.range.split(':');
7453
+ if (!from || !to) {
7454
+ logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
7455
+ process.exit(1);
7474
7456
  }
7475
- throw e;
7457
+ return {
7458
+ branch: branchName,
7459
+ commits: await getCommitLogRange(from, to, { git, noMerges: true }),
7460
+ };
7461
+ }
7462
+ if (argv.branch) {
7463
+ logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
7464
+ return {
7465
+ branch: branchName,
7466
+ commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
7467
+ };
7476
7468
  }
7469
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7470
+ color: 'yellow',
7471
+ });
7472
+ const commits = await getCommitLogCurrentBranch({ git, logger });
7473
+ return {
7474
+ branch: branchName,
7475
+ commits,
7476
+ };
7477
7477
  }
7478
- /**
7479
- * Method to get the format instructions for the parser.
7480
- * @returns The format instructions for the parser.
7481
- */
7482
- getFormatInstructions() {
7483
- return this.parser.getFormatInstructions();
7478
+ async function parser({ branch, commits }) {
7479
+ let result;
7480
+ if (!commits || commits.length === 0) {
7481
+ result = `## ${branch}\n\nNo commits found.`;
7482
+ }
7483
+ else {
7484
+ result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
7485
+ }
7486
+ return result;
7484
7487
  }
7485
- }
7488
+ const changelogMsg = await generateAndReviewLoop({
7489
+ label: 'changelog',
7490
+ options: {
7491
+ ...config,
7492
+ prompt: config.prompt || CHANGELOG_PROMPT.template,
7493
+ logger,
7494
+ interactive: INTERACTIVE,
7495
+ review: {
7496
+ enableFullRetry: false,
7497
+ },
7498
+ },
7499
+ factory,
7500
+ parser,
7501
+ agent: async (context, options) => {
7502
+ const parser = createSchemaParser(ChangelogResponseSchema, llm);
7503
+ const prompt = getPrompt({
7504
+ template: options.prompt,
7505
+ variables: CHANGELOG_PROMPT.inputVariables,
7506
+ fallback: CHANGELOG_PROMPT,
7507
+ });
7508
+ const formatInstructions = "Only respond with a valid JSON object, containing two fields: 'title' an escaped string, no more than 65 characters, and 'content' also an escaped string.";
7509
+ const changelog = await executeChain({
7510
+ llm,
7511
+ prompt,
7512
+ variables: {
7513
+ summary: context,
7514
+ format_instructions: formatInstructions,
7515
+ },
7516
+ parser,
7517
+ });
7518
+ const branchName = await getCurrentBranchName({ git });
7519
+ const ticketId = extractTicketIdFromBranchName(branchName);
7520
+ const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
7521
+ return `${changelog.title}\n\n${changelog.content}${footer}`;
7522
+ },
7523
+ noResult: async () => {
7524
+ if (config.range) {
7525
+ logger.log(`No commits found in the provided range.`, { color: 'red' });
7526
+ process.exit(0);
7527
+ }
7528
+ logger.log(`No commits found in the current branch.`, { color: 'red' });
7529
+ process.exit(0);
7530
+ },
7531
+ });
7532
+ const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
7533
+ handleResult({
7534
+ result: changelogMsg,
7535
+ interactiveModeCallback: async () => {
7536
+ logSuccess();
7537
+ },
7538
+ mode: MODE,
7539
+ });
7540
+ };
7541
+
7542
+ var changelog = {
7543
+ command: command$4,
7544
+ desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
7545
+ builder: builder$4,
7546
+ handler: commandExecutor(handler$4),
7547
+ options: options$4,
7548
+ };
7486
7549
 
7550
+ const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/;
7551
+ // Regular commit message schema with basic validation
7552
+ const CommitMessageResponseSchema = objectType({
7553
+ title: stringType(),
7554
+ body: stringType(),
7555
+ });
7556
+ // Conventional commit message schema with strict formatting rules
7557
+ const ConventionalCommitMessageResponseSchema = objectType({
7558
+ title: stringType()
7559
+ .max(50, "Title must be 50 characters or less")
7560
+ .refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')"),
7561
+ body: stringType()
7562
+ // .max(280, "Body must be 280 characters or less"),
7563
+ });
7564
+ const command$3 = 'commit';
7487
7565
  /**
7488
- * Creates a parser with built-in retry logic for schema-based generation
7489
- * @param schema - Zod schema for the expected output structure
7490
- * @param llm - LLM instance for retry attempts
7491
- * @param options - Configuration options for retry behavior
7492
- * @returns OutputFixingParser configured with retry logic
7493
- * @throws LangChainExecutionError if parser creation fails
7566
+ * Command line options via yargs
7494
7567
  */
7495
- function createSchemaParser(schema, llm, options = {}) {
7496
- validateRequired(schema, 'schema', 'createSchemaParser');
7497
- validateRequired(llm, 'llm', 'createSchemaParser');
7498
- validateRequired(options, 'options', 'createSchemaParser');
7499
- // Validate schema is actually a Zod schema
7500
- if (typeof schema.parse !== 'function') {
7501
- throw new LangChainExecutionError('createSchemaParser: Schema must be a valid Zod schema with a parse method', { schemaType: typeof schema, hasParseMethod: typeof schema.parse });
7502
- }
7503
- // Validate options structure
7504
- if (typeof options !== 'object' || Array.isArray(options)) {
7505
- throw new LangChainExecutionError('createSchemaParser: Options must be a non-array object', { options, type: typeof options, isArray: Array.isArray(options) });
7506
- }
7507
- const { retryTemplate } = options;
7508
- // Validate retryTemplate if provided
7509
- if (retryTemplate !== undefined && typeof retryTemplate !== 'string') {
7510
- throw new LangChainExecutionError('createSchemaParser: retryTemplate must be a string when provided', { retryTemplate, type: typeof retryTemplate });
7511
- }
7512
- try {
7513
- const baseParser = new StructuredOutputParser(schema);
7514
- const defaultRetryTemplate = `The following text failed to parse as valid JSON. Please convert it into a valid JSON object that matches the required schema.
7515
-
7516
- ## Text to fix:
7517
- {completion}
7518
-
7519
- ## Instructions:
7520
- {instructions}
7521
-
7522
- You must return ONLY valid JSON that matches the schema exactly. Do not include any additional text, explanations, or markdown formatting:`;
7523
- const retryPromptTemplate = new PromptTemplate({
7524
- template: retryTemplate || defaultRetryTemplate,
7525
- inputVariables: ['completion', 'instructions'],
7526
- });
7527
- const retryChain = retryPromptTemplate.pipe(llm).pipe(baseParser);
7528
- return new OutputFixingParser({
7529
- parser: baseParser,
7530
- retryChain: retryChain,
7531
- });
7532
- }
7533
- catch (error) {
7534
- handleLangChainError(error, 'createSchemaParser: Failed to create schema parser', {
7535
- schemaName: schema.constructor.name,
7536
- llmType: llm.constructor.name,
7537
- hasRetryTemplate: !!retryTemplate
7538
- });
7539
- }
7540
- }
7568
+ const options$3 = {
7569
+ i: {
7570
+ alias: 'interactive',
7571
+ description: 'Toggle interactive mode',
7572
+ type: 'boolean',
7573
+ },
7574
+ ignoredFiles: {
7575
+ description: 'Ignored files',
7576
+ type: 'array',
7577
+ },
7578
+ ignoredExtensions: {
7579
+ description: 'Ignored extensions',
7580
+ type: 'array',
7581
+ },
7582
+ append: {
7583
+ description: 'Add content to the end of the generated commit message',
7584
+ type: 'string',
7585
+ },
7586
+ appendTicket: {
7587
+ description: 'Append ticket ID from branch name to the commit message',
7588
+ type: 'boolean',
7589
+ alias: 't',
7590
+ },
7591
+ additional: {
7592
+ description: 'Add extra contextual information to the prompt',
7593
+ type: 'string',
7594
+ alias: 'a',
7595
+ },
7596
+ withPreviousCommits: {
7597
+ description: 'Include previous commits as context (specify number of commits, 0 for none)',
7598
+ type: 'number',
7599
+ default: 0,
7600
+ alias: 'p',
7601
+ },
7602
+ conventional: {
7603
+ description: 'Generate commit message in Conventional Commits format',
7604
+ type: 'boolean',
7605
+ default: false,
7606
+ alias: 'c',
7607
+ },
7608
+ includeBranchName: {
7609
+ description: 'Include the current branch name in the commit prompt for context',
7610
+ type: 'boolean',
7611
+ default: true,
7612
+ },
7613
+ noDiff: {
7614
+ description: 'Only pass basic "git status" result instead of providing entire diff',
7615
+ type: 'boolean',
7616
+ default: false,
7617
+ },
7618
+ };
7619
+ const builder$3 = (yargs) => {
7620
+ return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
7621
+ };
7541
7622
 
7542
7623
  /**
7543
7624
  * High-level function that combines chain execution with schema-based parsing
@@ -7823,7 +7904,38 @@ async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
7823
7904
  throw new Error(`Error reading untracked file: ${error?.message || 'Unknown error'}`);
7824
7905
  }
7825
7906
  }
7826
- return await git.diff([commit, nodeFile.filePath]);
7907
+ // For branch comparisons, handle files that may not exist in the base branch
7908
+ try {
7909
+ return await git.diff([commit, nodeFile.filePath]);
7910
+ }
7911
+ catch (error) {
7912
+ const errorMessage = error instanceof Error ? error.message : String(error);
7913
+ // If the error indicates the file doesn't exist in the base branch, handle it gracefully
7914
+ if (errorMessage.includes('unknown revision or path not in the working tree') ||
7915
+ errorMessage.includes('ambiguous argument')) {
7916
+ // This is likely a newly added file - show the entire file content as an addition
7917
+ if (nodeFile.status === 'added') {
7918
+ try {
7919
+ const fileContent = await promises.readFile(nodeFile.filePath, 'utf-8');
7920
+ return `+++ ${nodeFile.filePath}\n${fileContent.split('\n').map(line => `+${line}`).join('\n')}`;
7921
+ }
7922
+ catch (fsError) {
7923
+ return `Error reading added file ${nodeFile.filePath}: ${fsError instanceof Error ? fsError.message : String(fsError)}`;
7924
+ }
7925
+ }
7926
+ // For other cases, try to get the file content from the current HEAD
7927
+ try {
7928
+ const fileContent = await git.show([`HEAD:${nodeFile.filePath}`]);
7929
+ return `File content from current version:\n${fileContent}`;
7930
+ }
7931
+ catch (showError) {
7932
+ // If all else fails, provide a meaningful error message
7933
+ return `Unable to retrieve diff for ${nodeFile.filePath}. File may be newly added or renamed.`;
7934
+ }
7935
+ }
7936
+ // Re-throw other types of errors
7937
+ throw error;
7938
+ }
7827
7939
  }
7828
7940
  /**
7829
7941
  * Parses the diff for a renamed file.
@@ -10958,14 +11070,24 @@ const handler$3 = async (argv, logger) => {
10958
11070
  });
10959
11071
  }
10960
11072
  async function factory() {
10961
- const changes = await getChanges({
10962
- git,
10963
- options: {
10964
- ignoredFiles: config.ignoredFiles || undefined,
10965
- ignoredExtensions: config.ignoredExtensions || undefined,
10966
- },
10967
- });
10968
- return changes.staged;
11073
+ if (config.noDiff) {
11074
+ const status = await git.status();
11075
+ return status.files.map(file => ({
11076
+ filePath: file.path,
11077
+ status: (file.index === 'A' || file.index === '?' ? 'added' : 'modified'),
11078
+ summary: file.path, // Simplified summary for noDiff
11079
+ }));
11080
+ }
11081
+ else {
11082
+ const changes = await getChanges({
11083
+ git,
11084
+ options: {
11085
+ ignoredFiles: config.ignoredFiles || undefined,
11086
+ ignoredExtensions: config.ignoredExtensions || undefined,
11087
+ },
11088
+ });
11089
+ return changes.staged;
11090
+ }
10969
11091
  }
10970
11092
  async function parser(changes) {
10971
11093
  return await fileChangeParser({
@@ -11668,6 +11790,10 @@ const options$1 = {
11668
11790
  type: 'boolean',
11669
11791
  description: 'Recap for last tag',
11670
11792
  },
11793
+ currentBranch: {
11794
+ type: 'boolean',
11795
+ description: 'Recap for the current branch',
11796
+ },
11671
11797
  i: {
11672
11798
  type: 'boolean',
11673
11799
  alias: 'interactive',
@@ -11683,8 +11809,89 @@ const getChangesByTimestamp = async ({ since, git }) => {
11683
11809
  return formatCommitLog(commitLog);
11684
11810
  };
11685
11811
 
11812
+ /**
11813
+ * Retrieves the diff between the current branch and a specified target branch.
11814
+ *
11815
+ * @param {Object} options - The options for retrieving the diff.
11816
+ * @param {SimpleGit} options.git - The SimpleGit instance.
11817
+ * @param {Logger} options.logger - The logger for logging messages.
11818
+ * @param {string} options.baseBranch - The base branch to compare against.
11819
+ * @param {string} options.headBranch - The head branch to compare.
11820
+ * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
11821
+ * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
11822
+ * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
11823
+ */
11824
+ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
11825
+ try {
11826
+ logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
11827
+ color: 'blue',
11828
+ });
11829
+ // Validate branch names
11830
+ if (!baseBranch || !headBranch) {
11831
+ throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
11832
+ }
11833
+ const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
11834
+ // Prepare ignore patterns
11835
+ const ignorePatterns = [
11836
+ ...ignoredFiles.map((file) => `:!${file}`),
11837
+ ...ignoredExtensions.map((ext) => `:!*${ext}`),
11838
+ ];
11839
+ // Construct the diff command
11840
+ const diffArgs = [`${baseBranch}..${headBranch}`];
11841
+ if (ignorePatterns.length > 0) {
11842
+ diffArgs.push('--');
11843
+ diffArgs.push(...ignorePatterns);
11844
+ }
11845
+ logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
11846
+ color: 'blue',
11847
+ });
11848
+ // Get the diff
11849
+ const diff = await git.diff(diffArgs);
11850
+ logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
11851
+ color: 'blue',
11852
+ });
11853
+ const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
11854
+ const lines = fileDiff.split('\n');
11855
+ const filePathLine = lines[0];
11856
+ const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
11857
+ const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
11858
+ // Determine status based on diff headers
11859
+ let status = 'modified';
11860
+ if (fileDiff.includes('new file mode')) {
11861
+ status = 'added';
11862
+ }
11863
+ else if (fileDiff.includes('deleted file mode')) {
11864
+ status = 'deleted';
11865
+ }
11866
+ else if (fileDiff.includes('rename from')) {
11867
+ status = 'renamed';
11868
+ }
11869
+ return {
11870
+ filePath: filePath || '',
11871
+ oldFilePath: oldFilePath || '',
11872
+ status,
11873
+ summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
11874
+ };
11875
+ });
11876
+ return {
11877
+ staged: changes,
11878
+ unstaged: [],
11879
+ untracked: [],
11880
+ };
11881
+ }
11882
+ catch (error) {
11883
+ const errorMessage = error instanceof Error ? error.message : String(error);
11884
+ console.error('Error in getDiffForBranch:', error);
11885
+ logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
11886
+ logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
11887
+ // Re-throw the error so the caller can handle it appropriately
11888
+ throw error;
11889
+ }
11890
+ }
11891
+
11686
11892
  async function noResult$1({ logger }) {
11687
11893
  logger.log('No repo changes detected. 👀', { color: 'blue' });
11894
+ throw new Error('NO_CHANGES_DETECTED');
11688
11895
  }
11689
11896
 
11690
11897
  const template$1 = `Following the formatting instructions, summarize the following changes in the underlying git repository/branch.
@@ -11732,7 +11939,9 @@ const handler$1 = async (argv, logger) => {
11732
11939
  ? 'yesterday'
11733
11940
  : lastWeek
11734
11941
  ? 'last-week'
11735
- : 'current';
11942
+ : argv.currentBranch || config.currentBranch
11943
+ ? 'currentBranch'
11944
+ : 'current';
11736
11945
  logger.log(`Generating recap for timeframe: ${timeframe}`);
11737
11946
  async function factory() {
11738
11947
  switch (timeframe) {
@@ -11773,6 +11982,25 @@ const handler$1 = async (argv, logger) => {
11773
11982
  case 'last-tag':
11774
11983
  const tags = await getChangesSinceLastTag({ git });
11775
11984
  return tags;
11985
+ case 'currentBranch':
11986
+ const currentBranch = await getCurrentBranchName({ git });
11987
+ const baseBranch = config.defaultBranch || 'main';
11988
+ logger.log(`Recapping changes on branch '${currentBranch}' compared to '${baseBranch}'`);
11989
+ const changes = await getDiffForBranch({
11990
+ git,
11991
+ baseBranch,
11992
+ headBranch: currentBranch,
11993
+ options: {
11994
+ ignoredFiles: config.ignoredFiles || undefined,
11995
+ ignoredExtensions: config.ignoredExtensions || undefined,
11996
+ },
11997
+ });
11998
+ const branchChanges = await fileChangeParser({
11999
+ changes: changes.staged,
12000
+ commit: baseBranch,
12001
+ options: { tokenizer, git, llm, logger },
12002
+ });
12003
+ return [branchChanges];
11776
12004
  default:
11777
12005
  logger.log(`Invalid timeframe: ${timeframe}`, { color: 'red' });
11778
12006
  return [];
@@ -11813,7 +12041,7 @@ const handler$1 = async (argv, logger) => {
11813
12041
  fallback: RECAP_PROMPT,
11814
12042
  });
11815
12043
  try {
11816
- const parser = new StructuredOutputParser(RecapLlmResponseSchema);
12044
+ const parser = createSchemaParser(RecapLlmResponseSchema, llm);
11817
12045
  const response = await executeChain({
11818
12046
  llm,
11819
12047
  prompt,
@@ -11845,11 +12073,13 @@ ${errorMessage}
11845
12073
  },
11846
12074
  noResult: async () => {
11847
12075
  await noResult$1({ git, logger });
11848
- process.exit(0);
12076
+ if (process.env.NODE_ENV !== 'test') {
12077
+ process.exit(0);
12078
+ }
11849
12079
  },
11850
12080
  });
11851
12081
  // Handle the result based on the mode (interactive or stdout)
11852
- const MODE = (INTERACTIVE && 'interactive') || (config.recap && 'interactive') || config?.mode || 'stdout'; // Default to stdout
12082
+ const MODE = (INTERACTIVE && 'interactive') ?? (config.recap && 'interactive') ?? config?.mode ?? 'stdout'; // Default to stdout
11853
12083
  handleResult({
11854
12084
  result: recapResult,
11855
12085
  interactiveModeCallback: async () => {
@@ -11907,46 +12137,6 @@ const builder = (yargs) => {
11907
12137
  return yargs.options(options).usage(getCommandUsageHeader(command));
11908
12138
  };
11909
12139
 
11910
- /**
11911
- * Retrieves the diff between the current branch and a specified target branch.
11912
- *
11913
- * @param {Object} options - The options for retrieving the diff.
11914
- * @param {SimpleGit} options.git - The SimpleGit instance.
11915
- * @param {Logger} options.logger - The logger for logging messages.
11916
- * @param {string} options.targetBranch - The target branch to compare against.
11917
- * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
11918
- * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
11919
- * @returns {Promise<string>} The diff between the current branch and the target branch.
11920
- */
11921
- async function getDiffForBranch({ git, logger, targetBranch, ignoredFiles = [], ignoredExtensions = [], }) {
11922
- try {
11923
- // Get the current branch name
11924
- const currentBranch = await getCurrentBranchName({ git });
11925
- // Prepare ignore patterns
11926
- const ignorePatterns = [
11927
- ...ignoredFiles.map((file) => `:!${file}`),
11928
- ...ignoredExtensions.map((ext) => `:!*${ext}`),
11929
- ];
11930
- // Construct the diff command
11931
- const diffArgs = [`${targetBranch}..${currentBranch}`];
11932
- if (ignorePatterns.length > 0) {
11933
- diffArgs.push('--');
11934
- diffArgs.push(...ignorePatterns);
11935
- }
11936
- // Get the diff
11937
- const diff = await git.diff(diffArgs);
11938
- logger?.verbose(`Generated diff between "${currentBranch}" and "${targetBranch}"`, {
11939
- color: 'blue',
11940
- });
11941
- return diff;
11942
- }
11943
- catch (error) {
11944
- console.error('Error in getDiffForBranch:', error);
11945
- logger?.log('Encountered an error getting diff between branches', { color: 'red' });
11946
- return '';
11947
- }
11948
- }
11949
-
11950
12140
  /******************************************************************************
11951
12141
  Copyright (c) Microsoft Corporation.
11952
12142
 
@@ -14163,14 +14353,23 @@ const handler = async (argv, logger) => {
14163
14353
  async function factory() {
14164
14354
  if (argv.branch) {
14165
14355
  logger.verbose(`Generating diff for branch: ${argv.branch}`, { color: 'yellow' });
14356
+ const currentBranch = await getCurrentBranchName({ git });
14166
14357
  const diff = await getDiffForBranch({
14167
14358
  git,
14168
14359
  logger,
14169
- targetBranch: argv.branch,
14170
- ignoredFiles: config.ignoredFiles || [],
14171
- ignoredExtensions: config.ignoredExtensions || [],
14360
+ baseBranch: argv.branch,
14361
+ headBranch: currentBranch,
14362
+ options: {
14363
+ ignoredFiles: config.ignoredFiles || [],
14364
+ ignoredExtensions: config.ignoredExtensions || [],
14365
+ },
14366
+ });
14367
+ const branchChanges = await fileChangeParser({
14368
+ changes: diff.staged,
14369
+ commit: `--branch-diff-${argv.branch}`,
14370
+ options: { tokenizer, git, llm, logger },
14172
14371
  });
14173
- return [diff];
14372
+ return [branchChanges];
14174
14373
  }
14175
14374
  else {
14176
14375
  const { staged, unstaged, untracked } = await getChanges({
@@ -14234,7 +14433,7 @@ const handler = async (argv, logger) => {
14234
14433
  factory,
14235
14434
  parser,
14236
14435
  agent: async (context, options) => {
14237
- const parser = new StructuredOutputParser(ReviewFeedbackItemArraySchema);
14436
+ const parser = createSchemaParser(ReviewFeedbackItemArraySchema, llm);
14238
14437
  const formatInstructions = "Respond with a valid JSON object, containing four fields:'title' a string, 'summary' a short summary of the problem (include line number if big file), 'severity' a numeric enum up to ten, 'category' an enum string, and 'filePath' a relative filepath to file as string.";
14239
14438
  const prompt = getPrompt({
14240
14439
  template: options.prompt,