git-coco 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.d.ts +64 -8
- package/dist/index.esm.mjs +2433 -985
- package/dist/index.js +2429 -980
- package/package.json +11 -8
package/dist/index.esm.mjs
CHANGED
|
@@ -18,10 +18,11 @@ 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,
|
|
22
|
-
import {
|
|
21
|
+
import { StructuredOutputParser, BaseOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
|
|
22
|
+
import { minimatch } from 'minimatch';
|
|
23
|
+
import { simpleGit, GitError } from 'simple-git';
|
|
23
24
|
import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
|
|
24
|
-
import { createTwoFilesPatch } from 'diff';
|
|
25
|
+
import { createTwoFilesPatch, parsePatch, formatPatch } from 'diff';
|
|
25
26
|
import { ensureConfig, Runnable } from '@langchain/core/runnables';
|
|
26
27
|
import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
27
28
|
import { RUN_KEY } from '@langchain/core/outputs';
|
|
@@ -35,9 +36,8 @@ import '@langchain/core/utils/json_schema';
|
|
|
35
36
|
import '@langchain/core/utils/json_patch';
|
|
36
37
|
import '@langchain/core/utils/env';
|
|
37
38
|
import '@langchain/core/utils/async_caller';
|
|
38
|
-
import { minimatch } from 'minimatch';
|
|
39
39
|
import { encoding_for_model } from 'tiktoken';
|
|
40
|
-
import {
|
|
40
|
+
import { spawn, exec } from 'child_process';
|
|
41
41
|
import * as readline from 'readline';
|
|
42
42
|
import { pathToFileURL } from 'url';
|
|
43
43
|
|
|
@@ -46,7 +46,7 @@ import { pathToFileURL } from 'url';
|
|
|
46
46
|
/**
|
|
47
47
|
* Current build version from package.json
|
|
48
48
|
*/
|
|
49
|
-
const BUILD_VERSION = "0.
|
|
49
|
+
const BUILD_VERSION = "0.33.0";
|
|
50
50
|
|
|
51
51
|
const isInteractive = (config) => {
|
|
52
52
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -446,7 +446,7 @@ function getDefaultServiceConfigFromAlias(provider, model) {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
const DEFAULT_IGNORED_FILES = [
|
|
449
|
+
const DEFAULT_IGNORED_FILES$1 = [
|
|
450
450
|
'package-lock.json',
|
|
451
451
|
'yarn.lock',
|
|
452
452
|
'pnpm-lock.yaml',
|
|
@@ -454,7 +454,7 @@ const DEFAULT_IGNORED_FILES = [
|
|
|
454
454
|
'bun.lock',
|
|
455
455
|
'node_modules',
|
|
456
456
|
];
|
|
457
|
-
const DEFAULT_IGNORED_EXTENSIONS = ['.map', '.lock'];
|
|
457
|
+
const DEFAULT_IGNORED_EXTENSIONS$1 = ['.map', '.lock'];
|
|
458
458
|
const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
|
|
459
459
|
const COCO_CONFIG_END_COMMENT = '# -- end coco config --';
|
|
460
460
|
/**
|
|
@@ -468,8 +468,8 @@ const DEFAULT_CONFIG = {
|
|
|
468
468
|
defaultBranch: 'main',
|
|
469
469
|
service: getDefaultServiceConfigFromAlias('openai'),
|
|
470
470
|
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
471
|
-
ignoredFiles: DEFAULT_IGNORED_FILES,
|
|
472
|
-
ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS,
|
|
471
|
+
ignoredFiles: DEFAULT_IGNORED_FILES$1,
|
|
472
|
+
ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS$1,
|
|
473
473
|
};
|
|
474
474
|
/**
|
|
475
475
|
* Create a named export of all config keys for use in other modules.
|
|
@@ -501,6 +501,8 @@ function loadEnvConfig(config) {
|
|
|
501
501
|
'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT',
|
|
502
502
|
'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES',
|
|
503
503
|
'COCO_SERVICE_FIELDS',
|
|
504
|
+
'COCO_SERVICE_DYNAMIC_MODELS',
|
|
505
|
+
'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE',
|
|
504
506
|
];
|
|
505
507
|
envKeys.forEach((key) => {
|
|
506
508
|
const envVarName = toEnvVarName(key);
|
|
@@ -515,7 +517,9 @@ function loadEnvConfig(config) {
|
|
|
515
517
|
key === 'COCO_SERVICE_ENDPOINT' ||
|
|
516
518
|
key === 'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT' ||
|
|
517
519
|
key === 'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES' ||
|
|
518
|
-
key === 'COCO_SERVICE_FIELDS'
|
|
520
|
+
key === 'COCO_SERVICE_FIELDS' ||
|
|
521
|
+
key === 'COCO_SERVICE_DYNAMIC_MODELS' ||
|
|
522
|
+
key === 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE') {
|
|
519
523
|
// NOTE: We want to ensure that the service object is always defined
|
|
520
524
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
521
525
|
// @ts-ignore
|
|
@@ -570,6 +574,12 @@ function handleServiceEnvVar(service, key, value) {
|
|
|
570
574
|
case 'COCO_SERVICE_FIELDS':
|
|
571
575
|
service.fields = value;
|
|
572
576
|
break;
|
|
577
|
+
case 'COCO_SERVICE_DYNAMIC_MODELS':
|
|
578
|
+
service.dynamicModels = value;
|
|
579
|
+
break;
|
|
580
|
+
case 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE':
|
|
581
|
+
service.dynamicModelPreference = value;
|
|
582
|
+
break;
|
|
573
583
|
}
|
|
574
584
|
}
|
|
575
585
|
function parseEnvValue(key, value) {
|
|
@@ -978,7 +988,7 @@ const schema$1 = {
|
|
|
978
988
|
"$ref": "#/definitions/LLMProvider"
|
|
979
989
|
},
|
|
980
990
|
"model": {
|
|
981
|
-
"$ref": "#/definitions/
|
|
991
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
982
992
|
},
|
|
983
993
|
"baseURL": {
|
|
984
994
|
"type": "string",
|
|
@@ -1670,6 +1680,15 @@ const schema$1 = {
|
|
|
1670
1680
|
"type": "number",
|
|
1671
1681
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
1672
1682
|
"default": 3
|
|
1683
|
+
},
|
|
1684
|
+
"dynamicModels": {
|
|
1685
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
1686
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
1687
|
+
},
|
|
1688
|
+
"dynamicModelPreference": {
|
|
1689
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
1690
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
1691
|
+
"default": "balanced"
|
|
1673
1692
|
}
|
|
1674
1693
|
},
|
|
1675
1694
|
"required": [
|
|
@@ -1686,6 +1705,17 @@ const schema$1 = {
|
|
|
1686
1705
|
"anthropic"
|
|
1687
1706
|
]
|
|
1688
1707
|
},
|
|
1708
|
+
"ConfiguredLLMModel": {
|
|
1709
|
+
"anyOf": [
|
|
1710
|
+
{
|
|
1711
|
+
"$ref": "#/definitions/LLMModel"
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"type": "string",
|
|
1715
|
+
"const": "dynamic"
|
|
1716
|
+
}
|
|
1717
|
+
]
|
|
1718
|
+
},
|
|
1689
1719
|
"LLMModel": {
|
|
1690
1720
|
"anyOf": [
|
|
1691
1721
|
{
|
|
@@ -1983,6 +2013,41 @@ const schema$1 = {
|
|
|
1983
2013
|
null
|
|
1984
2014
|
]
|
|
1985
2015
|
},
|
|
2016
|
+
"DynamicModelProfile": {
|
|
2017
|
+
"type": "object",
|
|
2018
|
+
"properties": {
|
|
2019
|
+
"summarize": {
|
|
2020
|
+
"$ref": "#/definitions/LLMModel"
|
|
2021
|
+
},
|
|
2022
|
+
"commit": {
|
|
2023
|
+
"$ref": "#/definitions/LLMModel"
|
|
2024
|
+
},
|
|
2025
|
+
"changelog": {
|
|
2026
|
+
"$ref": "#/definitions/LLMModel"
|
|
2027
|
+
},
|
|
2028
|
+
"review": {
|
|
2029
|
+
"$ref": "#/definitions/LLMModel"
|
|
2030
|
+
},
|
|
2031
|
+
"recap": {
|
|
2032
|
+
"$ref": "#/definitions/LLMModel"
|
|
2033
|
+
},
|
|
2034
|
+
"repair": {
|
|
2035
|
+
"$ref": "#/definitions/LLMModel"
|
|
2036
|
+
},
|
|
2037
|
+
"largeDiff": {
|
|
2038
|
+
"$ref": "#/definitions/LLMModel"
|
|
2039
|
+
}
|
|
2040
|
+
},
|
|
2041
|
+
"additionalProperties": false
|
|
2042
|
+
},
|
|
2043
|
+
"DynamicModelPreference": {
|
|
2044
|
+
"type": "string",
|
|
2045
|
+
"enum": [
|
|
2046
|
+
"cost",
|
|
2047
|
+
"balanced",
|
|
2048
|
+
"quality"
|
|
2049
|
+
]
|
|
2050
|
+
},
|
|
1986
2051
|
"OllamaLLMService": {
|
|
1987
2052
|
"type": "object",
|
|
1988
2053
|
"additionalProperties": false,
|
|
@@ -1991,7 +2056,7 @@ const schema$1 = {
|
|
|
1991
2056
|
"$ref": "#/definitions/LLMProvider"
|
|
1992
2057
|
},
|
|
1993
2058
|
"model": {
|
|
1994
|
-
"$ref": "#/definitions/
|
|
2059
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
1995
2060
|
},
|
|
1996
2061
|
"endpoint": {
|
|
1997
2062
|
"type": "string"
|
|
@@ -2711,6 +2776,15 @@ const schema$1 = {
|
|
|
2711
2776
|
"type": "number",
|
|
2712
2777
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
2713
2778
|
"default": 3
|
|
2779
|
+
},
|
|
2780
|
+
"dynamicModels": {
|
|
2781
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
2782
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
2783
|
+
},
|
|
2784
|
+
"dynamicModelPreference": {
|
|
2785
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
2786
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
2787
|
+
"default": "balanced"
|
|
2714
2788
|
}
|
|
2715
2789
|
},
|
|
2716
2790
|
"required": [
|
|
@@ -2733,7 +2807,7 @@ const schema$1 = {
|
|
|
2733
2807
|
"$ref": "#/definitions/LLMProvider"
|
|
2734
2808
|
},
|
|
2735
2809
|
"model": {
|
|
2736
|
-
"$ref": "#/definitions/
|
|
2810
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
2737
2811
|
},
|
|
2738
2812
|
"fields": {
|
|
2739
2813
|
"type": "object",
|
|
@@ -2863,6 +2937,15 @@ const schema$1 = {
|
|
|
2863
2937
|
"type": "number",
|
|
2864
2938
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
2865
2939
|
"default": 3
|
|
2940
|
+
},
|
|
2941
|
+
"dynamicModels": {
|
|
2942
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
2943
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
2944
|
+
},
|
|
2945
|
+
"dynamicModelPreference": {
|
|
2946
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
2947
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
2948
|
+
"default": "balanced"
|
|
2866
2949
|
}
|
|
2867
2950
|
},
|
|
2868
2951
|
"required": [
|
|
@@ -6929,11 +7012,11 @@ const ChangelogResponseSchema = objectType({
|
|
|
6929
7012
|
title: stringType(),
|
|
6930
7013
|
content: stringType(),
|
|
6931
7014
|
});
|
|
6932
|
-
const command$
|
|
7015
|
+
const command$5 = 'changelog';
|
|
6933
7016
|
/**
|
|
6934
7017
|
* Command line options via yargs
|
|
6935
7018
|
*/
|
|
6936
|
-
const options$
|
|
7019
|
+
const options$5 = {
|
|
6937
7020
|
range: {
|
|
6938
7021
|
type: 'string',
|
|
6939
7022
|
alias: 'r',
|
|
@@ -6980,8 +7063,8 @@ const options$4 = {
|
|
|
6980
7063
|
description: 'Toggle interactive mode',
|
|
6981
7064
|
},
|
|
6982
7065
|
};
|
|
6983
|
-
const builder$
|
|
6984
|
-
return yargs.options(options$
|
|
7066
|
+
const builder$5 = (yargs) => {
|
|
7067
|
+
return yargs.options(options$5).usage(getCommandUsageHeader(command$5));
|
|
6985
7068
|
};
|
|
6986
7069
|
|
|
6987
7070
|
/**
|
|
@@ -7181,6 +7264,212 @@ function getLlm(provider, model, config) {
|
|
|
7181
7264
|
}
|
|
7182
7265
|
}
|
|
7183
7266
|
|
|
7267
|
+
const OPENAI_DYNAMIC_DEFAULTS = {
|
|
7268
|
+
cost: {
|
|
7269
|
+
summarize: 'gpt-4.1-nano',
|
|
7270
|
+
commit: 'gpt-4.1-mini',
|
|
7271
|
+
changelog: 'gpt-4.1-mini',
|
|
7272
|
+
review: 'gpt-4.1-mini',
|
|
7273
|
+
recap: 'gpt-4.1-nano',
|
|
7274
|
+
repair: 'gpt-4.1-mini',
|
|
7275
|
+
largeDiff: 'gpt-4.1',
|
|
7276
|
+
},
|
|
7277
|
+
balanced: {
|
|
7278
|
+
summarize: 'gpt-4.1-mini',
|
|
7279
|
+
commit: 'gpt-4.1-mini',
|
|
7280
|
+
changelog: 'gpt-4.1',
|
|
7281
|
+
review: 'gpt-4.1',
|
|
7282
|
+
recap: 'gpt-4.1-mini',
|
|
7283
|
+
repair: 'gpt-4.1',
|
|
7284
|
+
largeDiff: 'gpt-4.1',
|
|
7285
|
+
},
|
|
7286
|
+
quality: {
|
|
7287
|
+
summarize: 'gpt-4.1-mini',
|
|
7288
|
+
commit: 'gpt-4.1',
|
|
7289
|
+
changelog: 'gpt-4.1',
|
|
7290
|
+
review: 'gpt-4.1',
|
|
7291
|
+
recap: 'gpt-4.1',
|
|
7292
|
+
repair: 'gpt-4.1',
|
|
7293
|
+
largeDiff: 'gpt-4.1',
|
|
7294
|
+
},
|
|
7295
|
+
};
|
|
7296
|
+
const ANTHROPIC_DYNAMIC_DEFAULTS = {
|
|
7297
|
+
cost: {
|
|
7298
|
+
summarize: 'claude-3-5-haiku-latest',
|
|
7299
|
+
commit: 'claude-3-5-haiku-latest',
|
|
7300
|
+
changelog: 'claude-3-5-sonnet-latest',
|
|
7301
|
+
review: 'claude-3-5-sonnet-latest',
|
|
7302
|
+
recap: 'claude-3-5-haiku-latest',
|
|
7303
|
+
repair: 'claude-3-5-sonnet-latest',
|
|
7304
|
+
largeDiff: 'claude-3-5-sonnet-latest',
|
|
7305
|
+
},
|
|
7306
|
+
balanced: {
|
|
7307
|
+
summarize: 'claude-3-5-haiku-latest',
|
|
7308
|
+
commit: 'claude-3-5-sonnet-latest',
|
|
7309
|
+
changelog: 'claude-3-5-sonnet-latest',
|
|
7310
|
+
review: 'claude-3-7-sonnet-latest',
|
|
7311
|
+
recap: 'claude-3-5-sonnet-latest',
|
|
7312
|
+
repair: 'claude-3-7-sonnet-latest',
|
|
7313
|
+
largeDiff: 'claude-3-7-sonnet-latest',
|
|
7314
|
+
},
|
|
7315
|
+
quality: {
|
|
7316
|
+
summarize: 'claude-3-5-sonnet-latest',
|
|
7317
|
+
commit: 'claude-3-7-sonnet-latest',
|
|
7318
|
+
changelog: 'claude-3-7-sonnet-latest',
|
|
7319
|
+
review: 'claude-sonnet-4-0',
|
|
7320
|
+
recap: 'claude-3-7-sonnet-latest',
|
|
7321
|
+
repair: 'claude-sonnet-4-0',
|
|
7322
|
+
largeDiff: 'claude-sonnet-4-0',
|
|
7323
|
+
},
|
|
7324
|
+
};
|
|
7325
|
+
const OLLAMA_DYNAMIC_DEFAULTS = {
|
|
7326
|
+
cost: {
|
|
7327
|
+
summarize: 'llama3.2:3b',
|
|
7328
|
+
commit: 'llama3.1:8b',
|
|
7329
|
+
changelog: 'llama3.1:8b',
|
|
7330
|
+
review: 'qwen2.5-coder:7b',
|
|
7331
|
+
recap: 'llama3.2:3b',
|
|
7332
|
+
repair: 'qwen2.5-coder:7b',
|
|
7333
|
+
largeDiff: 'qwen2.5-coder:14b',
|
|
7334
|
+
},
|
|
7335
|
+
balanced: {
|
|
7336
|
+
summarize: 'llama3.1:8b',
|
|
7337
|
+
commit: 'qwen2.5-coder:14b',
|
|
7338
|
+
changelog: 'qwen2.5-coder:14b',
|
|
7339
|
+
review: 'qwen2.5-coder:32b',
|
|
7340
|
+
recap: 'llama3.1:8b',
|
|
7341
|
+
repair: 'qwen2.5-coder:32b',
|
|
7342
|
+
largeDiff: 'qwen2.5-coder:32b',
|
|
7343
|
+
},
|
|
7344
|
+
quality: {
|
|
7345
|
+
summarize: 'qwen2.5-coder:14b',
|
|
7346
|
+
commit: 'qwen2.5-coder:32b',
|
|
7347
|
+
changelog: 'qwen2.5-coder:32b',
|
|
7348
|
+
review: 'qwen2.5-coder:32b',
|
|
7349
|
+
recap: 'qwen2.5-coder:14b',
|
|
7350
|
+
repair: 'qwen2.5-coder:32b',
|
|
7351
|
+
largeDiff: 'qwen2.5-coder:32b',
|
|
7352
|
+
},
|
|
7353
|
+
};
|
|
7354
|
+
const DYNAMIC_DEFAULTS = {
|
|
7355
|
+
openai: OPENAI_DYNAMIC_DEFAULTS,
|
|
7356
|
+
anthropic: ANTHROPIC_DYNAMIC_DEFAULTS,
|
|
7357
|
+
ollama: OLLAMA_DYNAMIC_DEFAULTS,
|
|
7358
|
+
};
|
|
7359
|
+
const DYNAMIC_MODEL_TASKS = [
|
|
7360
|
+
'summarize',
|
|
7361
|
+
'commit',
|
|
7362
|
+
'changelog',
|
|
7363
|
+
'review',
|
|
7364
|
+
'recap',
|
|
7365
|
+
'repair',
|
|
7366
|
+
'largeDiff',
|
|
7367
|
+
];
|
|
7368
|
+
function validateDynamicModelProfile(service) {
|
|
7369
|
+
const dynamicModels = service.dynamicModels;
|
|
7370
|
+
if (!dynamicModels)
|
|
7371
|
+
return;
|
|
7372
|
+
const unknownTasks = Object.keys(dynamicModels).filter((task) => !DYNAMIC_MODEL_TASKS.includes(task));
|
|
7373
|
+
if (unknownTasks.length > 0) {
|
|
7374
|
+
throw new LangChainConfigurationError(`Unknown dynamic model task(s): ${unknownTasks.join(', ')}. Supported tasks: ${DYNAMIC_MODEL_TASKS.join(', ')}`, { unknownTasks, supportedTasks: DYNAMIC_MODEL_TASKS });
|
|
7375
|
+
}
|
|
7376
|
+
Object.entries(dynamicModels).forEach(([task, model]) => {
|
|
7377
|
+
if (typeof model !== 'string' || model.trim() === '' || model === 'dynamic') {
|
|
7378
|
+
throw new LangChainConfigurationError(`Dynamic model override for '${task}' must be a concrete model name`, { task, model });
|
|
7379
|
+
}
|
|
7380
|
+
});
|
|
7381
|
+
}
|
|
7382
|
+
function resolveDynamicModel(config, task) {
|
|
7383
|
+
const service = config.service;
|
|
7384
|
+
validateDynamicModelProfile(service);
|
|
7385
|
+
if (service.model !== 'dynamic') {
|
|
7386
|
+
return service.model;
|
|
7387
|
+
}
|
|
7388
|
+
const preference = service.dynamicModelPreference || 'balanced';
|
|
7389
|
+
const providerDefaults = DYNAMIC_DEFAULTS[service.provider];
|
|
7390
|
+
const defaultModel = providerDefaults[preference]?.[task];
|
|
7391
|
+
return service.dynamicModels?.[task] || defaultModel;
|
|
7392
|
+
}
|
|
7393
|
+
function resolveDynamicService(config, task) {
|
|
7394
|
+
const model = resolveDynamicModel(config, task);
|
|
7395
|
+
return {
|
|
7396
|
+
...config.service,
|
|
7397
|
+
model,
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
|
|
7401
|
+
const telemetryByCommand = new Map();
|
|
7402
|
+
function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
7403
|
+
if (!tokenizer)
|
|
7404
|
+
return undefined;
|
|
7405
|
+
try {
|
|
7406
|
+
return tokenizer(renderedPrompt);
|
|
7407
|
+
}
|
|
7408
|
+
catch {
|
|
7409
|
+
return undefined;
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
function logLlmCall(logger, metadata) {
|
|
7413
|
+
if (!logger)
|
|
7414
|
+
return;
|
|
7415
|
+
recordLlmTelemetry(metadata);
|
|
7416
|
+
const fields = [
|
|
7417
|
+
`task=${metadata.task}`,
|
|
7418
|
+
metadata.command ? `command=${metadata.command}` : undefined,
|
|
7419
|
+
metadata.provider ? `provider=${metadata.provider}` : undefined,
|
|
7420
|
+
metadata.model ? `model=${metadata.model}` : undefined,
|
|
7421
|
+
metadata.retryAttempt ? `retryAttempt=${metadata.retryAttempt}` : undefined,
|
|
7422
|
+
metadata.promptTokens !== undefined ? `promptTokens=${metadata.promptTokens}` : undefined,
|
|
7423
|
+
metadata.elapsedMs !== undefined ? `elapsedMs=${metadata.elapsedMs}` : undefined,
|
|
7424
|
+
metadata.inputDocuments !== undefined ? `inputDocuments=${metadata.inputDocuments}` : undefined,
|
|
7425
|
+
metadata.inputChunks !== undefined ? `inputChunks=${metadata.inputChunks}` : undefined,
|
|
7426
|
+
metadata.parserType ? `parser=${metadata.parserType}` : undefined,
|
|
7427
|
+
metadata.variableKeys?.length ? `variableKeys=${metadata.variableKeys.join(',')}` : undefined,
|
|
7428
|
+
].filter(Boolean);
|
|
7429
|
+
logger.verbose(`[llm] ${fields.join(' ')}`, { color: 'cyan' });
|
|
7430
|
+
}
|
|
7431
|
+
function recordLlmTelemetry(metadata) {
|
|
7432
|
+
const command = metadata.command || 'unknown';
|
|
7433
|
+
const current = telemetryByCommand.get(command) || {
|
|
7434
|
+
calls: 0,
|
|
7435
|
+
promptTokens: 0,
|
|
7436
|
+
elapsedMs: 0,
|
|
7437
|
+
inputDocuments: 0,
|
|
7438
|
+
inputChunks: 0,
|
|
7439
|
+
tasks: new Set(),
|
|
7440
|
+
models: new Set(),
|
|
7441
|
+
};
|
|
7442
|
+
current.calls += 1;
|
|
7443
|
+
current.promptTokens += metadata.promptTokens || 0;
|
|
7444
|
+
current.elapsedMs += metadata.elapsedMs || 0;
|
|
7445
|
+
current.inputDocuments += metadata.inputDocuments || 0;
|
|
7446
|
+
current.inputChunks += metadata.inputChunks || 0;
|
|
7447
|
+
current.tasks.add(metadata.task);
|
|
7448
|
+
if (metadata.model) {
|
|
7449
|
+
current.models.add(metadata.model);
|
|
7450
|
+
}
|
|
7451
|
+
telemetryByCommand.set(command, current);
|
|
7452
|
+
}
|
|
7453
|
+
function logLlmTelemetrySummary(logger, command) {
|
|
7454
|
+
if (!logger)
|
|
7455
|
+
return;
|
|
7456
|
+
const summary = telemetryByCommand.get(command);
|
|
7457
|
+
if (!summary || summary.calls === 0)
|
|
7458
|
+
return;
|
|
7459
|
+
const fields = [
|
|
7460
|
+
`command=${command}`,
|
|
7461
|
+
`calls=${summary.calls}`,
|
|
7462
|
+
summary.promptTokens > 0 ? `promptTokens=${summary.promptTokens}` : undefined,
|
|
7463
|
+
summary.elapsedMs > 0 ? `elapsedMs=${summary.elapsedMs}` : undefined,
|
|
7464
|
+
summary.inputDocuments > 0 ? `inputDocuments=${summary.inputDocuments}` : undefined,
|
|
7465
|
+
summary.inputChunks > 0 ? `inputChunks=${summary.inputChunks}` : undefined,
|
|
7466
|
+
summary.tasks.size > 0 ? `tasks=${[...summary.tasks].join(',')}` : undefined,
|
|
7467
|
+
summary.models.size > 0 ? `models=${[...summary.models].join(',')}` : undefined,
|
|
7468
|
+
].filter(Boolean);
|
|
7469
|
+
logger.verbose(`[llm:summary] ${fields.join(' ')}`, { color: 'cyan' });
|
|
7470
|
+
telemetryByCommand.delete(command);
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7184
7473
|
/**
|
|
7185
7474
|
* Creates a PromptTemplate from a template string or returns a fallback template.
|
|
7186
7475
|
*
|
|
@@ -7269,6 +7558,75 @@ function createSchemaParser(schema, llm, options = {}
|
|
|
7269
7558
|
}
|
|
7270
7559
|
}
|
|
7271
7560
|
|
|
7561
|
+
async function renderPrompt(prompt, variables) {
|
|
7562
|
+
if (typeof prompt.format === 'function') {
|
|
7563
|
+
return await prompt.format(variables);
|
|
7564
|
+
}
|
|
7565
|
+
if (typeof prompt.template === 'string') {
|
|
7566
|
+
return Object.entries(variables).reduce((result, [key, value]) => {
|
|
7567
|
+
return result
|
|
7568
|
+
.replaceAll(`{{${key}}}`, value)
|
|
7569
|
+
.replaceAll(`{${key}}`, value);
|
|
7570
|
+
}, prompt.template);
|
|
7571
|
+
}
|
|
7572
|
+
throw new Error('Prompt must provide either a format function or template string');
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* Ensure the fully rendered LLM prompt fits the configured request budget.
|
|
7576
|
+
*
|
|
7577
|
+
* Diff condensation budgets only cover the diff summary itself. This guard accounts
|
|
7578
|
+
* for the rest of the rendered prompt, then trims the summary as a deterministic
|
|
7579
|
+
* fallback when additional context pushes the request over budget.
|
|
7580
|
+
*/
|
|
7581
|
+
async function enforcePromptBudget({ prompt, variables, tokenizer, maxTokens, summaryKey = 'summary', responseTokenReserve = 512, }) {
|
|
7582
|
+
const renderedPrompt = await renderPrompt(prompt, variables);
|
|
7583
|
+
const promptTokenCount = tokenizer(renderedPrompt);
|
|
7584
|
+
if (promptTokenCount <= maxTokens) {
|
|
7585
|
+
return { variables, promptTokenCount, truncated: false };
|
|
7586
|
+
}
|
|
7587
|
+
const summary = variables[summaryKey] || '';
|
|
7588
|
+
const variablesWithoutSummary = { ...variables, [summaryKey]: '' };
|
|
7589
|
+
const overheadTokenCount = tokenizer(await renderPrompt(prompt, variablesWithoutSummary));
|
|
7590
|
+
const summaryBudget = Math.max(0, maxTokens - overheadTokenCount - responseTokenReserve);
|
|
7591
|
+
if (summaryBudget === 0) {
|
|
7592
|
+
const emptySummaryVariables = { ...variables, [summaryKey]: '' };
|
|
7593
|
+
const emptySummaryTokenCount = tokenizer(await renderPrompt(prompt, emptySummaryVariables));
|
|
7594
|
+
if (emptySummaryTokenCount > maxTokens) {
|
|
7595
|
+
throw new Error(`Rendered prompt exceeds token budget before adding ${summaryKey}: ` +
|
|
7596
|
+
`${emptySummaryTokenCount} > ${maxTokens}`);
|
|
7597
|
+
}
|
|
7598
|
+
return {
|
|
7599
|
+
variables: emptySummaryVariables,
|
|
7600
|
+
promptTokenCount: emptySummaryTokenCount,
|
|
7601
|
+
truncated: true,
|
|
7602
|
+
};
|
|
7603
|
+
}
|
|
7604
|
+
let low = 0;
|
|
7605
|
+
let high = summary.length;
|
|
7606
|
+
let bestSummary = '';
|
|
7607
|
+
let bestTokenCount = overheadTokenCount;
|
|
7608
|
+
while (low <= high) {
|
|
7609
|
+
const mid = Math.floor((low + high) / 2);
|
|
7610
|
+
const candidateSummary = summary.slice(0, mid);
|
|
7611
|
+
const candidateVariables = { ...variables, [summaryKey]: candidateSummary };
|
|
7612
|
+
const candidateTokenCount = tokenizer(await renderPrompt(prompt, candidateVariables));
|
|
7613
|
+
if (candidateTokenCount <= maxTokens - responseTokenReserve) {
|
|
7614
|
+
bestSummary = candidateSummary;
|
|
7615
|
+
bestTokenCount = candidateTokenCount;
|
|
7616
|
+
low = mid + 1;
|
|
7617
|
+
}
|
|
7618
|
+
else {
|
|
7619
|
+
high = mid - 1;
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
const trimmedVariables = { ...variables, [summaryKey]: bestSummary.trimEnd() };
|
|
7623
|
+
return {
|
|
7624
|
+
variables: trimmedVariables,
|
|
7625
|
+
promptTokenCount: bestTokenCount,
|
|
7626
|
+
truncated: true,
|
|
7627
|
+
};
|
|
7628
|
+
}
|
|
7629
|
+
|
|
7272
7630
|
/**
|
|
7273
7631
|
* Extracts provider and endpoint info from LLM instance if available
|
|
7274
7632
|
*/
|
|
@@ -7301,7 +7659,7 @@ function extractLlmInfo(llm) {
|
|
|
7301
7659
|
* @throws LangChainExecutionError if the chain execution fails or returns empty results
|
|
7302
7660
|
* @throws LangChainNetworkError if a network/connection error occurs
|
|
7303
7661
|
*/
|
|
7304
|
-
const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, }) => {
|
|
7662
|
+
const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, logger, tokenizer, metadata, }) => {
|
|
7305
7663
|
validateRequired(llm, 'llm', 'executeChain');
|
|
7306
7664
|
validateRequired(prompt, 'prompt', 'executeChain');
|
|
7307
7665
|
validateRequired(variables, 'variables', 'executeChain');
|
|
@@ -7319,8 +7677,21 @@ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint
|
|
|
7319
7677
|
const effectiveProvider = provider || llmInfo.provider;
|
|
7320
7678
|
const effectiveEndpoint = endpoint || llmInfo.endpoint;
|
|
7321
7679
|
try {
|
|
7680
|
+
const renderedPrompt = await prompt.format(variables);
|
|
7681
|
+
const promptTokens = estimatePromptTokens(tokenizer, renderedPrompt);
|
|
7322
7682
|
const chain = prompt.pipe(llm).pipe(parser);
|
|
7683
|
+
const startedAt = Date.now();
|
|
7323
7684
|
const result = (await chain.invoke(variables));
|
|
7685
|
+
const elapsedMs = Date.now() - startedAt;
|
|
7686
|
+
logLlmCall(logger, {
|
|
7687
|
+
task: metadata?.task || 'chain',
|
|
7688
|
+
provider: effectiveProvider,
|
|
7689
|
+
parserType: parser.constructor.name,
|
|
7690
|
+
variableKeys: Object.keys(variables),
|
|
7691
|
+
promptTokens,
|
|
7692
|
+
elapsedMs,
|
|
7693
|
+
...metadata,
|
|
7694
|
+
});
|
|
7324
7695
|
if (result === null || result === undefined) {
|
|
7325
7696
|
throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
|
|
7326
7697
|
}
|
|
@@ -7558,6 +7929,148 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
|
|
|
7558
7929
|
return [];
|
|
7559
7930
|
}
|
|
7560
7931
|
|
|
7932
|
+
/**
|
|
7933
|
+
* Determines the status of a file based on its changes in the Git repository.
|
|
7934
|
+
*
|
|
7935
|
+
* @param file - The file to check the status of.
|
|
7936
|
+
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
7937
|
+
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
7938
|
+
* @throws Error if the file type is invalid.
|
|
7939
|
+
*/
|
|
7940
|
+
function getStatus(file, location = 'index') {
|
|
7941
|
+
if ('index' in file && 'working_dir' in file) {
|
|
7942
|
+
const statusCode = file[location];
|
|
7943
|
+
switch (statusCode) {
|
|
7944
|
+
case 'A':
|
|
7945
|
+
return 'added';
|
|
7946
|
+
case 'D':
|
|
7947
|
+
return 'deleted';
|
|
7948
|
+
case 'M':
|
|
7949
|
+
return 'modified';
|
|
7950
|
+
case 'R':
|
|
7951
|
+
return 'renamed';
|
|
7952
|
+
case '?':
|
|
7953
|
+
return 'untracked';
|
|
7954
|
+
default:
|
|
7955
|
+
return 'unknown';
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
else if ('binary' in file && file.binary === true) {
|
|
7959
|
+
// DiffResultBinaryFile: has before/after, no changes/insertions/deletions
|
|
7960
|
+
if (file.file.includes('=>'))
|
|
7961
|
+
return 'renamed';
|
|
7962
|
+
if (file.before === 0 && file.after > 0)
|
|
7963
|
+
return 'added';
|
|
7964
|
+
if (file.after === 0 && file.before > 0)
|
|
7965
|
+
return 'deleted';
|
|
7966
|
+
if (file.before > 0 && file.after > 0)
|
|
7967
|
+
return 'modified';
|
|
7968
|
+
return 'untracked';
|
|
7969
|
+
}
|
|
7970
|
+
else if ('changes' in file && 'binary' in file) {
|
|
7971
|
+
// DiffResultTextFile: has changes/insertions/deletions
|
|
7972
|
+
if (file.changes === 0)
|
|
7973
|
+
return 'untracked';
|
|
7974
|
+
if (file.file.includes('=>'))
|
|
7975
|
+
return 'renamed';
|
|
7976
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
7977
|
+
return 'added';
|
|
7978
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
7979
|
+
return 'deleted';
|
|
7980
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
7981
|
+
return 'modified';
|
|
7982
|
+
return 'unknown';
|
|
7983
|
+
}
|
|
7984
|
+
else {
|
|
7985
|
+
throw new Error('Invalid file type');
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
|
|
7989
|
+
/**
|
|
7990
|
+
* Returns the summary text for a file change.
|
|
7991
|
+
*
|
|
7992
|
+
* @param file - The file status or diff result.
|
|
7993
|
+
* @param change - The partial file change object.
|
|
7994
|
+
* @returns The summary text for the file change.
|
|
7995
|
+
* @throws Error if the file type is invalid.
|
|
7996
|
+
*/
|
|
7997
|
+
function getSummaryText(file, change) {
|
|
7998
|
+
const status = change.status || getStatus(file);
|
|
7999
|
+
let filePath;
|
|
8000
|
+
if ('path' in file) {
|
|
8001
|
+
filePath = file.path;
|
|
8002
|
+
}
|
|
8003
|
+
else if ('file' in file) {
|
|
8004
|
+
filePath = change?.filePath || file.file;
|
|
8005
|
+
}
|
|
8006
|
+
else {
|
|
8007
|
+
throw new Error('Invalid file type');
|
|
8008
|
+
}
|
|
8009
|
+
if (change.oldFilePath) {
|
|
8010
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
8011
|
+
}
|
|
8012
|
+
return `${status}: ${filePath}`;
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
/**
|
|
8016
|
+
* Parses a file string and returns the parsed file paths.
|
|
8017
|
+
* If the file string contains a separator, it splits the string into root path, file path, and old file path.
|
|
8018
|
+
* If the file string doesn't contain the separator, it assumes the file string itself is the file path and old file path is undefined.
|
|
8019
|
+
* @param file The file string to parse.
|
|
8020
|
+
* @returns The parsed file paths.
|
|
8021
|
+
*/
|
|
8022
|
+
function parseFileString(file) {
|
|
8023
|
+
const separator = ' => ';
|
|
8024
|
+
if (file.includes(separator)) {
|
|
8025
|
+
const [oldFilePathWithRoot, filePath] = file.split(separator);
|
|
8026
|
+
const [rootPath, oldFilePath] = oldFilePathWithRoot.split('{');
|
|
8027
|
+
return {
|
|
8028
|
+
filePath: rootPath + filePath.trim().replace('{', '').replace('}', ''),
|
|
8029
|
+
oldFilePath: rootPath + oldFilePath.trim().replace('{', '').replace('}', ''),
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
else {
|
|
8033
|
+
return {
|
|
8034
|
+
filePath: file.trim(),
|
|
8035
|
+
oldFilePath: undefined,
|
|
8036
|
+
};
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
|
|
8040
|
+
const config = loadConfig();
|
|
8041
|
+
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
8042
|
+
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
8043
|
+
/**
|
|
8044
|
+
* Retrieves the changes made in a commit.
|
|
8045
|
+
*
|
|
8046
|
+
* @deprecated use `getChanges` instead
|
|
8047
|
+
*
|
|
8048
|
+
* @param commit - The commit hash.
|
|
8049
|
+
* @param options - Optional parameters for customization.
|
|
8050
|
+
* @returns A promise that resolves to an array of FileChange objects representing the changes made in the commit.
|
|
8051
|
+
*/
|
|
8052
|
+
async function getChangesByCommit({ commit, options: { git, ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS, }, }) {
|
|
8053
|
+
const changes = [];
|
|
8054
|
+
const diffSummary = await git.diffSummary([`${commit}^..${commit}`]);
|
|
8055
|
+
diffSummary.files.forEach((file) => {
|
|
8056
|
+
const { filePath, oldFilePath } = parseFileString(file.file);
|
|
8057
|
+
const fileChange = {
|
|
8058
|
+
filePath,
|
|
8059
|
+
oldFilePath,
|
|
8060
|
+
status: getStatus(file),
|
|
8061
|
+
};
|
|
8062
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
8063
|
+
changes.push(fileChange);
|
|
8064
|
+
});
|
|
8065
|
+
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
8066
|
+
const filteredChanges = changes.filter((file) => {
|
|
8067
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
8068
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
8069
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
8070
|
+
});
|
|
8071
|
+
return filteredChanges;
|
|
8072
|
+
}
|
|
8073
|
+
|
|
7561
8074
|
/**
|
|
7562
8075
|
* Retrieves the SimpleGit instance for the repository.
|
|
7563
8076
|
* @returns {SimpleGit} The SimpleGit instance.
|
|
@@ -7919,116 +8432,18 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
|
|
|
7919
8432
|
}
|
|
7920
8433
|
|
|
7921
8434
|
/**
|
|
7922
|
-
*
|
|
8435
|
+
* Retrieves the diff between the current branch and a specified target branch.
|
|
7923
8436
|
*
|
|
7924
|
-
* @param
|
|
7925
|
-
* @
|
|
8437
|
+
* @param {Object} options - The options for retrieving the diff.
|
|
8438
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
8439
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
8440
|
+
* @param {string} options.baseBranch - The base branch to compare against.
|
|
8441
|
+
* @param {string} options.headBranch - The head branch to compare.
|
|
8442
|
+
* @param {string[]} options.ignoredFiles - Array of specific files to ignore.
|
|
8443
|
+
* @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
|
|
8444
|
+
* @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
|
|
7926
8445
|
*/
|
|
7927
|
-
async function
|
|
7928
|
-
try {
|
|
7929
|
-
return await git.diff(['-p', `${commitId}^..${commitId}`]);
|
|
7930
|
-
}
|
|
7931
|
-
catch (error) {
|
|
7932
|
-
throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
|
|
7933
|
-
}
|
|
7934
|
-
}
|
|
7935
|
-
|
|
7936
|
-
/**
|
|
7937
|
-
* Determines the status of a file based on its changes in the Git repository.
|
|
7938
|
-
*
|
|
7939
|
-
* @param file - The file to check the status of.
|
|
7940
|
-
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
7941
|
-
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
7942
|
-
* @throws Error if the file type is invalid.
|
|
7943
|
-
*/
|
|
7944
|
-
function getStatus(file, location = 'index') {
|
|
7945
|
-
if ('index' in file && 'working_dir' in file) {
|
|
7946
|
-
const statusCode = file[location];
|
|
7947
|
-
switch (statusCode) {
|
|
7948
|
-
case 'A':
|
|
7949
|
-
return 'added';
|
|
7950
|
-
case 'D':
|
|
7951
|
-
return 'deleted';
|
|
7952
|
-
case 'M':
|
|
7953
|
-
return 'modified';
|
|
7954
|
-
case 'R':
|
|
7955
|
-
return 'renamed';
|
|
7956
|
-
case '?':
|
|
7957
|
-
return 'untracked';
|
|
7958
|
-
default:
|
|
7959
|
-
return 'unknown';
|
|
7960
|
-
}
|
|
7961
|
-
}
|
|
7962
|
-
else if ('binary' in file && file.binary === true) {
|
|
7963
|
-
// DiffResultBinaryFile: has before/after, no changes/insertions/deletions
|
|
7964
|
-
if (file.file.includes('=>'))
|
|
7965
|
-
return 'renamed';
|
|
7966
|
-
if (file.before === 0 && file.after > 0)
|
|
7967
|
-
return 'added';
|
|
7968
|
-
if (file.after === 0 && file.before > 0)
|
|
7969
|
-
return 'deleted';
|
|
7970
|
-
if (file.before > 0 && file.after > 0)
|
|
7971
|
-
return 'modified';
|
|
7972
|
-
return 'untracked';
|
|
7973
|
-
}
|
|
7974
|
-
else if ('changes' in file && 'binary' in file) {
|
|
7975
|
-
// DiffResultTextFile: has changes/insertions/deletions
|
|
7976
|
-
if (file.changes === 0)
|
|
7977
|
-
return 'untracked';
|
|
7978
|
-
if (file.file.includes('=>'))
|
|
7979
|
-
return 'renamed';
|
|
7980
|
-
if (file.deletions === 0 && file.insertions > 0)
|
|
7981
|
-
return 'added';
|
|
7982
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
7983
|
-
return 'deleted';
|
|
7984
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
7985
|
-
return 'modified';
|
|
7986
|
-
return 'unknown';
|
|
7987
|
-
}
|
|
7988
|
-
else {
|
|
7989
|
-
throw new Error('Invalid file type');
|
|
7990
|
-
}
|
|
7991
|
-
}
|
|
7992
|
-
|
|
7993
|
-
/**
|
|
7994
|
-
* Returns the summary text for a file change.
|
|
7995
|
-
*
|
|
7996
|
-
* @param file - The file status or diff result.
|
|
7997
|
-
* @param change - The partial file change object.
|
|
7998
|
-
* @returns The summary text for the file change.
|
|
7999
|
-
* @throws Error if the file type is invalid.
|
|
8000
|
-
*/
|
|
8001
|
-
function getSummaryText(file, change) {
|
|
8002
|
-
const status = change.status || getStatus(file);
|
|
8003
|
-
let filePath;
|
|
8004
|
-
if ('path' in file) {
|
|
8005
|
-
filePath = file.path;
|
|
8006
|
-
}
|
|
8007
|
-
else if ('file' in file) {
|
|
8008
|
-
filePath = change?.filePath || file.file;
|
|
8009
|
-
}
|
|
8010
|
-
else {
|
|
8011
|
-
throw new Error('Invalid file type');
|
|
8012
|
-
}
|
|
8013
|
-
if (change.oldFilePath) {
|
|
8014
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
8015
|
-
}
|
|
8016
|
-
return `${status}: ${filePath}`;
|
|
8017
|
-
}
|
|
8018
|
-
|
|
8019
|
-
/**
|
|
8020
|
-
* Retrieves the diff between the current branch and a specified target branch.
|
|
8021
|
-
*
|
|
8022
|
-
* @param {Object} options - The options for retrieving the diff.
|
|
8023
|
-
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
8024
|
-
* @param {Logger} options.logger - The logger for logging messages.
|
|
8025
|
-
* @param {string} options.baseBranch - The base branch to compare against.
|
|
8026
|
-
* @param {string} options.headBranch - The head branch to compare.
|
|
8027
|
-
* @param {string[]} options.ignoredFiles - Array of specific files to ignore.
|
|
8028
|
-
* @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
|
|
8029
|
-
* @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
|
|
8030
|
-
*/
|
|
8031
|
-
async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
|
|
8446
|
+
async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
|
|
8032
8447
|
try {
|
|
8033
8448
|
logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
|
|
8034
8449
|
color: 'blue',
|
|
@@ -8096,726 +8511,239 @@ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options,
|
|
|
8096
8511
|
}
|
|
8097
8512
|
}
|
|
8098
8513
|
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
## Rules
|
|
8108
|
-
- Create a descriptive title for the changelog that gives a high-level overview of the changes.
|
|
8109
|
-
- **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
|
|
8110
|
-
- **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
|
|
8111
|
-
- **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
|
|
8112
|
-
- **Summaries**: For each change, provide a concise summary.
|
|
8113
|
-
- **Attribution**: {{author_instructions}}
|
|
8114
|
-
- **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
|
|
8115
|
-
- **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
|
|
8116
|
-
- **Formatting**: Your entire response must be valid Markdown.
|
|
8117
|
-
|
|
8118
|
-
## Formatting Instructions
|
|
8119
|
-
{{format_instructions}}
|
|
8120
|
-
|
|
8121
|
-
{{additional_context}}
|
|
8514
|
+
/**
|
|
8515
|
+
* Extract the path from a file path string.
|
|
8516
|
+
* @param {string} filePath - The full file path.
|
|
8517
|
+
* @returns {string} The path portion of the file path.
|
|
8518
|
+
*/
|
|
8519
|
+
function getPathFromFilePath(filePath) {
|
|
8520
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
8521
|
+
}
|
|
8122
8522
|
|
|
8123
|
-
|
|
8124
|
-
const
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
const
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
});
|
|
8523
|
+
async function summarize(documents, { chain, textSplitter, options, logger, tokenizer, metadata }) {
|
|
8524
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
8525
|
+
const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
|
|
8526
|
+
const promptTokens = tokenizer
|
|
8527
|
+
? docs.reduce((sum, doc) => sum + tokenizer(doc.pageContent), 0)
|
|
8528
|
+
: undefined;
|
|
8529
|
+
const startedAt = Date.now();
|
|
8530
|
+
const res = await chain.invoke({
|
|
8531
|
+
input_documents: docs,
|
|
8532
|
+
returnIntermediateSteps,
|
|
8533
|
+
});
|
|
8534
|
+
const elapsedMs = Date.now() - startedAt;
|
|
8535
|
+
logLlmCall(logger, {
|
|
8536
|
+
task: 'summarize',
|
|
8537
|
+
promptTokens,
|
|
8538
|
+
elapsedMs,
|
|
8539
|
+
inputDocuments: documents.length,
|
|
8540
|
+
inputChunks: docs.length,
|
|
8541
|
+
...metadata,
|
|
8542
|
+
});
|
|
8543
|
+
if (res.error)
|
|
8544
|
+
throw new Error(res.error);
|
|
8545
|
+
return res.text && res.text.trim();
|
|
8546
|
+
}
|
|
8134
8547
|
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8548
|
+
/**
|
|
8549
|
+
* Summarize a single file diff that exceeds the token threshold.
|
|
8550
|
+
*/
|
|
8551
|
+
async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer, logger, metadata, }) {
|
|
8552
|
+
try {
|
|
8553
|
+
const fileSummary = await summarize([
|
|
8554
|
+
{
|
|
8555
|
+
pageContent: fileDiff.diff,
|
|
8556
|
+
metadata: {
|
|
8557
|
+
file: fileDiff.file,
|
|
8558
|
+
summary: fileDiff.summary,
|
|
8559
|
+
},
|
|
8560
|
+
},
|
|
8561
|
+
], {
|
|
8562
|
+
chain,
|
|
8563
|
+
textSplitter,
|
|
8564
|
+
tokenizer,
|
|
8565
|
+
logger,
|
|
8566
|
+
metadata: {
|
|
8567
|
+
...metadata,
|
|
8568
|
+
task: 'summarize-large-file',
|
|
8569
|
+
},
|
|
8570
|
+
options: {
|
|
8571
|
+
returnIntermediateSteps: false,
|
|
8572
|
+
},
|
|
8573
|
+
});
|
|
8574
|
+
const newTokenCount = tokenizer(fileSummary);
|
|
8575
|
+
return {
|
|
8576
|
+
...fileDiff,
|
|
8577
|
+
diff: fileSummary,
|
|
8578
|
+
tokenCount: newTokenCount,
|
|
8579
|
+
};
|
|
8148
8580
|
}
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8581
|
+
catch (error) {
|
|
8582
|
+
// On error, return original diff unchanged
|
|
8583
|
+
console.error(`Failed to summarize file ${fileDiff.file}:`, error);
|
|
8584
|
+
return fileDiff;
|
|
8152
8585
|
}
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8586
|
+
}
|
|
8587
|
+
/**
|
|
8588
|
+
* Process files in waves to respect concurrency limits.
|
|
8589
|
+
*/
|
|
8590
|
+
async function processInWaves$1(items, processor, maxConcurrent) {
|
|
8591
|
+
const results = [];
|
|
8592
|
+
for (let i = 0; i < items.length; i += maxConcurrent) {
|
|
8593
|
+
const wave = items.slice(i, i + maxConcurrent);
|
|
8594
|
+
const waveResults = await Promise.all(wave.map(processor));
|
|
8595
|
+
results.push(...waveResults);
|
|
8159
8596
|
}
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
const [from, to] = config.range.split(':');
|
|
8180
|
-
if (!from || !to) {
|
|
8181
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
8182
|
-
process.exit(1);
|
|
8183
|
-
}
|
|
8184
|
-
commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
|
|
8185
|
-
}
|
|
8186
|
-
else if (argv.branch) {
|
|
8187
|
-
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
8188
|
-
commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
|
|
8189
|
-
}
|
|
8190
|
-
else if (argv.tag) {
|
|
8191
|
-
logger.verbose(`Generating commit log against tag: ${argv.tag}`, { color: 'yellow' });
|
|
8192
|
-
commits = await getCommitLogAgainstTag({ git, logger, targetTag: argv.tag });
|
|
8193
|
-
}
|
|
8194
|
-
else {
|
|
8195
|
-
logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
|
|
8196
|
-
color: 'yellow',
|
|
8197
|
-
});
|
|
8198
|
-
commits = await getCommitLogCurrentBranch({ git, logger });
|
|
8199
|
-
}
|
|
8200
|
-
let commitsWithDiffText = commits;
|
|
8201
|
-
if (argv.withDiff) {
|
|
8202
|
-
commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
|
|
8203
|
-
...commit,
|
|
8204
|
-
diffText: await getDiffForCommit(commit.hash, { git }),
|
|
8205
|
-
})));
|
|
8597
|
+
return results;
|
|
8598
|
+
}
|
|
8599
|
+
/**
|
|
8600
|
+
* Pre-summarize individual files that exceed the maxFileTokens threshold.
|
|
8601
|
+
* This prevents large files from dominating the token budget and biasing
|
|
8602
|
+
* the final commit message toward a single file's changes.
|
|
8603
|
+
*
|
|
8604
|
+
* @param diffs - Array of file diffs to process
|
|
8605
|
+
* @param options - Configuration options for summarization
|
|
8606
|
+
* @returns Array of file diffs with large files summarized
|
|
8607
|
+
*/
|
|
8608
|
+
async function summarizeLargeFiles(diffs, options) {
|
|
8609
|
+
const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter, metadata } = options;
|
|
8610
|
+
// Identify files that need summarization
|
|
8611
|
+
const filesToSummarize = [];
|
|
8612
|
+
const results = [...diffs];
|
|
8613
|
+
diffs.forEach((diff, index) => {
|
|
8614
|
+
if (diff.tokenCount > maxFileTokens && diff.tokenCount >= minTokensForSummary) {
|
|
8615
|
+
filesToSummarize.push({ index, diff });
|
|
8206
8616
|
}
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
withDiff: argv.withDiff,
|
|
8211
|
-
};
|
|
8617
|
+
});
|
|
8618
|
+
if (filesToSummarize.length === 0) {
|
|
8619
|
+
return results;
|
|
8212
8620
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
if (data.withDiff && commit.diffText) {
|
|
8224
|
-
commitStr += `\nDiff:\n${commit.diffText}`;
|
|
8225
|
-
}
|
|
8226
|
-
return commitStr.trim();
|
|
8227
|
-
}).join('\n\n---\n\n');
|
|
8228
|
-
return result;
|
|
8229
|
-
}
|
|
8230
|
-
const changelogMsg = await generateAndReviewLoop({
|
|
8231
|
-
label: 'changelog',
|
|
8232
|
-
options: {
|
|
8233
|
-
...config,
|
|
8234
|
-
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
8235
|
-
logger,
|
|
8236
|
-
interactive: INTERACTIVE,
|
|
8237
|
-
review: {
|
|
8238
|
-
enableFullRetry: false,
|
|
8239
|
-
},
|
|
8240
|
-
},
|
|
8241
|
-
factory,
|
|
8242
|
-
parser,
|
|
8243
|
-
agent: async (context, options) => {
|
|
8244
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8245
|
-
const parser = createSchemaParser(ChangelogResponseSchema, llm);
|
|
8246
|
-
const prompt = getPrompt({
|
|
8247
|
-
template: options.prompt,
|
|
8248
|
-
variables: CHANGELOG_PROMPT.inputVariables,
|
|
8249
|
-
fallback: CHANGELOG_PROMPT,
|
|
8250
|
-
});
|
|
8251
|
-
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.";
|
|
8252
|
-
let additional_context = '';
|
|
8253
|
-
if (argv.additional) {
|
|
8254
|
-
additional_context = `## Additional Context\n${argv.additional}`;
|
|
8255
|
-
}
|
|
8256
|
-
const author_instructions = argv.author
|
|
8257
|
-
? 'At the end of each item, attribute the author and include a reference to the commit hash, like this: `by @author_name (f6dbe61)`. Use the first 7 characters of the hash.'
|
|
8258
|
-
: 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
|
|
8259
|
-
const changelog = await executeChain({
|
|
8260
|
-
llm,
|
|
8261
|
-
prompt,
|
|
8262
|
-
variables: {
|
|
8263
|
-
summary: context,
|
|
8264
|
-
format_instructions: formatInstructions,
|
|
8265
|
-
additional_context: additional_context,
|
|
8266
|
-
author_instructions: author_instructions,
|
|
8267
|
-
},
|
|
8268
|
-
parser,
|
|
8269
|
-
});
|
|
8270
|
-
const branchName = await getCurrentBranchName({ git });
|
|
8271
|
-
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
8272
|
-
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
8273
|
-
return `${changelog.title}\n\n${changelog.content}${footer}`;
|
|
8274
|
-
},
|
|
8275
|
-
noResult: async () => {
|
|
8276
|
-
if (config.range) {
|
|
8277
|
-
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
8278
|
-
process.exit(0);
|
|
8279
|
-
}
|
|
8280
|
-
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
8281
|
-
process.exit(0);
|
|
8282
|
-
},
|
|
8283
|
-
});
|
|
8284
|
-
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
8285
|
-
handleResult({
|
|
8286
|
-
result: changelogMsg,
|
|
8287
|
-
interactiveModeCallback: async () => {
|
|
8288
|
-
logSuccess();
|
|
8289
|
-
},
|
|
8290
|
-
mode: MODE,
|
|
8621
|
+
logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
|
|
8622
|
+
// Process large files in waves
|
|
8623
|
+
const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer, logger, metadata }), maxConcurrent);
|
|
8624
|
+
// Update results with summarized files
|
|
8625
|
+
summarizedFiles.forEach((summarizedDiff, i) => {
|
|
8626
|
+
const originalIndex = filesToSummarize[i].index;
|
|
8627
|
+
const originalTokens = results[originalIndex].tokenCount;
|
|
8628
|
+
const newTokens = summarizedDiff.tokenCount;
|
|
8629
|
+
logger.verbose(` - ${summarizedDiff.file}: ${originalTokens} -> ${newTokens} tokens`, { color: 'magenta' });
|
|
8630
|
+
results[originalIndex] = summarizedDiff;
|
|
8291
8631
|
});
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
var changelog = {
|
|
8295
|
-
command: command$4,
|
|
8296
|
-
desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
|
|
8297
|
-
builder: builder$4,
|
|
8298
|
-
handler: commandExecutor(handler$4),
|
|
8299
|
-
options: options$4,
|
|
8300
|
-
};
|
|
8301
|
-
|
|
8302
|
-
const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
|
|
8303
|
-
// Regular commit message schema with basic validation
|
|
8304
|
-
const CommitMessageResponseSchema = objectType({
|
|
8305
|
-
title: stringType().describe("Title of the commit message"),
|
|
8306
|
-
body: stringType().describe("Body of the commit message"),
|
|
8307
|
-
}).describe("Object with commit message 'title' and 'body'");
|
|
8308
|
-
// Conventional commit message schema with strict formatting rules
|
|
8309
|
-
const ConventionalCommitMessageResponseSchema = objectType({
|
|
8310
|
-
title: stringType()
|
|
8311
|
-
.max(50, "Title must be 50 characters or less")
|
|
8312
|
-
.refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')").describe("Title of the commit message"),
|
|
8313
|
-
body: stringType().describe("Body of the commit message")
|
|
8314
|
-
// .max(280, "Body must be 280 characters or less"),
|
|
8315
|
-
}).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
|
|
8316
|
-
const command$3 = 'commit';
|
|
8632
|
+
return results;
|
|
8633
|
+
}
|
|
8317
8634
|
/**
|
|
8318
|
-
*
|
|
8635
|
+
* Pre-process a DiffNode tree, summarizing large files at the leaf level.
|
|
8636
|
+
* Returns a new DiffNode with updated token counts.
|
|
8319
8637
|
*/
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
additional: {
|
|
8344
|
-
description: 'Add extra contextual information to the prompt',
|
|
8345
|
-
type: 'string',
|
|
8346
|
-
alias: 'a',
|
|
8347
|
-
},
|
|
8348
|
-
withPreviousCommits: {
|
|
8349
|
-
description: 'Include previous commits as context (specify number of commits, 0 for none)',
|
|
8350
|
-
type: 'number',
|
|
8351
|
-
default: 0,
|
|
8352
|
-
alias: 'p',
|
|
8353
|
-
},
|
|
8354
|
-
conventional: {
|
|
8355
|
-
description: 'Generate commit message in Conventional Commits format',
|
|
8356
|
-
type: 'boolean',
|
|
8357
|
-
default: false,
|
|
8358
|
-
alias: 'c',
|
|
8359
|
-
},
|
|
8360
|
-
includeBranchName: {
|
|
8361
|
-
description: 'Include the current branch name in the commit prompt for context',
|
|
8362
|
-
type: 'boolean',
|
|
8363
|
-
default: true,
|
|
8364
|
-
},
|
|
8365
|
-
noDiff: {
|
|
8366
|
-
description: 'Only pass basic "git status" result instead of providing entire diff',
|
|
8367
|
-
type: 'boolean',
|
|
8368
|
-
default: false,
|
|
8369
|
-
},
|
|
8370
|
-
};
|
|
8371
|
-
const builder$3 = (yargs) => {
|
|
8372
|
-
return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
|
|
8373
|
-
};
|
|
8638
|
+
async function preprocessLargeFiles(rootNode, options) {
|
|
8639
|
+
// Collect all diffs from the tree
|
|
8640
|
+
const allDiffs = [];
|
|
8641
|
+
function collectDiffs(node) {
|
|
8642
|
+
allDiffs.push(...node.diffs);
|
|
8643
|
+
node.children.forEach(collectDiffs);
|
|
8644
|
+
}
|
|
8645
|
+
collectDiffs(rootNode);
|
|
8646
|
+
// Summarize large files
|
|
8647
|
+
const processedDiffs = await summarizeLargeFiles(allDiffs, options);
|
|
8648
|
+
// Create a map for quick lookup
|
|
8649
|
+
const diffMap = new Map();
|
|
8650
|
+
processedDiffs.forEach((diff) => diffMap.set(diff.file, diff));
|
|
8651
|
+
// Rebuild tree with processed diffs
|
|
8652
|
+
function rebuildNode(node) {
|
|
8653
|
+
return {
|
|
8654
|
+
path: node.path,
|
|
8655
|
+
diffs: node.diffs.map((diff) => diffMap.get(diff.file) || diff),
|
|
8656
|
+
children: node.children.map(rebuildNode),
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
return rebuildNode(rootNode);
|
|
8660
|
+
}
|
|
8374
8661
|
|
|
8375
8662
|
/**
|
|
8376
|
-
*
|
|
8377
|
-
*
|
|
8378
|
-
* @
|
|
8379
|
-
* @param llm - LLM instance
|
|
8380
|
-
* @param prompt - Prompt template
|
|
8381
|
-
* @param variables - Variables for the prompt
|
|
8382
|
-
* @param options - Configuration options
|
|
8383
|
-
* @returns Parsed result matching the schema type
|
|
8663
|
+
* Create groups from a given node info.
|
|
8664
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
8665
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
8384
8666
|
*/
|
|
8385
|
-
|
|
8386
|
-
const
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
prompt,
|
|
8393
|
-
variables,
|
|
8394
|
-
parser,
|
|
8395
|
-
});
|
|
8396
|
-
return result;
|
|
8397
|
-
};
|
|
8398
|
-
try {
|
|
8399
|
-
return await withRetry(operation, retryOptions);
|
|
8400
|
-
}
|
|
8401
|
-
catch (error) {
|
|
8402
|
-
if (fallbackParser) {
|
|
8403
|
-
if (onFallback) {
|
|
8404
|
-
onFallback();
|
|
8667
|
+
function createDirectoryDiffs(node) {
|
|
8668
|
+
const groupByPath = {};
|
|
8669
|
+
function traverse(node) {
|
|
8670
|
+
node.diffs.forEach((diff) => {
|
|
8671
|
+
const path = getPathFromFilePath(diff.file);
|
|
8672
|
+
if (!groupByPath[path]) {
|
|
8673
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
8405
8674
|
}
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
parser: new StringOutputParser(),
|
|
8411
|
-
});
|
|
8412
|
-
const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
|
|
8413
|
-
return fallbackParser(fallbackText);
|
|
8414
|
-
}
|
|
8415
|
-
// No fallback available, re-throw the error
|
|
8416
|
-
throw error;
|
|
8675
|
+
groupByPath[path].diffs.push(diff);
|
|
8676
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
8677
|
+
});
|
|
8678
|
+
node.children.forEach(traverse);
|
|
8417
8679
|
}
|
|
8680
|
+
traverse(node);
|
|
8681
|
+
return Object.values(groupByPath);
|
|
8418
8682
|
}
|
|
8419
|
-
|
|
8420
8683
|
/**
|
|
8421
|
-
*
|
|
8422
|
-
* Specifically handles cases where string values are not properly quoted
|
|
8684
|
+
* Summarize a directory diff asynchronously.
|
|
8423
8685
|
*/
|
|
8424
|
-
function
|
|
8425
|
-
// Remove any markdown code block wrapping
|
|
8426
|
-
let cleaned = jsonString.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, '$1').trim();
|
|
8427
|
-
// Remove inline code block wrapping
|
|
8428
|
-
cleaned = cleaned.replace(/^`(.*)`$/, '$1').trim();
|
|
8429
|
-
// If it doesn't look like JSON, return as-is
|
|
8430
|
-
if (!cleaned.startsWith('{') || !cleaned.endsWith('}')) {
|
|
8431
|
-
return jsonString;
|
|
8432
|
-
}
|
|
8686
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer, logger, metadata }) {
|
|
8433
8687
|
try {
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
// Try to repair common issues
|
|
8440
|
-
let repaired = cleaned;
|
|
8441
|
-
// Fix unquoted string values in title and body fields
|
|
8442
|
-
// Pattern: "title": unquoted_value, -> "title": "unquoted_value",
|
|
8443
|
-
repaired = repaired.replace(/"(title|body)":\s*([^",\{\}\[\]]+?)(?=\s*[,\}])/g, (match, field, value) => {
|
|
8444
|
-
// Clean up the value (remove leading/trailing whitespace)
|
|
8445
|
-
const cleanValue = value.trim();
|
|
8446
|
-
// If it's already quoted or looks like a number/boolean, leave it
|
|
8447
|
-
if (cleanValue.startsWith('"') || /^(true|false|\d+)$/.test(cleanValue)) {
|
|
8448
|
-
return match;
|
|
8449
|
-
}
|
|
8450
|
-
// Quote the value
|
|
8451
|
-
return `"${field}": "${cleanValue}"`;
|
|
8452
|
-
});
|
|
8453
|
-
// Fix missing quotes around field names (though this should be rare)
|
|
8454
|
-
repaired = repaired.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
8455
|
-
// Remove trailing commas before closing braces
|
|
8456
|
-
repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
|
|
8457
|
-
try {
|
|
8458
|
-
// Test if the repair worked
|
|
8459
|
-
JSON.parse(repaired);
|
|
8460
|
-
return repaired;
|
|
8461
|
-
}
|
|
8462
|
-
catch {
|
|
8463
|
-
// If repair failed, return original
|
|
8464
|
-
return jsonString;
|
|
8465
|
-
}
|
|
8466
|
-
}
|
|
8467
|
-
}
|
|
8468
|
-
|
|
8469
|
-
/**
|
|
8470
|
-
* Extract the first complete JSON object from a string by tracking balanced braces
|
|
8471
|
-
*/
|
|
8472
|
-
function extractFirstJsonObject(text) {
|
|
8473
|
-
const startIndex = text.indexOf('{');
|
|
8474
|
-
if (startIndex === -1)
|
|
8475
|
-
return null;
|
|
8476
|
-
let braceCount = 0;
|
|
8477
|
-
let inString = false;
|
|
8478
|
-
let escapeNext = false;
|
|
8479
|
-
for (let i = startIndex; i < text.length; i++) {
|
|
8480
|
-
const char = text[i];
|
|
8481
|
-
if (escapeNext) {
|
|
8482
|
-
escapeNext = false;
|
|
8483
|
-
continue;
|
|
8484
|
-
}
|
|
8485
|
-
if (char === '\\') {
|
|
8486
|
-
escapeNext = true;
|
|
8487
|
-
continue;
|
|
8488
|
-
}
|
|
8489
|
-
if (char === '"') {
|
|
8490
|
-
inString = !inString;
|
|
8491
|
-
continue;
|
|
8492
|
-
}
|
|
8493
|
-
if (inString)
|
|
8494
|
-
continue;
|
|
8495
|
-
if (char === '{') {
|
|
8496
|
-
braceCount++;
|
|
8497
|
-
}
|
|
8498
|
-
else if (char === '}') {
|
|
8499
|
-
braceCount--;
|
|
8500
|
-
if (braceCount === 0) {
|
|
8501
|
-
// Found the end of the first complete JSON object
|
|
8502
|
-
return text.substring(startIndex, i + 1);
|
|
8503
|
-
}
|
|
8504
|
-
}
|
|
8505
|
-
}
|
|
8506
|
-
return null;
|
|
8507
|
-
}
|
|
8508
|
-
/**
|
|
8509
|
-
* Utility function to ensure commit messages are properly formatted as strings
|
|
8510
|
-
* rather than JSON objects, whether they come as parsed objects or stringified JSON
|
|
8511
|
-
*/
|
|
8512
|
-
function formatCommitMessage(result, options = {}) {
|
|
8513
|
-
const { append, ticketId, appendTicket } = options;
|
|
8514
|
-
// Helper function to construct the final message with appends
|
|
8515
|
-
const constructMessage = (title, body) => {
|
|
8516
|
-
const appendedText = append ? `\n\n${append}` : '';
|
|
8517
|
-
const ticketFooter = appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
8518
|
-
return `${title}\n\n${body}${appendedText}${ticketFooter}`;
|
|
8519
|
-
};
|
|
8520
|
-
// If it's a string, check if it contains a JSON object (including markdown code blocks)
|
|
8521
|
-
if (typeof result === 'string') {
|
|
8522
|
-
// Early return if string clearly doesn't contain JSON-like content
|
|
8523
|
-
if (!result.includes('{') && !result.includes('"title"')) {
|
|
8524
|
-
return result;
|
|
8525
|
-
}
|
|
8526
|
-
// Handle multiple markdown code block formats and embedded JSON
|
|
8527
|
-
const extractionPatterns = [
|
|
8528
|
-
/```(?:json)?\s*(\{[\s\S]*?\})\s*```/, // Standard markdown blocks
|
|
8529
|
-
/`(\{[\s\S]*?\})`/, // Inline code blocks
|
|
8530
|
-
/^\s*(\{[\s\S]*\})\s*$/, // Raw JSON without blocks (entire string)
|
|
8531
|
-
/(\{[\s\S]*?\})/ // JSON anywhere in text (fallback)
|
|
8532
|
-
];
|
|
8533
|
-
let jsonString = result;
|
|
8534
|
-
let foundMatch = false;
|
|
8535
|
-
// Try each pattern to extract JSON
|
|
8536
|
-
for (const pattern of extractionPatterns) {
|
|
8537
|
-
const match = result.match(pattern);
|
|
8538
|
-
if (match && match[1]) {
|
|
8539
|
-
jsonString = match[1].trim();
|
|
8540
|
-
foundMatch = true;
|
|
8541
|
-
break;
|
|
8542
|
-
}
|
|
8543
|
-
}
|
|
8544
|
-
// Only attempt JSON parsing if we found potential JSON content
|
|
8545
|
-
if (foundMatch || jsonString.startsWith('{')) {
|
|
8546
|
-
try {
|
|
8547
|
-
// Try to parse as JSON to see if it's a stringified object
|
|
8548
|
-
const parsed = JSON.parse(jsonString);
|
|
8549
|
-
if (parsed &&
|
|
8550
|
-
typeof parsed === 'object' &&
|
|
8551
|
-
typeof parsed.title === 'string' &&
|
|
8552
|
-
typeof parsed.body === 'string' &&
|
|
8553
|
-
parsed.title.length > 0 &&
|
|
8554
|
-
parsed.body.length > 0) {
|
|
8555
|
-
// It's a valid stringified JSON object, format it properly
|
|
8556
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8557
|
-
}
|
|
8558
|
-
}
|
|
8559
|
-
catch {
|
|
8560
|
-
// Try to repair the JSON and parse again
|
|
8561
|
-
try {
|
|
8562
|
-
const repairedJson = repairJson(jsonString);
|
|
8563
|
-
const parsed = JSON.parse(repairedJson);
|
|
8564
|
-
if (parsed &&
|
|
8565
|
-
typeof parsed === 'object' &&
|
|
8566
|
-
typeof parsed.title === 'string' &&
|
|
8567
|
-
typeof parsed.body === 'string' &&
|
|
8568
|
-
parsed.title.length > 0 &&
|
|
8569
|
-
parsed.body.length > 0) {
|
|
8570
|
-
// Successfully repaired and parsed JSON
|
|
8571
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8572
|
-
}
|
|
8573
|
-
}
|
|
8574
|
-
catch {
|
|
8575
|
-
// Repair failed, try extracting just the first complete JSON object
|
|
8576
|
-
const firstObject = extractFirstJsonObject(jsonString);
|
|
8577
|
-
if (firstObject) {
|
|
8578
|
-
try {
|
|
8579
|
-
const parsed = JSON.parse(firstObject);
|
|
8580
|
-
if (parsed &&
|
|
8581
|
-
typeof parsed === 'object' &&
|
|
8582
|
-
typeof parsed.title === 'string' &&
|
|
8583
|
-
typeof parsed.body === 'string' &&
|
|
8584
|
-
parsed.title.length > 0 &&
|
|
8585
|
-
parsed.body.length > 0) {
|
|
8586
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8587
|
-
}
|
|
8588
|
-
}
|
|
8589
|
-
catch {
|
|
8590
|
-
// Even first object extraction failed, continue to fallback
|
|
8591
|
-
}
|
|
8592
|
-
}
|
|
8593
|
-
}
|
|
8594
|
-
}
|
|
8595
|
-
}
|
|
8596
|
-
// If no JSON found and it's already formatted, return as-is
|
|
8597
|
-
return result;
|
|
8598
|
-
}
|
|
8599
|
-
// If it's already an object with title and body, format it
|
|
8600
|
-
if (typeof result === 'object' && result !== null &&
|
|
8601
|
-
'title' in result && 'body' in result) {
|
|
8602
|
-
const commitMsgObj = result;
|
|
8603
|
-
if (typeof commitMsgObj.title === 'string' && typeof commitMsgObj.body === 'string') {
|
|
8604
|
-
return constructMessage(commitMsgObj.title, commitMsgObj.body);
|
|
8605
|
-
}
|
|
8606
|
-
}
|
|
8607
|
-
// Fallback - convert to string and return as-is
|
|
8608
|
-
return String(result);
|
|
8609
|
-
}
|
|
8610
|
-
|
|
8611
|
-
/**
|
|
8612
|
-
* Extract the path from a file path string.
|
|
8613
|
-
* @param {string} filePath - The full file path.
|
|
8614
|
-
* @returns {string} The path portion of the file path.
|
|
8615
|
-
*/
|
|
8616
|
-
function getPathFromFilePath(filePath) {
|
|
8617
|
-
return filePath.split('/').slice(0, -1).join('/');
|
|
8618
|
-
}
|
|
8619
|
-
|
|
8620
|
-
async function summarize(documents, { chain, textSplitter, options }) {
|
|
8621
|
-
const { returnIntermediateSteps = false } = options || {};
|
|
8622
|
-
const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
|
|
8623
|
-
const res = await chain.invoke({
|
|
8624
|
-
input_documents: docs,
|
|
8625
|
-
returnIntermediateSteps,
|
|
8626
|
-
});
|
|
8627
|
-
if (res.error)
|
|
8628
|
-
throw new Error(res.error);
|
|
8629
|
-
return res.text && res.text.trim();
|
|
8630
|
-
}
|
|
8631
|
-
|
|
8632
|
-
/**
|
|
8633
|
-
* Summarize a single file diff that exceeds the token threshold.
|
|
8634
|
-
*/
|
|
8635
|
-
async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
|
|
8636
|
-
try {
|
|
8637
|
-
const fileSummary = await summarize([
|
|
8638
|
-
{
|
|
8639
|
-
pageContent: fileDiff.diff,
|
|
8640
|
-
metadata: {
|
|
8641
|
-
file: fileDiff.file,
|
|
8642
|
-
summary: fileDiff.summary,
|
|
8643
|
-
},
|
|
8688
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
8689
|
+
pageContent: diff.diff,
|
|
8690
|
+
metadata: {
|
|
8691
|
+
file: diff.file,
|
|
8692
|
+
summary: diff.summary,
|
|
8644
8693
|
},
|
|
8645
|
-
|
|
8694
|
+
})), {
|
|
8646
8695
|
chain,
|
|
8647
8696
|
textSplitter,
|
|
8697
|
+
tokenizer,
|
|
8698
|
+
logger,
|
|
8699
|
+
metadata: {
|
|
8700
|
+
...metadata,
|
|
8701
|
+
task: 'summarize-directory-diff',
|
|
8702
|
+
},
|
|
8648
8703
|
options: {
|
|
8649
|
-
returnIntermediateSteps:
|
|
8704
|
+
returnIntermediateSteps: true,
|
|
8650
8705
|
},
|
|
8651
8706
|
});
|
|
8652
|
-
const
|
|
8707
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
8653
8708
|
return {
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8709
|
+
diffs: directory.diffs,
|
|
8710
|
+
path: directory.path,
|
|
8711
|
+
summary: directorySummary,
|
|
8712
|
+
tokenCount: newTokenTotal,
|
|
8657
8713
|
};
|
|
8658
8714
|
}
|
|
8659
8715
|
catch (error) {
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
return fileDiff;
|
|
8716
|
+
console.error(error);
|
|
8717
|
+
return directory;
|
|
8663
8718
|
}
|
|
8664
8719
|
}
|
|
8665
8720
|
/**
|
|
8666
|
-
*
|
|
8721
|
+
* Default output formatter for directory diffs.
|
|
8722
|
+
*
|
|
8723
|
+
* TODO: Future improvements to consider:
|
|
8724
|
+
* - Hierarchical output showing file -> directory -> overall summary
|
|
8725
|
+
* - Configurable verbosity levels (compact, standard, detailed)
|
|
8726
|
+
* - Machine-readable format option (JSON) for programmatic use
|
|
8727
|
+
* - Semantic grouping by change type (added/modified/deleted) or feature area
|
|
8728
|
+
* - Visual diff indicators showing magnitude of changes
|
|
8667
8729
|
*/
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
results.push(...waveResults);
|
|
8730
|
+
const defaultOutputCallback = (group) => {
|
|
8731
|
+
let output = `
|
|
8732
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
8733
|
+
if (group.summary) {
|
|
8734
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
8674
8735
|
}
|
|
8675
|
-
|
|
8676
|
-
}
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
* the final commit message toward a single file's changes.
|
|
8681
|
-
*
|
|
8682
|
-
* @param diffs - Array of file diffs to process
|
|
8683
|
-
* @param options - Configuration options for summarization
|
|
8684
|
-
* @returns Array of file diffs with large files summarized
|
|
8685
|
-
*/
|
|
8686
|
-
async function summarizeLargeFiles(diffs, options) {
|
|
8687
|
-
const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter } = options;
|
|
8688
|
-
// Identify files that need summarization
|
|
8689
|
-
const filesToSummarize = [];
|
|
8690
|
-
const results = [...diffs];
|
|
8691
|
-
diffs.forEach((diff, index) => {
|
|
8692
|
-
if (diff.tokenCount > maxFileTokens && diff.tokenCount >= minTokensForSummary) {
|
|
8693
|
-
filesToSummarize.push({ index, diff });
|
|
8694
|
-
}
|
|
8695
|
-
});
|
|
8696
|
-
if (filesToSummarize.length === 0) {
|
|
8697
|
-
return results;
|
|
8698
|
-
}
|
|
8699
|
-
logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
|
|
8700
|
-
// Process large files in waves
|
|
8701
|
-
const summarizedFiles = await processInWaves(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer }), maxConcurrent);
|
|
8702
|
-
// Update results with summarized files
|
|
8703
|
-
summarizedFiles.forEach((summarizedDiff, i) => {
|
|
8704
|
-
const originalIndex = filesToSummarize[i].index;
|
|
8705
|
-
const originalTokens = results[originalIndex].tokenCount;
|
|
8706
|
-
const newTokens = summarizedDiff.tokenCount;
|
|
8707
|
-
logger.verbose(` - ${summarizedDiff.file}: ${originalTokens} -> ${newTokens} tokens`, { color: 'magenta' });
|
|
8708
|
-
results[originalIndex] = summarizedDiff;
|
|
8709
|
-
});
|
|
8710
|
-
return results;
|
|
8711
|
-
}
|
|
8712
|
-
/**
|
|
8713
|
-
* Pre-process a DiffNode tree, summarizing large files at the leaf level.
|
|
8714
|
-
* Returns a new DiffNode with updated token counts.
|
|
8715
|
-
*/
|
|
8716
|
-
async function preprocessLargeFiles(rootNode, options) {
|
|
8717
|
-
// Collect all diffs from the tree
|
|
8718
|
-
const allDiffs = [];
|
|
8719
|
-
function collectDiffs(node) {
|
|
8720
|
-
allDiffs.push(...node.diffs);
|
|
8721
|
-
node.children.forEach(collectDiffs);
|
|
8722
|
-
}
|
|
8723
|
-
collectDiffs(rootNode);
|
|
8724
|
-
// Summarize large files
|
|
8725
|
-
const processedDiffs = await summarizeLargeFiles(allDiffs, options);
|
|
8726
|
-
// Create a map for quick lookup
|
|
8727
|
-
const diffMap = new Map();
|
|
8728
|
-
processedDiffs.forEach((diff) => diffMap.set(diff.file, diff));
|
|
8729
|
-
// Rebuild tree with processed diffs
|
|
8730
|
-
function rebuildNode(node) {
|
|
8731
|
-
return {
|
|
8732
|
-
path: node.path,
|
|
8733
|
-
diffs: node.diffs.map((diff) => diffMap.get(diff.file) || diff),
|
|
8734
|
-
children: node.children.map(rebuildNode),
|
|
8735
|
-
};
|
|
8736
|
-
}
|
|
8737
|
-
return rebuildNode(rootNode);
|
|
8738
|
-
}
|
|
8739
|
-
|
|
8740
|
-
/**
|
|
8741
|
-
* Create groups from a given node info.
|
|
8742
|
-
* @param {DiffNode} node - The node info to start grouping.
|
|
8743
|
-
* @returns {DirectoryDiff[]} The groups created.
|
|
8744
|
-
*/
|
|
8745
|
-
function createDirectoryDiffs(node) {
|
|
8746
|
-
const groupByPath = {};
|
|
8747
|
-
function traverse(node) {
|
|
8748
|
-
node.diffs.forEach((diff) => {
|
|
8749
|
-
const path = getPathFromFilePath(diff.file);
|
|
8750
|
-
if (!groupByPath[path]) {
|
|
8751
|
-
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
8752
|
-
}
|
|
8753
|
-
groupByPath[path].diffs.push(diff);
|
|
8754
|
-
groupByPath[path].tokenCount += diff.tokenCount;
|
|
8755
|
-
});
|
|
8756
|
-
node.children.forEach(traverse);
|
|
8757
|
-
}
|
|
8758
|
-
traverse(node);
|
|
8759
|
-
return Object.values(groupByPath);
|
|
8760
|
-
}
|
|
8761
|
-
/**
|
|
8762
|
-
* Summarize a directory diff asynchronously.
|
|
8763
|
-
*/
|
|
8764
|
-
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
8765
|
-
try {
|
|
8766
|
-
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
8767
|
-
pageContent: diff.diff,
|
|
8768
|
-
metadata: {
|
|
8769
|
-
file: diff.file,
|
|
8770
|
-
summary: diff.summary,
|
|
8771
|
-
},
|
|
8772
|
-
})), {
|
|
8773
|
-
chain,
|
|
8774
|
-
textSplitter,
|
|
8775
|
-
options: {
|
|
8776
|
-
returnIntermediateSteps: true,
|
|
8777
|
-
},
|
|
8778
|
-
});
|
|
8779
|
-
const newTokenTotal = tokenizer(directorySummary);
|
|
8780
|
-
return {
|
|
8781
|
-
diffs: directory.diffs,
|
|
8782
|
-
path: directory.path,
|
|
8783
|
-
summary: directorySummary,
|
|
8784
|
-
tokenCount: newTokenTotal,
|
|
8785
|
-
};
|
|
8786
|
-
}
|
|
8787
|
-
catch (error) {
|
|
8788
|
-
console.error(error);
|
|
8789
|
-
return directory;
|
|
8790
|
-
}
|
|
8791
|
-
}
|
|
8792
|
-
/**
|
|
8793
|
-
* Default output formatter for directory diffs.
|
|
8794
|
-
*
|
|
8795
|
-
* TODO: Future improvements to consider:
|
|
8796
|
-
* - Hierarchical output showing file -> directory -> overall summary
|
|
8797
|
-
* - Configurable verbosity levels (compact, standard, detailed)
|
|
8798
|
-
* - Machine-readable format option (JSON) for programmatic use
|
|
8799
|
-
* - Semantic grouping by change type (added/modified/deleted) or feature area
|
|
8800
|
-
* - Visual diff indicators showing magnitude of changes
|
|
8801
|
-
*/
|
|
8802
|
-
const defaultOutputCallback = (group) => {
|
|
8803
|
-
let output = `
|
|
8804
|
-
-------\n* changes in "/${group.path}"\n\n`;
|
|
8805
|
-
if (group.summary) {
|
|
8806
|
-
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
8807
|
-
}
|
|
8808
|
-
else {
|
|
8809
|
-
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
8810
|
-
}
|
|
8811
|
-
return output;
|
|
8812
|
-
};
|
|
8736
|
+
else {
|
|
8737
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
8738
|
+
}
|
|
8739
|
+
return output;
|
|
8740
|
+
};
|
|
8813
8741
|
/**
|
|
8814
8742
|
* Process directory summarization in waves to respect concurrency limits
|
|
8815
8743
|
* while maintaining predictable behavior.
|
|
8816
8744
|
*/
|
|
8817
8745
|
async function summarizeInWaves(directories, options) {
|
|
8818
|
-
const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, } = options;
|
|
8746
|
+
const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, metadata, } = options;
|
|
8819
8747
|
let totalTokenCount = initialTotal;
|
|
8820
8748
|
const results = [...directories];
|
|
8821
8749
|
// Create sorted indices by token count (descending) for prioritized processing
|
|
@@ -8847,7 +8775,7 @@ async function summarizeInWaves(directories, options) {
|
|
|
8847
8775
|
}
|
|
8848
8776
|
logger.verbose(`\nProcessing wave of ${wave.length} directories...`, { color: 'blue' });
|
|
8849
8777
|
// Process wave in parallel
|
|
8850
|
-
const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer })));
|
|
8778
|
+
const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer, logger, metadata })));
|
|
8851
8779
|
// Update results and recalculate total
|
|
8852
8780
|
waveResults.forEach((result, i) => {
|
|
8853
8781
|
const idx = wave[i];
|
|
@@ -8884,10 +8812,25 @@ async function summarizeInWaves(directories, options) {
|
|
|
8884
8812
|
* - Efficient parallel processing with predictable behavior
|
|
8885
8813
|
* - Early exit when under token budget
|
|
8886
8814
|
*/
|
|
8887
|
-
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
8815
|
+
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, metadata, handleOutput = defaultOutputCallback, }) {
|
|
8888
8816
|
// Calculate maxFileTokens as 25% of maxTokens if not specified
|
|
8889
8817
|
const effectiveMaxFileTokens = maxFileTokens ?? Math.floor(maxTokens * 0.25);
|
|
8890
|
-
// PHASE 1:
|
|
8818
|
+
// PHASE 1: Directory grouping & assessment
|
|
8819
|
+
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
8820
|
+
let directoryDiffs = createDirectoryDiffs(rootDiffNode);
|
|
8821
|
+
// Sort by token count descending for consistent output ordering
|
|
8822
|
+
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
8823
|
+
let totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
8824
|
+
logger.stopSpinner('Diffs Organized').stopTimer();
|
|
8825
|
+
logger.verbose(`Total token count: ${totalTokenCount}, max allowed: ${maxTokens}`, {
|
|
8826
|
+
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
8827
|
+
});
|
|
8828
|
+
// Early exit if already under budget
|
|
8829
|
+
if (totalTokenCount <= maxTokens) {
|
|
8830
|
+
logger.verbose(`Already under token budget, skipping summarization.`, { color: 'green' });
|
|
8831
|
+
return directoryDiffs.map(handleOutput).join('');
|
|
8832
|
+
}
|
|
8833
|
+
// PHASE 2: Pre-process large files only when the raw diff is over budget
|
|
8891
8834
|
logger.startTimer().startSpinner(`Pre-processing large files...`, { color: 'blue' });
|
|
8892
8835
|
const preprocessedNode = await preprocessLargeFiles(rootDiffNode, {
|
|
8893
8836
|
maxFileTokens: effectiveMaxFileTokens,
|
|
@@ -8897,21 +8840,17 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
|
|
|
8897
8840
|
logger,
|
|
8898
8841
|
chain,
|
|
8899
8842
|
textSplitter,
|
|
8843
|
+
metadata,
|
|
8900
8844
|
});
|
|
8901
8845
|
logger.stopSpinner('Files pre-processed').stopTimer();
|
|
8902
|
-
|
|
8903
|
-
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
8904
|
-
const directoryDiffs = createDirectoryDiffs(preprocessedNode);
|
|
8905
|
-
// Sort by token count descending for consistent output ordering
|
|
8846
|
+
directoryDiffs = createDirectoryDiffs(preprocessedNode);
|
|
8906
8847
|
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
8907
|
-
|
|
8908
|
-
logger.
|
|
8909
|
-
logger.verbose(`Total token count: ${totalTokenCount}, max allowed: ${maxTokens}`, {
|
|
8848
|
+
totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
8849
|
+
logger.verbose(`Total token count after file pre-processing: ${totalTokenCount}`, {
|
|
8910
8850
|
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
8911
8851
|
});
|
|
8912
|
-
// Early exit if already under budget
|
|
8913
8852
|
if (totalTokenCount <= maxTokens) {
|
|
8914
|
-
logger.verbose(`
|
|
8853
|
+
logger.verbose(`Under token budget after file pre-processing.`, { color: 'green' });
|
|
8915
8854
|
return directoryDiffs.map(handleOutput).join('');
|
|
8916
8855
|
}
|
|
8917
8856
|
// PHASE 3: Wave-based summarization
|
|
@@ -8925,17 +8864,41 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
|
|
|
8925
8864
|
chain,
|
|
8926
8865
|
textSplitter,
|
|
8927
8866
|
tokenizer,
|
|
8867
|
+
metadata,
|
|
8928
8868
|
});
|
|
8929
8869
|
logger.stopSpinner(`Diffs Consolidated`).stopTimer();
|
|
8930
8870
|
return summarizedDiffs.map(handleOutput).join('');
|
|
8931
8871
|
}
|
|
8932
8872
|
|
|
8873
|
+
function createLimit(maxConcurrent) {
|
|
8874
|
+
const limit = Math.max(1, maxConcurrent);
|
|
8875
|
+
let active = 0;
|
|
8876
|
+
const queue = [];
|
|
8877
|
+
const runNext = () => {
|
|
8878
|
+
active--;
|
|
8879
|
+
const next = queue.shift();
|
|
8880
|
+
if (next)
|
|
8881
|
+
next();
|
|
8882
|
+
};
|
|
8883
|
+
return async (operation) => {
|
|
8884
|
+
if (active >= limit) {
|
|
8885
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
8886
|
+
}
|
|
8887
|
+
active++;
|
|
8888
|
+
try {
|
|
8889
|
+
return await operation();
|
|
8890
|
+
}
|
|
8891
|
+
finally {
|
|
8892
|
+
runNext();
|
|
8893
|
+
}
|
|
8894
|
+
};
|
|
8895
|
+
}
|
|
8933
8896
|
/**
|
|
8934
8897
|
* Asynchronously collect diffs for a given node and its children.
|
|
8935
8898
|
*/
|
|
8936
|
-
async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
8899
|
+
async function collectDiffs(node, getFileDiff, tokenizer, logger, maxConcurrent = 6, limit = createLimit(maxConcurrent)) {
|
|
8937
8900
|
// Collect diffs for the files of the current node
|
|
8938
|
-
const diffPromises = node.files.map(async (
|
|
8901
|
+
const diffPromises = node.files.map((nodeFile) => limit(async () => {
|
|
8939
8902
|
const diff = await getFileDiff(nodeFile);
|
|
8940
8903
|
const tokenCount = tokenizer(diff);
|
|
8941
8904
|
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
@@ -8947,9 +8910,9 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
8947
8910
|
diff,
|
|
8948
8911
|
tokenCount,
|
|
8949
8912
|
};
|
|
8950
|
-
});
|
|
8913
|
+
}));
|
|
8951
8914
|
// Collect diffs for the children of the current node
|
|
8952
|
-
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger));
|
|
8915
|
+
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger, maxConcurrent, limit));
|
|
8953
8916
|
const [diffs, children] = await Promise.all([
|
|
8954
8917
|
Promise.all(diffPromises),
|
|
8955
8918
|
Promise.all(childrenPromises),
|
|
@@ -10745,7 +10708,7 @@ var RecursiveCharacterTextSplitter = class RecursiveCharacterTextSplitter extend
|
|
|
10745
10708
|
};
|
|
10746
10709
|
|
|
10747
10710
|
//#region src/chains/summarization/stuff_prompts.ts
|
|
10748
|
-
const template$
|
|
10711
|
+
const template$3 = `Write a concise summary of the following:
|
|
10749
10712
|
|
|
10750
10713
|
|
|
10751
10714
|
"{text}"
|
|
@@ -10753,7 +10716,7 @@ const template$2 = `Write a concise summary of the following:
|
|
|
10753
10716
|
|
|
10754
10717
|
CONCISE SUMMARY:`;
|
|
10755
10718
|
const DEFAULT_PROMPT = /* @__PURE__ */ new PromptTemplate({
|
|
10756
|
-
template: template$
|
|
10719
|
+
template: template$3,
|
|
10757
10720
|
inputVariables: ["text"]
|
|
10758
10721
|
});
|
|
10759
10722
|
|
|
@@ -10843,7 +10806,7 @@ function isObject(subject) {
|
|
|
10843
10806
|
}
|
|
10844
10807
|
|
|
10845
10808
|
|
|
10846
|
-
function toArray(sequence) {
|
|
10809
|
+
function toArray$1(sequence) {
|
|
10847
10810
|
if (Array.isArray(sequence)) return sequence;
|
|
10848
10811
|
else if (isNothing(sequence)) return [];
|
|
10849
10812
|
|
|
@@ -10885,7 +10848,7 @@ function isNegativeZero(number) {
|
|
|
10885
10848
|
|
|
10886
10849
|
var isNothing_1 = isNothing;
|
|
10887
10850
|
var isObject_1 = isObject;
|
|
10888
|
-
var toArray_1 = toArray;
|
|
10851
|
+
var toArray_1 = toArray$1;
|
|
10889
10852
|
var repeat_1 = repeat;
|
|
10890
10853
|
var isNegativeZero_1 = isNegativeZero;
|
|
10891
10854
|
var extend_1 = extend;
|
|
@@ -11808,87 +11771,753 @@ function constructYamlSet(data) {
|
|
|
11808
11771
|
return data !== null ? data : {};
|
|
11809
11772
|
}
|
|
11810
11773
|
|
|
11811
|
-
var set = new type('tag:yaml.org,2002:set', {
|
|
11812
|
-
kind: 'mapping',
|
|
11813
|
-
resolve: resolveYamlSet,
|
|
11814
|
-
construct: constructYamlSet
|
|
11815
|
-
});
|
|
11816
|
-
|
|
11817
|
-
core.extend({
|
|
11818
|
-
implicit: [
|
|
11819
|
-
timestamp,
|
|
11820
|
-
merge
|
|
11821
|
-
],
|
|
11822
|
-
explicit: [
|
|
11823
|
-
binary,
|
|
11824
|
-
omap,
|
|
11825
|
-
pairs,
|
|
11826
|
-
set
|
|
11827
|
-
]
|
|
11828
|
-
});
|
|
11829
|
-
|
|
11830
|
-
function simpleEscapeSequence(c) {
|
|
11831
|
-
/* eslint-disable indent */
|
|
11832
|
-
return (c === 0x30/* 0 */) ? '\x00' :
|
|
11833
|
-
(c === 0x61/* a */) ? '\x07' :
|
|
11834
|
-
(c === 0x62/* b */) ? '\x08' :
|
|
11835
|
-
(c === 0x74/* t */) ? '\x09' :
|
|
11836
|
-
(c === 0x09/* Tab */) ? '\x09' :
|
|
11837
|
-
(c === 0x6E/* n */) ? '\x0A' :
|
|
11838
|
-
(c === 0x76/* v */) ? '\x0B' :
|
|
11839
|
-
(c === 0x66/* f */) ? '\x0C' :
|
|
11840
|
-
(c === 0x72/* r */) ? '\x0D' :
|
|
11841
|
-
(c === 0x65/* e */) ? '\x1B' :
|
|
11842
|
-
(c === 0x20/* Space */) ? ' ' :
|
|
11843
|
-
(c === 0x22/* " */) ? '\x22' :
|
|
11844
|
-
(c === 0x2F/* / */) ? '/' :
|
|
11845
|
-
(c === 0x5C/* \ */) ? '\x5C' :
|
|
11846
|
-
(c === 0x4E/* N */) ? '\x85' :
|
|
11847
|
-
(c === 0x5F/* _ */) ? '\xA0' :
|
|
11848
|
-
(c === 0x4C/* L */) ? '\u2028' :
|
|
11849
|
-
(c === 0x50/* P */) ? '\u2029' : '';
|
|
11774
|
+
var set = new type('tag:yaml.org,2002:set', {
|
|
11775
|
+
kind: 'mapping',
|
|
11776
|
+
resolve: resolveYamlSet,
|
|
11777
|
+
construct: constructYamlSet
|
|
11778
|
+
});
|
|
11779
|
+
|
|
11780
|
+
core.extend({
|
|
11781
|
+
implicit: [
|
|
11782
|
+
timestamp,
|
|
11783
|
+
merge
|
|
11784
|
+
],
|
|
11785
|
+
explicit: [
|
|
11786
|
+
binary,
|
|
11787
|
+
omap,
|
|
11788
|
+
pairs,
|
|
11789
|
+
set
|
|
11790
|
+
]
|
|
11791
|
+
});
|
|
11792
|
+
|
|
11793
|
+
function simpleEscapeSequence(c) {
|
|
11794
|
+
/* eslint-disable indent */
|
|
11795
|
+
return (c === 0x30/* 0 */) ? '\x00' :
|
|
11796
|
+
(c === 0x61/* a */) ? '\x07' :
|
|
11797
|
+
(c === 0x62/* b */) ? '\x08' :
|
|
11798
|
+
(c === 0x74/* t */) ? '\x09' :
|
|
11799
|
+
(c === 0x09/* Tab */) ? '\x09' :
|
|
11800
|
+
(c === 0x6E/* n */) ? '\x0A' :
|
|
11801
|
+
(c === 0x76/* v */) ? '\x0B' :
|
|
11802
|
+
(c === 0x66/* f */) ? '\x0C' :
|
|
11803
|
+
(c === 0x72/* r */) ? '\x0D' :
|
|
11804
|
+
(c === 0x65/* e */) ? '\x1B' :
|
|
11805
|
+
(c === 0x20/* Space */) ? ' ' :
|
|
11806
|
+
(c === 0x22/* " */) ? '\x22' :
|
|
11807
|
+
(c === 0x2F/* / */) ? '/' :
|
|
11808
|
+
(c === 0x5C/* \ */) ? '\x5C' :
|
|
11809
|
+
(c === 0x4E/* N */) ? '\x85' :
|
|
11810
|
+
(c === 0x5F/* _ */) ? '\xA0' :
|
|
11811
|
+
(c === 0x4C/* L */) ? '\u2028' :
|
|
11812
|
+
(c === 0x50/* P */) ? '\u2029' : '';
|
|
11813
|
+
}
|
|
11814
|
+
|
|
11815
|
+
var simpleEscapeCheck = new Array(256); // integer, for fast access
|
|
11816
|
+
var simpleEscapeMap = new Array(256);
|
|
11817
|
+
for (var i = 0; i < 256; i++) {
|
|
11818
|
+
simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0;
|
|
11819
|
+
simpleEscapeMap[i] = simpleEscapeSequence(i);
|
|
11820
|
+
}
|
|
11821
|
+
|
|
11822
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, metadata, }, }) {
|
|
11823
|
+
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
11824
|
+
const summarizationChain = loadSummarizationChain(model, {
|
|
11825
|
+
type: 'map_reduce',
|
|
11826
|
+
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
11827
|
+
combinePrompt: SUMMARIZE_PROMPT,
|
|
11828
|
+
});
|
|
11829
|
+
logger.startTimer();
|
|
11830
|
+
const rootTreeNode = createDiffTree(changes);
|
|
11831
|
+
logger.stopTimer('Created file hierarchy');
|
|
11832
|
+
// Collect diffs
|
|
11833
|
+
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
11834
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger, maxConcurrent);
|
|
11835
|
+
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
11836
|
+
// Summarize diffs using three-phase approach:
|
|
11837
|
+
// 1. Pre-process large files to prevent bias
|
|
11838
|
+
// 2. Group by directory and assess token count
|
|
11839
|
+
// 3. Wave-based parallel summarization until under budget
|
|
11840
|
+
logger.startTimer();
|
|
11841
|
+
const summary = await summarizeDiffs(diffs, {
|
|
11842
|
+
tokenizer,
|
|
11843
|
+
maxTokens: maxTokens || 2048,
|
|
11844
|
+
minTokensForSummary,
|
|
11845
|
+
maxFileTokens,
|
|
11846
|
+
maxConcurrent,
|
|
11847
|
+
textSplitter,
|
|
11848
|
+
chain: summarizationChain,
|
|
11849
|
+
logger,
|
|
11850
|
+
metadata,
|
|
11851
|
+
});
|
|
11852
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
11853
|
+
return summary;
|
|
11854
|
+
}
|
|
11855
|
+
|
|
11856
|
+
/**
|
|
11857
|
+
* Retrieves a TikToken for the specified model.
|
|
11858
|
+
*
|
|
11859
|
+
* @param {TiktokenModel} modelName - The name of the TiktokenModel.
|
|
11860
|
+
* @returns A Promise that resolves to the TikToken.
|
|
11861
|
+
*/
|
|
11862
|
+
const getTikToken = async (modelName) => {
|
|
11863
|
+
return await encoding_for_model(modelName);
|
|
11864
|
+
};
|
|
11865
|
+
/**
|
|
11866
|
+
* Retrieves the token counter for a given model name.
|
|
11867
|
+
*
|
|
11868
|
+
* @param {TikTokenModel} modelName - The name of the Tiktoken model.
|
|
11869
|
+
* @returns A promise that resolves to a function that calculates the number of tokens in a given text.
|
|
11870
|
+
*/
|
|
11871
|
+
const getTokenCounter = async (modelName) => {
|
|
11872
|
+
return getTikToken(modelName).then((tokenizer) => (text) => {
|
|
11873
|
+
const tokens = tokenizer.encode(text);
|
|
11874
|
+
return tokens.length;
|
|
11875
|
+
});
|
|
11876
|
+
};
|
|
11877
|
+
|
|
11878
|
+
const template$2 = `You are a highly skilled software engineer tasked with writing a git changelog. Your response should be informative, well-structured, and in the imperative.
|
|
11879
|
+
|
|
11880
|
+
## Input
|
|
11881
|
+
You will be provided with a summary of changes. This summary can be one of the following:
|
|
11882
|
+
1. A list of commits, each with its author, hash, message, and body.
|
|
11883
|
+
2. A list of commits, each with its details AND the full diff of the changes.
|
|
11884
|
+
3. A single, comprehensive diff for an entire branch.
|
|
11885
|
+
|
|
11886
|
+
## Rules
|
|
11887
|
+
- Create a descriptive title for the changelog that gives a high-level overview of the changes.
|
|
11888
|
+
- **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
|
|
11889
|
+
- **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
|
|
11890
|
+
- **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
|
|
11891
|
+
- **Summaries**: For each change, provide a concise summary.
|
|
11892
|
+
- **Attribution**: {{author_instructions}}
|
|
11893
|
+
- **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
|
|
11894
|
+
- **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
|
|
11895
|
+
- **Formatting**: Your entire response must be valid Markdown.
|
|
11896
|
+
|
|
11897
|
+
## Formatting Instructions
|
|
11898
|
+
{{format_instructions}}
|
|
11899
|
+
|
|
11900
|
+
{{additional_context}}
|
|
11901
|
+
|
|
11902
|
+
"""{{summary}}"""`;
|
|
11903
|
+
const inputVariables$2 = [
|
|
11904
|
+
'format_instructions',
|
|
11905
|
+
'summary',
|
|
11906
|
+
'additional_context',
|
|
11907
|
+
'author_instructions',
|
|
11908
|
+
];
|
|
11909
|
+
const CHANGELOG_PROMPT = new PromptTemplate({
|
|
11910
|
+
template: template$2,
|
|
11911
|
+
inputVariables: inputVariables$2,
|
|
11912
|
+
});
|
|
11913
|
+
|
|
11914
|
+
async function processInWaves(items, processor, maxConcurrent = 6) {
|
|
11915
|
+
const results = [];
|
|
11916
|
+
const limit = Math.max(1, maxConcurrent);
|
|
11917
|
+
for (let i = 0; i < items.length; i += limit) {
|
|
11918
|
+
const waveResults = await Promise.all(items.slice(i, i + limit).map(processor));
|
|
11919
|
+
results.push(...waveResults);
|
|
11920
|
+
}
|
|
11921
|
+
return results;
|
|
11922
|
+
}
|
|
11923
|
+
const handler$5 = async (argv, logger) => {
|
|
11924
|
+
const config = loadConfig(argv);
|
|
11925
|
+
const git = getRepo();
|
|
11926
|
+
const key = getApiKeyForModel(config);
|
|
11927
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
11928
|
+
const changelogService = resolveDynamicService(config, 'changelog');
|
|
11929
|
+
const summaryService = resolveDynamicService(config, argv.withDiff || argv.onlyDiff ? 'largeDiff' : 'summarize');
|
|
11930
|
+
const model = changelogService.model;
|
|
11931
|
+
const exclusiveOptions = [
|
|
11932
|
+
argv.branch ? '--branch' : null,
|
|
11933
|
+
argv.tag ? '--tag' : null,
|
|
11934
|
+
config.sinceLastTag ? '--since-last-tag' : null,
|
|
11935
|
+
].filter(Boolean);
|
|
11936
|
+
if (exclusiveOptions.length > 1) {
|
|
11937
|
+
logger.log(`Options ${exclusiveOptions.join(', ')} cannot be used together.`, { color: 'red' });
|
|
11938
|
+
process.exit(1);
|
|
11939
|
+
}
|
|
11940
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
11941
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
11942
|
+
process.exit(1);
|
|
11943
|
+
}
|
|
11944
|
+
const llm = getLlm(provider, model, { ...config, service: changelogService });
|
|
11945
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
11946
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
11947
|
+
const INTERACTIVE = isInteractive(config);
|
|
11948
|
+
if (INTERACTIVE) {
|
|
11949
|
+
if (!config.hideCocoBanner) {
|
|
11950
|
+
logger.log(LOGO);
|
|
11951
|
+
}
|
|
11952
|
+
}
|
|
11953
|
+
async function factory() {
|
|
11954
|
+
const branchName = await getCurrentBranchName({ git });
|
|
11955
|
+
if (argv.onlyDiff) {
|
|
11956
|
+
const baseBranch = argv.branch || config.defaultBranch || 'main';
|
|
11957
|
+
logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
|
|
11958
|
+
const diff = await getDiffForBranch({ git, logger, baseBranch, headBranch: branchName });
|
|
11959
|
+
return {
|
|
11960
|
+
branch: branchName,
|
|
11961
|
+
diffChanges: diff.staged,
|
|
11962
|
+
diffCommit: `${baseBranch}..${branchName}`,
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11965
|
+
let commits = [];
|
|
11966
|
+
if (config.sinceLastTag) {
|
|
11967
|
+
logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
|
|
11968
|
+
// This function returns string[], needs to be adapted or replaced
|
|
11969
|
+
// For now, this path will have limited details.
|
|
11970
|
+
const commitMessages = await getChangesSinceLastTag({ git});
|
|
11971
|
+
commits = commitMessages.map(msg => ({ message: msg }));
|
|
11972
|
+
}
|
|
11973
|
+
else if (config.range && config.range.includes(':')) {
|
|
11974
|
+
const [from, to] = config.range.split(':');
|
|
11975
|
+
if (!from || !to) {
|
|
11976
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
11977
|
+
process.exit(1);
|
|
11978
|
+
}
|
|
11979
|
+
commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
|
|
11980
|
+
}
|
|
11981
|
+
else if (argv.branch) {
|
|
11982
|
+
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
11983
|
+
commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
|
|
11984
|
+
}
|
|
11985
|
+
else if (argv.tag) {
|
|
11986
|
+
logger.verbose(`Generating commit log against tag: ${argv.tag}`, { color: 'yellow' });
|
|
11987
|
+
commits = await getCommitLogAgainstTag({ git, logger, targetTag: argv.tag });
|
|
11988
|
+
}
|
|
11989
|
+
else {
|
|
11990
|
+
logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
|
|
11991
|
+
color: 'yellow',
|
|
11992
|
+
});
|
|
11993
|
+
commits = await getCommitLogCurrentBranch({ git, logger });
|
|
11994
|
+
}
|
|
11995
|
+
let commitsWithDiffText = commits;
|
|
11996
|
+
if (argv.withDiff) {
|
|
11997
|
+
commitsWithDiffText = await processInWaves(commits, async (commit) => {
|
|
11998
|
+
const changes = await getChangesByCommit({
|
|
11999
|
+
commit: commit.hash,
|
|
12000
|
+
options: {
|
|
12001
|
+
git,
|
|
12002
|
+
ignoredFiles: config.ignoredFiles || undefined,
|
|
12003
|
+
ignoredExtensions: config.ignoredExtensions || undefined,
|
|
12004
|
+
},
|
|
12005
|
+
});
|
|
12006
|
+
return {
|
|
12007
|
+
...commit,
|
|
12008
|
+
diffText: changes.length > 0
|
|
12009
|
+
? await fileChangeParser({
|
|
12010
|
+
changes,
|
|
12011
|
+
commit: `${commit.hash}^..${commit.hash}`,
|
|
12012
|
+
options: {
|
|
12013
|
+
tokenizer,
|
|
12014
|
+
git,
|
|
12015
|
+
llm: summaryLlm,
|
|
12016
|
+
logger,
|
|
12017
|
+
maxTokens: config.service.tokenLimit,
|
|
12018
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
12019
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
12020
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
12021
|
+
metadata: {
|
|
12022
|
+
command: 'changelog',
|
|
12023
|
+
provider,
|
|
12024
|
+
model: String(summaryService.model),
|
|
12025
|
+
},
|
|
12026
|
+
},
|
|
12027
|
+
})
|
|
12028
|
+
: undefined,
|
|
12029
|
+
};
|
|
12030
|
+
}, config.service.maxConcurrent);
|
|
12031
|
+
}
|
|
12032
|
+
return {
|
|
12033
|
+
branch: branchName,
|
|
12034
|
+
commits: commitsWithDiffText,
|
|
12035
|
+
withDiff: argv.withDiff,
|
|
12036
|
+
};
|
|
12037
|
+
}
|
|
12038
|
+
async function parser(data) {
|
|
12039
|
+
if (data.diffChanges && data.diffCommit) {
|
|
12040
|
+
const diffSummary = await fileChangeParser({
|
|
12041
|
+
changes: data.diffChanges,
|
|
12042
|
+
commit: data.diffCommit,
|
|
12043
|
+
options: {
|
|
12044
|
+
tokenizer,
|
|
12045
|
+
git,
|
|
12046
|
+
llm: summaryLlm,
|
|
12047
|
+
logger,
|
|
12048
|
+
maxTokens: config.service.tokenLimit,
|
|
12049
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
12050
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
12051
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
12052
|
+
metadata: {
|
|
12053
|
+
command: 'changelog',
|
|
12054
|
+
provider,
|
|
12055
|
+
model: String(summaryService.model),
|
|
12056
|
+
},
|
|
12057
|
+
},
|
|
12058
|
+
});
|
|
12059
|
+
return `## Diff for ${data.branch}\n\n${diffSummary}`;
|
|
12060
|
+
}
|
|
12061
|
+
if (!data.commits || data.commits.length === 0) {
|
|
12062
|
+
return `## ${data.branch}\n\nNo commits found.`;
|
|
12063
|
+
}
|
|
12064
|
+
let result = `## ${data.branch}\n\n`;
|
|
12065
|
+
result += data.commits.map(commit => {
|
|
12066
|
+
let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
|
|
12067
|
+
if (data.withDiff && commit.diffText) {
|
|
12068
|
+
commitStr += `\nDiff:\n${commit.diffText}`;
|
|
12069
|
+
}
|
|
12070
|
+
return commitStr.trim();
|
|
12071
|
+
}).join('\n\n---\n\n');
|
|
12072
|
+
return result;
|
|
12073
|
+
}
|
|
12074
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
12075
|
+
label: 'changelog',
|
|
12076
|
+
options: {
|
|
12077
|
+
...config,
|
|
12078
|
+
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
12079
|
+
logger,
|
|
12080
|
+
interactive: INTERACTIVE,
|
|
12081
|
+
review: {
|
|
12082
|
+
enableFullRetry: false,
|
|
12083
|
+
},
|
|
12084
|
+
},
|
|
12085
|
+
factory,
|
|
12086
|
+
parser,
|
|
12087
|
+
agent: async (context, options) => {
|
|
12088
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12089
|
+
const parser = createSchemaParser(ChangelogResponseSchema, llm);
|
|
12090
|
+
const prompt = getPrompt({
|
|
12091
|
+
template: options.prompt,
|
|
12092
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
12093
|
+
fallback: CHANGELOG_PROMPT,
|
|
12094
|
+
});
|
|
12095
|
+
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.";
|
|
12096
|
+
let additional_context = '';
|
|
12097
|
+
if (argv.additional) {
|
|
12098
|
+
additional_context = `## Additional Context\n${argv.additional}`;
|
|
12099
|
+
}
|
|
12100
|
+
const author_instructions = argv.author
|
|
12101
|
+
? 'At the end of each item, attribute the author and include a reference to the commit hash, like this: `by @author_name (f6dbe61)`. Use the first 7 characters of the hash.'
|
|
12102
|
+
: 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
|
|
12103
|
+
const variables = {
|
|
12104
|
+
summary: context,
|
|
12105
|
+
format_instructions: formatInstructions,
|
|
12106
|
+
additional_context: additional_context,
|
|
12107
|
+
author_instructions: author_instructions,
|
|
12108
|
+
};
|
|
12109
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
12110
|
+
prompt,
|
|
12111
|
+
variables,
|
|
12112
|
+
tokenizer,
|
|
12113
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
12114
|
+
});
|
|
12115
|
+
if (budgetedPrompt.truncated) {
|
|
12116
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
12117
|
+
}
|
|
12118
|
+
const changelog = await executeChain({
|
|
12119
|
+
llm,
|
|
12120
|
+
prompt,
|
|
12121
|
+
variables: budgetedPrompt.variables,
|
|
12122
|
+
parser,
|
|
12123
|
+
logger,
|
|
12124
|
+
tokenizer,
|
|
12125
|
+
metadata: {
|
|
12126
|
+
task: argv.withDiff ? 'changelog-with-diff' : argv.onlyDiff ? 'changelog-only-diff' : 'changelog',
|
|
12127
|
+
command: 'changelog',
|
|
12128
|
+
provider,
|
|
12129
|
+
model: String(model),
|
|
12130
|
+
},
|
|
12131
|
+
});
|
|
12132
|
+
const branchName = await getCurrentBranchName({ git });
|
|
12133
|
+
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
12134
|
+
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
12135
|
+
return `${changelog.title}\n\n${changelog.content}${footer}`;
|
|
12136
|
+
},
|
|
12137
|
+
noResult: async () => {
|
|
12138
|
+
if (config.range) {
|
|
12139
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
12140
|
+
process.exit(0);
|
|
12141
|
+
}
|
|
12142
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
12143
|
+
process.exit(0);
|
|
12144
|
+
},
|
|
12145
|
+
});
|
|
12146
|
+
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
12147
|
+
handleResult({
|
|
12148
|
+
result: changelogMsg,
|
|
12149
|
+
interactiveModeCallback: async () => {
|
|
12150
|
+
logSuccess();
|
|
12151
|
+
},
|
|
12152
|
+
mode: MODE,
|
|
12153
|
+
});
|
|
12154
|
+
logLlmTelemetrySummary(logger, 'changelog');
|
|
12155
|
+
};
|
|
12156
|
+
|
|
12157
|
+
var changelog = {
|
|
12158
|
+
command: command$5,
|
|
12159
|
+
desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
|
|
12160
|
+
builder: builder$5,
|
|
12161
|
+
handler: commandExecutor(handler$5),
|
|
12162
|
+
options: options$5,
|
|
12163
|
+
};
|
|
12164
|
+
|
|
12165
|
+
const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
|
|
12166
|
+
// Regular commit message schema with basic validation
|
|
12167
|
+
const CommitMessageResponseSchema = objectType({
|
|
12168
|
+
title: stringType().describe("Title of the commit message"),
|
|
12169
|
+
body: stringType().describe("Body of the commit message"),
|
|
12170
|
+
}).describe("Object with commit message 'title' and 'body'");
|
|
12171
|
+
// Conventional commit message schema with strict formatting rules
|
|
12172
|
+
const ConventionalCommitMessageResponseSchema = objectType({
|
|
12173
|
+
title: stringType()
|
|
12174
|
+
.max(50, "Title must be 50 characters or less")
|
|
12175
|
+
.refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')").describe("Title of the commit message"),
|
|
12176
|
+
body: stringType().describe("Body of the commit message")
|
|
12177
|
+
// .max(280, "Body must be 280 characters or less"),
|
|
12178
|
+
}).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
|
|
12179
|
+
const command$4 = 'commit';
|
|
12180
|
+
/**
|
|
12181
|
+
* Command line options via yargs
|
|
12182
|
+
*/
|
|
12183
|
+
const options$4 = {
|
|
12184
|
+
i: {
|
|
12185
|
+
alias: 'interactive',
|
|
12186
|
+
description: 'Toggle interactive mode',
|
|
12187
|
+
type: 'boolean',
|
|
12188
|
+
},
|
|
12189
|
+
ignoredFiles: {
|
|
12190
|
+
description: 'Ignored files',
|
|
12191
|
+
type: 'array',
|
|
12192
|
+
},
|
|
12193
|
+
ignoredExtensions: {
|
|
12194
|
+
description: 'Ignored extensions',
|
|
12195
|
+
type: 'array',
|
|
12196
|
+
},
|
|
12197
|
+
append: {
|
|
12198
|
+
description: 'Add content to the end of the generated commit message',
|
|
12199
|
+
type: 'string',
|
|
12200
|
+
},
|
|
12201
|
+
appendTicket: {
|
|
12202
|
+
description: 'Append ticket ID from branch name to the commit message',
|
|
12203
|
+
type: 'boolean',
|
|
12204
|
+
alias: 't',
|
|
12205
|
+
},
|
|
12206
|
+
additional: {
|
|
12207
|
+
description: 'Add extra contextual information to the prompt',
|
|
12208
|
+
type: 'string',
|
|
12209
|
+
alias: 'a',
|
|
12210
|
+
},
|
|
12211
|
+
withPreviousCommits: {
|
|
12212
|
+
description: 'Include previous commits as context (specify number of commits, 0 for none)',
|
|
12213
|
+
type: 'number',
|
|
12214
|
+
default: 0,
|
|
12215
|
+
alias: 'p',
|
|
12216
|
+
},
|
|
12217
|
+
conventional: {
|
|
12218
|
+
description: 'Generate commit message in Conventional Commits format',
|
|
12219
|
+
type: 'boolean',
|
|
12220
|
+
default: false,
|
|
12221
|
+
alias: 'c',
|
|
12222
|
+
},
|
|
12223
|
+
includeBranchName: {
|
|
12224
|
+
description: 'Include the current branch name in the commit prompt for context',
|
|
12225
|
+
type: 'boolean',
|
|
12226
|
+
default: true,
|
|
12227
|
+
},
|
|
12228
|
+
noDiff: {
|
|
12229
|
+
description: 'Only pass basic "git status" result instead of providing entire diff',
|
|
12230
|
+
type: 'boolean',
|
|
12231
|
+
default: false,
|
|
12232
|
+
},
|
|
12233
|
+
noVerify: {
|
|
12234
|
+
description: 'Skip pre-commit and commit-msg hooks (passes --no-verify to git commit)',
|
|
12235
|
+
type: 'boolean',
|
|
12236
|
+
default: false,
|
|
12237
|
+
alias: 'n',
|
|
12238
|
+
},
|
|
12239
|
+
split: {
|
|
12240
|
+
description: 'Group staged changes into multiple related commit proposals',
|
|
12241
|
+
type: 'boolean',
|
|
12242
|
+
default: false,
|
|
12243
|
+
},
|
|
12244
|
+
plan: {
|
|
12245
|
+
description: 'Only print a commit split plan without changing git state',
|
|
12246
|
+
type: 'boolean',
|
|
12247
|
+
default: false,
|
|
12248
|
+
},
|
|
12249
|
+
apply: {
|
|
12250
|
+
description: 'Apply a generated file-level or hunk-level commit split plan and create commits',
|
|
12251
|
+
type: 'boolean',
|
|
12252
|
+
default: false,
|
|
12253
|
+
},
|
|
12254
|
+
};
|
|
12255
|
+
const builder$4 = (yargs) => {
|
|
12256
|
+
return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
|
|
12257
|
+
};
|
|
12258
|
+
|
|
12259
|
+
/**
|
|
12260
|
+
* High-level function that combines chain execution with schema-based parsing
|
|
12261
|
+
* Includes automatic retry logic and graceful degradation
|
|
12262
|
+
* @param schema - Zod schema for the expected output structure
|
|
12263
|
+
* @param llm - LLM instance
|
|
12264
|
+
* @param prompt - Prompt template
|
|
12265
|
+
* @param variables - Variables for the prompt
|
|
12266
|
+
* @param options - Configuration options
|
|
12267
|
+
* @returns Parsed result matching the schema type
|
|
12268
|
+
*/
|
|
12269
|
+
async function executeChainWithSchema(schema, llm, prompt, variables, options = {}) {
|
|
12270
|
+
const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, logger, tokenizer, metadata, ...parserOptions } = options;
|
|
12271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12272
|
+
const parser = createSchemaParser(schema, llm, parserOptions);
|
|
12273
|
+
let attempt = 0;
|
|
12274
|
+
const operation = async () => {
|
|
12275
|
+
attempt++;
|
|
12276
|
+
const result = await executeChain({
|
|
12277
|
+
llm,
|
|
12278
|
+
prompt,
|
|
12279
|
+
variables,
|
|
12280
|
+
parser,
|
|
12281
|
+
logger,
|
|
12282
|
+
tokenizer,
|
|
12283
|
+
metadata: {
|
|
12284
|
+
task: 'schema-chain',
|
|
12285
|
+
...metadata,
|
|
12286
|
+
retryAttempt: attempt,
|
|
12287
|
+
},
|
|
12288
|
+
});
|
|
12289
|
+
return result;
|
|
12290
|
+
};
|
|
12291
|
+
try {
|
|
12292
|
+
return await withRetry(operation, retryOptions);
|
|
12293
|
+
}
|
|
12294
|
+
catch (error) {
|
|
12295
|
+
if (fallbackParser) {
|
|
12296
|
+
if (onFallback) {
|
|
12297
|
+
onFallback();
|
|
12298
|
+
}
|
|
12299
|
+
const fallbackResult = await executeChain({
|
|
12300
|
+
llm,
|
|
12301
|
+
prompt,
|
|
12302
|
+
variables,
|
|
12303
|
+
parser: new StringOutputParser(),
|
|
12304
|
+
logger,
|
|
12305
|
+
tokenizer,
|
|
12306
|
+
metadata: {
|
|
12307
|
+
task: 'schema-chain-fallback',
|
|
12308
|
+
...metadata,
|
|
12309
|
+
},
|
|
12310
|
+
});
|
|
12311
|
+
const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
|
|
12312
|
+
return fallbackParser(fallbackText);
|
|
12313
|
+
}
|
|
12314
|
+
// No fallback available, re-throw the error
|
|
12315
|
+
throw error;
|
|
12316
|
+
}
|
|
12317
|
+
}
|
|
12318
|
+
|
|
12319
|
+
/**
|
|
12320
|
+
* Utility to repair common JSON formatting issues that LLMs make
|
|
12321
|
+
* Specifically handles cases where string values are not properly quoted
|
|
12322
|
+
*/
|
|
12323
|
+
function repairJson(jsonString) {
|
|
12324
|
+
// Remove any markdown code block wrapping
|
|
12325
|
+
let cleaned = jsonString.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, '$1').trim();
|
|
12326
|
+
// Remove inline code block wrapping
|
|
12327
|
+
cleaned = cleaned.replace(/^`(.*)`$/, '$1').trim();
|
|
12328
|
+
// If it doesn't look like JSON, return as-is
|
|
12329
|
+
if (!cleaned.startsWith('{') || !cleaned.endsWith('}')) {
|
|
12330
|
+
return jsonString;
|
|
12331
|
+
}
|
|
12332
|
+
try {
|
|
12333
|
+
// First try parsing as-is
|
|
12334
|
+
JSON.parse(cleaned);
|
|
12335
|
+
return cleaned;
|
|
12336
|
+
}
|
|
12337
|
+
catch {
|
|
12338
|
+
// Try to repair common issues
|
|
12339
|
+
let repaired = cleaned;
|
|
12340
|
+
// Fix unquoted string values in title and body fields
|
|
12341
|
+
// Pattern: "title": unquoted_value, -> "title": "unquoted_value",
|
|
12342
|
+
repaired = repaired.replace(/"(title|body)":\s*([^",\{\}\[\]]+?)(?=\s*[,\}])/g, (match, field, value) => {
|
|
12343
|
+
// Clean up the value (remove leading/trailing whitespace)
|
|
12344
|
+
const cleanValue = value.trim();
|
|
12345
|
+
// If it's already quoted or looks like a number/boolean, leave it
|
|
12346
|
+
if (cleanValue.startsWith('"') || /^(true|false|\d+)$/.test(cleanValue)) {
|
|
12347
|
+
return match;
|
|
12348
|
+
}
|
|
12349
|
+
// Quote the value
|
|
12350
|
+
return `"${field}": "${cleanValue}"`;
|
|
12351
|
+
});
|
|
12352
|
+
// Fix missing quotes around field names (though this should be rare)
|
|
12353
|
+
repaired = repaired.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
12354
|
+
// Remove trailing commas before closing braces
|
|
12355
|
+
repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
|
|
12356
|
+
try {
|
|
12357
|
+
// Test if the repair worked
|
|
12358
|
+
JSON.parse(repaired);
|
|
12359
|
+
return repaired;
|
|
12360
|
+
}
|
|
12361
|
+
catch {
|
|
12362
|
+
// If repair failed, return original
|
|
12363
|
+
return jsonString;
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
|
|
12368
|
+
/**
|
|
12369
|
+
* Extract the first complete JSON object from a string by tracking balanced braces
|
|
12370
|
+
*/
|
|
12371
|
+
function extractFirstJsonObject(text) {
|
|
12372
|
+
const startIndex = text.indexOf('{');
|
|
12373
|
+
if (startIndex === -1)
|
|
12374
|
+
return null;
|
|
12375
|
+
let braceCount = 0;
|
|
12376
|
+
let inString = false;
|
|
12377
|
+
let escapeNext = false;
|
|
12378
|
+
for (let i = startIndex; i < text.length; i++) {
|
|
12379
|
+
const char = text[i];
|
|
12380
|
+
if (escapeNext) {
|
|
12381
|
+
escapeNext = false;
|
|
12382
|
+
continue;
|
|
12383
|
+
}
|
|
12384
|
+
if (char === '\\') {
|
|
12385
|
+
escapeNext = true;
|
|
12386
|
+
continue;
|
|
12387
|
+
}
|
|
12388
|
+
if (char === '"') {
|
|
12389
|
+
inString = !inString;
|
|
12390
|
+
continue;
|
|
12391
|
+
}
|
|
12392
|
+
if (inString)
|
|
12393
|
+
continue;
|
|
12394
|
+
if (char === '{') {
|
|
12395
|
+
braceCount++;
|
|
12396
|
+
}
|
|
12397
|
+
else if (char === '}') {
|
|
12398
|
+
braceCount--;
|
|
12399
|
+
if (braceCount === 0) {
|
|
12400
|
+
// Found the end of the first complete JSON object
|
|
12401
|
+
return text.substring(startIndex, i + 1);
|
|
12402
|
+
}
|
|
12403
|
+
}
|
|
12404
|
+
}
|
|
12405
|
+
return null;
|
|
11850
12406
|
}
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
12407
|
+
/**
|
|
12408
|
+
* Utility function to ensure commit messages are properly formatted as strings
|
|
12409
|
+
* rather than JSON objects, whether they come as parsed objects or stringified JSON
|
|
12410
|
+
*/
|
|
12411
|
+
function formatCommitMessage(result, options = {}) {
|
|
12412
|
+
const { append, ticketId, appendTicket } = options;
|
|
12413
|
+
// Helper function to construct the final message with appends
|
|
12414
|
+
const constructMessage = (title, body) => {
|
|
12415
|
+
const appendedText = append ? `\n\n${append}` : '';
|
|
12416
|
+
const ticketFooter = appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
12417
|
+
return `${title}\n\n${body}${appendedText}${ticketFooter}`;
|
|
12418
|
+
};
|
|
12419
|
+
// If it's a string, check if it contains a JSON object (including markdown code blocks)
|
|
12420
|
+
if (typeof result === 'string') {
|
|
12421
|
+
// Early return if string clearly doesn't contain JSON-like content
|
|
12422
|
+
if (!result.includes('{') && !result.includes('"title"')) {
|
|
12423
|
+
return result;
|
|
12424
|
+
}
|
|
12425
|
+
// Handle multiple markdown code block formats and embedded JSON
|
|
12426
|
+
const extractionPatterns = [
|
|
12427
|
+
/```(?:json)?\s*(\{[\s\S]*?\})\s*```/, // Standard markdown blocks
|
|
12428
|
+
/`(\{[\s\S]*?\})`/, // Inline code blocks
|
|
12429
|
+
/^\s*(\{[\s\S]*\})\s*$/, // Raw JSON without blocks (entire string)
|
|
12430
|
+
/(\{[\s\S]*?\})/ // JSON anywhere in text (fallback)
|
|
12431
|
+
];
|
|
12432
|
+
let jsonString = result;
|
|
12433
|
+
let foundMatch = false;
|
|
12434
|
+
// Try each pattern to extract JSON
|
|
12435
|
+
for (const pattern of extractionPatterns) {
|
|
12436
|
+
const match = result.match(pattern);
|
|
12437
|
+
if (match && match[1]) {
|
|
12438
|
+
jsonString = match[1].trim();
|
|
12439
|
+
foundMatch = true;
|
|
12440
|
+
break;
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12443
|
+
// Only attempt JSON parsing if we found potential JSON content
|
|
12444
|
+
if (foundMatch || jsonString.startsWith('{')) {
|
|
12445
|
+
try {
|
|
12446
|
+
// Try to parse as JSON to see if it's a stringified object
|
|
12447
|
+
const parsed = JSON.parse(jsonString);
|
|
12448
|
+
if (parsed &&
|
|
12449
|
+
typeof parsed === 'object' &&
|
|
12450
|
+
typeof parsed.title === 'string' &&
|
|
12451
|
+
typeof parsed.body === 'string' &&
|
|
12452
|
+
parsed.title.length > 0 &&
|
|
12453
|
+
parsed.body.length > 0) {
|
|
12454
|
+
// It's a valid stringified JSON object, format it properly
|
|
12455
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
catch {
|
|
12459
|
+
// Try to repair the JSON and parse again
|
|
12460
|
+
try {
|
|
12461
|
+
const repairedJson = repairJson(jsonString);
|
|
12462
|
+
const parsed = JSON.parse(repairedJson);
|
|
12463
|
+
if (parsed &&
|
|
12464
|
+
typeof parsed === 'object' &&
|
|
12465
|
+
typeof parsed.title === 'string' &&
|
|
12466
|
+
typeof parsed.body === 'string' &&
|
|
12467
|
+
parsed.title.length > 0 &&
|
|
12468
|
+
parsed.body.length > 0) {
|
|
12469
|
+
// Successfully repaired and parsed JSON
|
|
12470
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12471
|
+
}
|
|
12472
|
+
}
|
|
12473
|
+
catch {
|
|
12474
|
+
// Repair failed, try extracting just the first complete JSON object
|
|
12475
|
+
const firstObject = extractFirstJsonObject(jsonString);
|
|
12476
|
+
if (firstObject) {
|
|
12477
|
+
try {
|
|
12478
|
+
const parsed = JSON.parse(firstObject);
|
|
12479
|
+
if (parsed &&
|
|
12480
|
+
typeof parsed === 'object' &&
|
|
12481
|
+
typeof parsed.title === 'string' &&
|
|
12482
|
+
typeof parsed.body === 'string' &&
|
|
12483
|
+
parsed.title.length > 0 &&
|
|
12484
|
+
parsed.body.length > 0) {
|
|
12485
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12486
|
+
}
|
|
12487
|
+
}
|
|
12488
|
+
catch {
|
|
12489
|
+
// Even first object extraction failed, continue to fallback
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
}
|
|
12494
|
+
}
|
|
12495
|
+
// If no JSON found and it's already formatted, return as-is
|
|
12496
|
+
return result;
|
|
12497
|
+
}
|
|
12498
|
+
// If it's already an object with title and body, format it
|
|
12499
|
+
if (typeof result === 'object' && result !== null &&
|
|
12500
|
+
'title' in result && 'body' in result) {
|
|
12501
|
+
const commitMsgObj = result;
|
|
12502
|
+
if (typeof commitMsgObj.title === 'string' && typeof commitMsgObj.body === 'string') {
|
|
12503
|
+
return constructMessage(commitMsgObj.title, commitMsgObj.body);
|
|
12504
|
+
}
|
|
12505
|
+
}
|
|
12506
|
+
// Fallback - convert to string and return as-is
|
|
12507
|
+
return String(result);
|
|
11857
12508
|
}
|
|
11858
12509
|
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
// Collect diffs
|
|
11870
|
-
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
11871
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
11872
|
-
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
11873
|
-
// Summarize diffs using three-phase approach:
|
|
11874
|
-
// 1. Pre-process large files to prevent bias
|
|
11875
|
-
// 2. Group by directory and assess token count
|
|
11876
|
-
// 3. Wave-based parallel summarization until under budget
|
|
11877
|
-
logger.startTimer();
|
|
11878
|
-
const summary = await summarizeDiffs(diffs, {
|
|
11879
|
-
tokenizer,
|
|
11880
|
-
maxTokens: maxTokens || 2048,
|
|
11881
|
-
minTokensForSummary,
|
|
11882
|
-
maxFileTokens,
|
|
11883
|
-
maxConcurrent,
|
|
11884
|
-
textSplitter,
|
|
11885
|
-
chain: summarizationChain,
|
|
11886
|
-
logger,
|
|
11887
|
-
});
|
|
11888
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
11889
|
-
return summary;
|
|
12510
|
+
/**
|
|
12511
|
+
* Error thrown when a pre-commit hook blocks a commit (e.g. a linter exits non-zero).
|
|
12512
|
+
* Contains the raw hook output so callers can present it cleanly to the user.
|
|
12513
|
+
*/
|
|
12514
|
+
class PreCommitHookError extends Error {
|
|
12515
|
+
constructor(hookOutput) {
|
|
12516
|
+
super('Pre-commit hook failed');
|
|
12517
|
+
this.name = 'PreCommitHookError';
|
|
12518
|
+
this.hookOutput = hookOutput;
|
|
12519
|
+
}
|
|
11890
12520
|
}
|
|
11891
|
-
|
|
11892
12521
|
/**
|
|
11893
12522
|
* Detects whether a GitError was caused by a pre-commit hook that modified files.
|
|
11894
12523
|
* These hooks (e.g. black, prettier) reformat files and exit non-zero on the first run,
|
|
@@ -11908,15 +12537,19 @@ function isPreCommitHookModifiedFilesError(error) {
|
|
|
11908
12537
|
* Creates a commit with the specified commit message.
|
|
11909
12538
|
* Handles the case where pre-commit hooks modify files (e.g. black, prettier):
|
|
11910
12539
|
* when detected, stages the hook-modified files and retries the commit once.
|
|
12540
|
+
* Any other GitError (e.g. hook lint failure) is wrapped in a PreCommitHookError
|
|
12541
|
+
* so callers can present it cleanly instead of showing a raw stack trace.
|
|
11911
12542
|
*
|
|
11912
12543
|
* @param message The commit message.
|
|
11913
12544
|
* @param git The SimpleGit instance.
|
|
11914
12545
|
* @param onHookModifiedFiles Optional callback invoked before the auto-retry so callers can notify the user.
|
|
12546
|
+
* @param options Optional commit options (e.g. noVerify).
|
|
11915
12547
|
* @returns A Promise that resolves to the CommitResult.
|
|
11916
12548
|
*/
|
|
11917
|
-
async function createCommit(message, git, onHookModifiedFiles) {
|
|
12549
|
+
async function createCommit(message, git, onHookModifiedFiles, options) {
|
|
12550
|
+
const flags = options?.noVerify ? ['--no-verify'] : [];
|
|
11918
12551
|
try {
|
|
11919
|
-
return await git.commit(message);
|
|
12552
|
+
return await git.commit(message, flags);
|
|
11920
12553
|
}
|
|
11921
12554
|
catch (error) {
|
|
11922
12555
|
if (isPreCommitHookModifiedFilesError(error)) {
|
|
@@ -11926,7 +12559,12 @@ async function createCommit(message, git, onHookModifiedFiles) {
|
|
|
11926
12559
|
}
|
|
11927
12560
|
// Stage all hook-modified files and retry the commit once
|
|
11928
12561
|
await git.add('.');
|
|
11929
|
-
return await git.commit(message);
|
|
12562
|
+
return await git.commit(message, flags);
|
|
12563
|
+
}
|
|
12564
|
+
// Wrap any other GitError so callers can present it cleanly rather than
|
|
12565
|
+
// showing a raw Node.js stack trace originating from simple-git internals.
|
|
12566
|
+
if (error instanceof GitError) {
|
|
12567
|
+
throw new PreCommitHookError(error.message);
|
|
11930
12568
|
}
|
|
11931
12569
|
throw error;
|
|
11932
12570
|
}
|
|
@@ -11939,7 +12577,7 @@ async function createCommit(message, git, onHookModifiedFiles) {
|
|
|
11939
12577
|
* @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
|
|
11940
12578
|
*/
|
|
11941
12579
|
async function getChanges({ git, options }) {
|
|
11942
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
12580
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES$1, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS$1 } = options || {};
|
|
11943
12581
|
const staged = [];
|
|
11944
12582
|
const unstaged = [];
|
|
11945
12583
|
const untracked = [];
|
|
@@ -12019,41 +12657,19 @@ async function getPreviousCommits(options) {
|
|
|
12019
12657
|
try {
|
|
12020
12658
|
const logs = await git.log({ maxCount: count });
|
|
12021
12659
|
if (!logs || logs.total === 0) {
|
|
12022
|
-
return '';
|
|
12023
|
-
}
|
|
12024
|
-
// Format the commit logs
|
|
12025
|
-
const formattedLogs = logs.all.map((commit) => {
|
|
12026
|
-
return formatSingleCommit(commit);
|
|
12027
|
-
}).join('\n\n');
|
|
12028
|
-
return formattedLogs;
|
|
12029
|
-
}
|
|
12030
|
-
catch (error) {
|
|
12031
|
-
console.error(`Error getting previous commits: ${error.message}`);
|
|
12032
|
-
return '';
|
|
12033
|
-
}
|
|
12034
|
-
}
|
|
12035
|
-
|
|
12036
|
-
/**
|
|
12037
|
-
* Retrieves a TikToken for the specified model.
|
|
12038
|
-
*
|
|
12039
|
-
* @param {TiktokenModel} modelName - The name of the TiktokenModel.
|
|
12040
|
-
* @returns A Promise that resolves to the TikToken.
|
|
12041
|
-
*/
|
|
12042
|
-
const getTikToken = async (modelName) => {
|
|
12043
|
-
return await encoding_for_model(modelName);
|
|
12044
|
-
};
|
|
12045
|
-
/**
|
|
12046
|
-
* Retrieves the token counter for a given model name.
|
|
12047
|
-
*
|
|
12048
|
-
* @param {TikTokenModel} modelName - The name of the Tiktoken model.
|
|
12049
|
-
* @returns A promise that resolves to a function that calculates the number of tokens in a given text.
|
|
12050
|
-
*/
|
|
12051
|
-
const getTokenCounter = async (modelName) => {
|
|
12052
|
-
return getTikToken(modelName).then((tokenizer) => (text) => {
|
|
12053
|
-
const tokens = tokenizer.encode(text);
|
|
12054
|
-
return tokens.length;
|
|
12055
|
-
});
|
|
12056
|
-
};
|
|
12660
|
+
return '';
|
|
12661
|
+
}
|
|
12662
|
+
// Format the commit logs
|
|
12663
|
+
const formattedLogs = logs.all.map((commit) => {
|
|
12664
|
+
return formatSingleCommit(commit);
|
|
12665
|
+
}).join('\n\n');
|
|
12666
|
+
return formattedLogs;
|
|
12667
|
+
}
|
|
12668
|
+
catch (error) {
|
|
12669
|
+
console.error(`Error getting previous commits: ${error.message}`);
|
|
12670
|
+
return '';
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
12057
12673
|
|
|
12058
12674
|
const COMMITLINT_CONFIG_FILES = [
|
|
12059
12675
|
'.commitlintrc',
|
|
@@ -12153,17 +12769,345 @@ async function noResult$2({ git, logger }) {
|
|
|
12153
12769
|
}
|
|
12154
12770
|
}
|
|
12155
12771
|
|
|
12156
|
-
const
|
|
12772
|
+
const CommitSplitPlanSchema = objectType({
|
|
12773
|
+
groups: arrayType(objectType({
|
|
12774
|
+
title: stringType().min(1),
|
|
12775
|
+
body: stringType().optional(),
|
|
12776
|
+
rationale: stringType().optional(),
|
|
12777
|
+
files: arrayType(stringType()),
|
|
12778
|
+
hunks: arrayType(stringType()),
|
|
12779
|
+
})
|
|
12780
|
+
.refine((group) => group.files.length > 0 || group.hunks.length > 0, {
|
|
12781
|
+
message: 'Each group must include at least one file or hunk',
|
|
12782
|
+
}))
|
|
12783
|
+
.min(1),
|
|
12784
|
+
});
|
|
12785
|
+
const COMMIT_SPLIT_PROMPT = PromptTemplate.fromTemplate(`You are helping split staged git changes into a small sequence of coherent commits.
|
|
12786
|
+
|
|
12787
|
+
Return ONLY valid JSON matching this schema:
|
|
12788
|
+
{{
|
|
12789
|
+
"groups": [
|
|
12790
|
+
{{
|
|
12791
|
+
"title": "conventional commit style title",
|
|
12792
|
+
"body": "commit body",
|
|
12793
|
+
"rationale": "why these files belong together",
|
|
12794
|
+
"files": ["relative/path.ts"],
|
|
12795
|
+
"hunks": ["relative/path.ts::hunk-1"]
|
|
12796
|
+
}}
|
|
12797
|
+
]
|
|
12798
|
+
}}
|
|
12799
|
+
|
|
12800
|
+
Rules:
|
|
12801
|
+
- Use each staged file exactly once.
|
|
12802
|
+
- If a file has hunk IDs and contains unrelated changes, assign every hunk ID exactly once instead of assigning the whole file.
|
|
12803
|
+
- Do not list the same file in "files" when assigning that file through "hunks".
|
|
12804
|
+
- Only use file paths listed in the staged file inventory.
|
|
12805
|
+
- Only use hunk IDs listed in the staged hunk inventory.
|
|
12806
|
+
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
12807
|
+
- Keep commit titles concise and understandable.
|
|
12808
|
+
- Do not invent files.
|
|
12809
|
+
|
|
12810
|
+
Staged file inventory:
|
|
12811
|
+
{file_inventory}
|
|
12812
|
+
|
|
12813
|
+
Staged hunk inventory:
|
|
12814
|
+
{hunk_inventory}
|
|
12815
|
+
|
|
12816
|
+
Condensed staged diff:
|
|
12817
|
+
{summary}
|
|
12818
|
+
|
|
12819
|
+
Additional context:
|
|
12820
|
+
{additional_context}`);
|
|
12821
|
+
function isCommitSplitCommand(argv) {
|
|
12822
|
+
return Boolean(argv.split || argv.plan || argv.apply || argv._.includes('split'));
|
|
12823
|
+
}
|
|
12824
|
+
function formatCommitSplitPlan(plan) {
|
|
12825
|
+
return plan.groups
|
|
12826
|
+
.map((group, index) => {
|
|
12827
|
+
const body = group.body ? `\n\n${group.body}` : '';
|
|
12828
|
+
const rationale = group.rationale ? `\n\nRationale: ${group.rationale}` : '';
|
|
12829
|
+
const files = (group.files || []).map((file) => `- ${file}`).join('\n');
|
|
12830
|
+
const hunks = (group.hunks || []).map((hunk) => `- ${hunk}`).join('\n');
|
|
12831
|
+
const sections = [
|
|
12832
|
+
files ? `Files:\n${files}` : undefined,
|
|
12833
|
+
hunks ? `Hunks:\n${hunks}` : undefined,
|
|
12834
|
+
].filter(Boolean);
|
|
12835
|
+
return `## ${index + 1}. ${group.title}${body}${rationale}\n\n${sections.join('\n\n')}`;
|
|
12836
|
+
})
|
|
12837
|
+
.join('\n\n---\n\n');
|
|
12838
|
+
}
|
|
12839
|
+
function getStagedFileSet(changes) {
|
|
12840
|
+
return new Set(changes.map((change) => change.filePath));
|
|
12841
|
+
}
|
|
12842
|
+
function getGroupFiles(group) {
|
|
12843
|
+
return group.files || [];
|
|
12844
|
+
}
|
|
12845
|
+
function getGroupHunks(group) {
|
|
12846
|
+
return group.hunks || [];
|
|
12847
|
+
}
|
|
12848
|
+
function hunkHeader(hunk) {
|
|
12849
|
+
return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
12850
|
+
}
|
|
12851
|
+
function hunkPreview(hunk) {
|
|
12852
|
+
return hunk.lines
|
|
12853
|
+
.filter((line) => line.startsWith('+') || line.startsWith('-'))
|
|
12854
|
+
.slice(0, 6)
|
|
12855
|
+
.join('\n');
|
|
12856
|
+
}
|
|
12857
|
+
async function collectHunkInventory(staged, git) {
|
|
12858
|
+
const hunks = [];
|
|
12859
|
+
const byId = new Map();
|
|
12860
|
+
const byFile = new Map();
|
|
12861
|
+
for (const change of staged) {
|
|
12862
|
+
if (change.status !== 'modified') {
|
|
12863
|
+
continue;
|
|
12864
|
+
}
|
|
12865
|
+
const diff = await git.diff(['--staged', '--', change.filePath]);
|
|
12866
|
+
const [patch] = parsePatch(diff);
|
|
12867
|
+
if (!patch || patch.hunks.length === 0) {
|
|
12868
|
+
continue;
|
|
12869
|
+
}
|
|
12870
|
+
patch.hunks.forEach((hunk, index) => {
|
|
12871
|
+
const stagedHunk = {
|
|
12872
|
+
id: `${change.filePath}::hunk-${index + 1}`,
|
|
12873
|
+
filePath: change.filePath,
|
|
12874
|
+
patch,
|
|
12875
|
+
hunk,
|
|
12876
|
+
header: hunkHeader(hunk),
|
|
12877
|
+
preview: hunkPreview(hunk),
|
|
12878
|
+
};
|
|
12879
|
+
hunks.push(stagedHunk);
|
|
12880
|
+
byId.set(stagedHunk.id, stagedHunk);
|
|
12881
|
+
byFile.set(change.filePath, [...(byFile.get(change.filePath) || []), stagedHunk]);
|
|
12882
|
+
});
|
|
12883
|
+
}
|
|
12884
|
+
return { hunks, byId, byFile };
|
|
12885
|
+
}
|
|
12886
|
+
function formatHunkInventory(inventory) {
|
|
12887
|
+
if (inventory.hunks.length === 0) {
|
|
12888
|
+
return 'No hunk-level inventory available. Use file-level groups.';
|
|
12889
|
+
}
|
|
12890
|
+
return inventory.hunks
|
|
12891
|
+
.map((hunk) => {
|
|
12892
|
+
const preview = hunk.preview ? `\n${hunk.preview}` : '';
|
|
12893
|
+
return `- ${hunk.id}: ${hunk.header}${preview}`;
|
|
12894
|
+
})
|
|
12895
|
+
.join('\n');
|
|
12896
|
+
}
|
|
12897
|
+
function validatePlanForStagedFiles(plan, staged, hunkInventory) {
|
|
12898
|
+
const stagedFiles = getStagedFileSet(staged);
|
|
12899
|
+
const seen = new Set();
|
|
12900
|
+
const seenHunks = new Set();
|
|
12901
|
+
const unknown = [];
|
|
12902
|
+
const duplicate = [];
|
|
12903
|
+
const unknownHunks = [];
|
|
12904
|
+
const duplicateHunks = [];
|
|
12905
|
+
plan.groups.forEach((group) => {
|
|
12906
|
+
getGroupFiles(group).forEach((file) => {
|
|
12907
|
+
if (!stagedFiles.has(file)) {
|
|
12908
|
+
unknown.push(file);
|
|
12909
|
+
return;
|
|
12910
|
+
}
|
|
12911
|
+
if (seen.has(file)) {
|
|
12912
|
+
duplicate.push(file);
|
|
12913
|
+
return;
|
|
12914
|
+
}
|
|
12915
|
+
seen.add(file);
|
|
12916
|
+
});
|
|
12917
|
+
getGroupHunks(group).forEach((hunkId) => {
|
|
12918
|
+
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12919
|
+
if (!hunk) {
|
|
12920
|
+
unknownHunks.push(hunkId);
|
|
12921
|
+
return;
|
|
12922
|
+
}
|
|
12923
|
+
if (seenHunks.has(hunkId)) {
|
|
12924
|
+
duplicateHunks.push(hunkId);
|
|
12925
|
+
return;
|
|
12926
|
+
}
|
|
12927
|
+
seenHunks.add(hunkId);
|
|
12928
|
+
});
|
|
12929
|
+
});
|
|
12930
|
+
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12931
|
+
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12932
|
+
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12933
|
+
.filter((file) => Boolean(file))
|
|
12934
|
+
.filter((file) => {
|
|
12935
|
+
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12936
|
+
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12937
|
+
});
|
|
12938
|
+
const missing = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12939
|
+
if (unknown.length ||
|
|
12940
|
+
duplicate.length ||
|
|
12941
|
+
unknownHunks.length ||
|
|
12942
|
+
duplicateHunks.length ||
|
|
12943
|
+
mixedFiles.length ||
|
|
12944
|
+
partiallyCoveredFiles.length ||
|
|
12945
|
+
missing.length) {
|
|
12946
|
+
throw new Error([
|
|
12947
|
+
unknown.length ? `unknown files: ${unknown.join(', ')}` : undefined,
|
|
12948
|
+
duplicate.length ? `duplicate files: ${duplicate.join(', ')}` : undefined,
|
|
12949
|
+
unknownHunks.length ? `unknown hunks: ${unknownHunks.join(', ')}` : undefined,
|
|
12950
|
+
duplicateHunks.length ? `duplicate hunks: ${duplicateHunks.join(', ')}` : undefined,
|
|
12951
|
+
mixedFiles.length ? `files assigned both as whole files and hunks: ${mixedFiles.join(', ')}` : undefined,
|
|
12952
|
+
partiallyCoveredFiles.length
|
|
12953
|
+
? `files with only some hunks assigned: ${partiallyCoveredFiles.join(', ')}`
|
|
12954
|
+
: undefined,
|
|
12955
|
+
missing.length ? `missing files: ${missing.join(', ')}` : undefined,
|
|
12956
|
+
]
|
|
12957
|
+
.filter(Boolean)
|
|
12958
|
+
.join('; '));
|
|
12959
|
+
}
|
|
12960
|
+
}
|
|
12961
|
+
function assertNoUnstagedOverlap(plan, changes, hunkInventory) {
|
|
12962
|
+
const hunkFiles = new Set(plan.groups.flatMap((group) => getGroupHunks(group)
|
|
12963
|
+
.map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath)
|
|
12964
|
+
.filter((file) => Boolean(file))));
|
|
12965
|
+
const plannedFiles = new Set(plan.groups
|
|
12966
|
+
.flatMap((group) => getGroupFiles(group))
|
|
12967
|
+
.filter((file) => !hunkFiles.has(file)));
|
|
12968
|
+
const overlapping = [...(changes.unstaged || []), ...(changes.untracked || [])]
|
|
12969
|
+
.map((change) => change.filePath)
|
|
12970
|
+
.filter((file) => plannedFiles.has(file));
|
|
12971
|
+
if (overlapping.length > 0) {
|
|
12972
|
+
throw new Error(`Cannot apply split plan because these files also have unstaged or untracked changes: ${overlapping.join(', ')}`);
|
|
12973
|
+
}
|
|
12974
|
+
}
|
|
12975
|
+
function buildPatchForHunks(hunks) {
|
|
12976
|
+
const byFile = new Map();
|
|
12977
|
+
hunks.forEach((hunk) => {
|
|
12978
|
+
byFile.set(hunk.filePath, [...(byFile.get(hunk.filePath) || []), hunk]);
|
|
12979
|
+
});
|
|
12980
|
+
return [...byFile.values()]
|
|
12981
|
+
.map((fileHunks) => {
|
|
12982
|
+
const [firstHunk] = fileHunks;
|
|
12983
|
+
return formatPatch({
|
|
12984
|
+
...firstHunk.patch,
|
|
12985
|
+
hunks: fileHunks.map((hunk) => hunk.hunk),
|
|
12986
|
+
});
|
|
12987
|
+
})
|
|
12988
|
+
.join('\n');
|
|
12989
|
+
}
|
|
12990
|
+
async function applyPatchToIndex(patch, git) {
|
|
12991
|
+
const cwd = await git.revparse(['--show-toplevel']);
|
|
12992
|
+
await new Promise((resolve, reject) => {
|
|
12993
|
+
const child = spawn('git', ['apply', '--cached', '-'], {
|
|
12994
|
+
cwd,
|
|
12995
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
12996
|
+
});
|
|
12997
|
+
let stderr = '';
|
|
12998
|
+
child.stderr.on('data', (chunk) => {
|
|
12999
|
+
stderr += String(chunk);
|
|
13000
|
+
});
|
|
13001
|
+
child.on('error', reject);
|
|
13002
|
+
child.on('close', (code) => {
|
|
13003
|
+
if (code === 0) {
|
|
13004
|
+
resolve();
|
|
13005
|
+
return;
|
|
13006
|
+
}
|
|
13007
|
+
reject(new Error(`Failed to apply hunk patch to index: ${stderr.trim()}`));
|
|
13008
|
+
});
|
|
13009
|
+
child.stdin.write(patch);
|
|
13010
|
+
child.stdin.end();
|
|
13011
|
+
});
|
|
13012
|
+
}
|
|
13013
|
+
async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger, noVerify, }) {
|
|
13014
|
+
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
13015
|
+
assertNoUnstagedOverlap(plan, changes, hunkInventory);
|
|
13016
|
+
await git.raw(['reset']);
|
|
13017
|
+
for (const group of plan.groups) {
|
|
13018
|
+
const groupFiles = getGroupFiles(group);
|
|
13019
|
+
const groupHunks = getGroupHunks(group).map((hunkId) => hunkInventory.byId.get(hunkId));
|
|
13020
|
+
if (groupFiles.length > 0) {
|
|
13021
|
+
await git.add(groupFiles);
|
|
13022
|
+
}
|
|
13023
|
+
if (groupHunks.length > 0) {
|
|
13024
|
+
const patch = buildPatchForHunks(groupHunks.filter((hunk) => Boolean(hunk)));
|
|
13025
|
+
await applyPatchToIndex(patch, git);
|
|
13026
|
+
}
|
|
13027
|
+
await createCommit(`${group.title}\n\n${group.body}`.trim(), git, undefined, { noVerify });
|
|
13028
|
+
logger.verbose(`Created split commit: ${group.title}`, { color: 'green' });
|
|
13029
|
+
}
|
|
13030
|
+
return `Created ${plan.groups.length} split commit(s).`;
|
|
13031
|
+
}
|
|
13032
|
+
async function handleCommitSplit({ argv, config, git, logger, tokenizer, llm, }) {
|
|
13033
|
+
const changes = await getChanges({
|
|
13034
|
+
git,
|
|
13035
|
+
options: {
|
|
13036
|
+
ignoredFiles: config.ignoredFiles || undefined,
|
|
13037
|
+
ignoredExtensions: config.ignoredExtensions || undefined,
|
|
13038
|
+
},
|
|
13039
|
+
});
|
|
13040
|
+
if (changes.staged.length === 0) {
|
|
13041
|
+
return 'No staged changes found.';
|
|
13042
|
+
}
|
|
13043
|
+
const hunkInventory = await collectHunkInventory(changes.staged, git);
|
|
13044
|
+
const summary = await fileChangeParser({
|
|
13045
|
+
changes: changes.staged,
|
|
13046
|
+
commit: '--staged',
|
|
13047
|
+
options: {
|
|
13048
|
+
tokenizer,
|
|
13049
|
+
git,
|
|
13050
|
+
llm,
|
|
13051
|
+
logger,
|
|
13052
|
+
maxTokens: config.service.tokenLimit,
|
|
13053
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
13054
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
13055
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
13056
|
+
metadata: {
|
|
13057
|
+
command: 'commit',
|
|
13058
|
+
provider: config.service.provider,
|
|
13059
|
+
model: String(config.service.model),
|
|
13060
|
+
},
|
|
13061
|
+
},
|
|
13062
|
+
});
|
|
13063
|
+
const fileInventory = changes.staged
|
|
13064
|
+
.map((change) => `- ${change.filePath}: ${change.status} - ${change.summary}`)
|
|
13065
|
+
.join('\n');
|
|
13066
|
+
const hunkInventoryText = formatHunkInventory(hunkInventory);
|
|
13067
|
+
const plan = await executeChainWithSchema(CommitSplitPlanSchema, llm, COMMIT_SPLIT_PROMPT, {
|
|
13068
|
+
file_inventory: fileInventory,
|
|
13069
|
+
hunk_inventory: hunkInventoryText,
|
|
13070
|
+
summary,
|
|
13071
|
+
additional_context: argv.additional || '',
|
|
13072
|
+
}, {
|
|
13073
|
+
logger,
|
|
13074
|
+
tokenizer,
|
|
13075
|
+
metadata: {
|
|
13076
|
+
task: 'commit-split-plan',
|
|
13077
|
+
command: 'commit',
|
|
13078
|
+
provider: config.service.provider,
|
|
13079
|
+
model: String(config.service.model),
|
|
13080
|
+
},
|
|
13081
|
+
});
|
|
13082
|
+
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
13083
|
+
if (argv.apply) {
|
|
13084
|
+
return await applyCommitSplitPlan({
|
|
13085
|
+
plan,
|
|
13086
|
+
changes,
|
|
13087
|
+
hunkInventory,
|
|
13088
|
+
git,
|
|
13089
|
+
logger,
|
|
13090
|
+
noVerify: argv.noVerify || config.noVerify || false,
|
|
13091
|
+
});
|
|
13092
|
+
}
|
|
13093
|
+
return formatCommitSplitPlan(plan);
|
|
13094
|
+
}
|
|
13095
|
+
|
|
13096
|
+
const handler$4 = async (argv, logger) => {
|
|
12157
13097
|
const git = getRepo();
|
|
12158
13098
|
const config = loadConfig(argv);
|
|
12159
13099
|
const key = getApiKeyForModel(config);
|
|
12160
|
-
const { provider
|
|
13100
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
13101
|
+
const commitService = resolveDynamicService(config, 'commit');
|
|
13102
|
+
const summaryService = resolveDynamicService(config, 'summarize');
|
|
13103
|
+
const model = commitService.model;
|
|
12161
13104
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
12162
13105
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
12163
13106
|
process.exit(1);
|
|
12164
13107
|
}
|
|
12165
13108
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
12166
|
-
const llm = getLlm(provider, model, config);
|
|
13109
|
+
const llm = getLlm(provider, model, { ...config, service: commitService });
|
|
13110
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
12167
13111
|
const INTERACTIVE = argv.interactive || isInteractive(config);
|
|
12168
13112
|
if (INTERACTIVE) {
|
|
12169
13113
|
if (!config.hideCocoBanner) {
|
|
@@ -12181,6 +13125,22 @@ const handler$3 = async (argv, logger) => {
|
|
|
12181
13125
|
logger.verbose(`→ ${provider} (${model})`, {
|
|
12182
13126
|
color: 'green',
|
|
12183
13127
|
});
|
|
13128
|
+
if (isCommitSplitCommand(argv)) {
|
|
13129
|
+
const splitResult = await handleCommitSplit({
|
|
13130
|
+
argv,
|
|
13131
|
+
config,
|
|
13132
|
+
git,
|
|
13133
|
+
logger,
|
|
13134
|
+
tokenizer,
|
|
13135
|
+
llm,
|
|
13136
|
+
});
|
|
13137
|
+
await handleResult({
|
|
13138
|
+
result: splitResult,
|
|
13139
|
+
mode: config.mode || 'stdout',
|
|
13140
|
+
});
|
|
13141
|
+
logLlmTelemetrySummary(logger, 'commit');
|
|
13142
|
+
return;
|
|
13143
|
+
}
|
|
12184
13144
|
const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
|
|
12185
13145
|
async function factory() {
|
|
12186
13146
|
if (config.noDiff) {
|
|
@@ -12218,12 +13178,17 @@ const handler$3 = async (argv, logger) => {
|
|
|
12218
13178
|
options: {
|
|
12219
13179
|
tokenizer,
|
|
12220
13180
|
git,
|
|
12221
|
-
llm,
|
|
13181
|
+
llm: summaryLlm,
|
|
12222
13182
|
logger,
|
|
12223
13183
|
maxTokens: config.service.tokenLimit,
|
|
12224
13184
|
minTokensForSummary: config.service.minTokensForSummary,
|
|
12225
13185
|
maxFileTokens: config.service.maxFileTokens,
|
|
12226
13186
|
maxConcurrent: config.service.maxConcurrent,
|
|
13187
|
+
metadata: {
|
|
13188
|
+
command: 'commit',
|
|
13189
|
+
provider,
|
|
13190
|
+
model: String(summaryService.model),
|
|
13191
|
+
},
|
|
12227
13192
|
},
|
|
12228
13193
|
});
|
|
12229
13194
|
}
|
|
@@ -12363,7 +13328,24 @@ IMPORTANT RULES:
|
|
|
12363
13328
|
? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
|
|
12364
13329
|
: variables.additional_context,
|
|
12365
13330
|
};
|
|
12366
|
-
const
|
|
13331
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
13332
|
+
prompt,
|
|
13333
|
+
variables: currentVariables,
|
|
13334
|
+
tokenizer,
|
|
13335
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
13336
|
+
});
|
|
13337
|
+
if (budgetedPrompt.truncated) {
|
|
13338
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
13339
|
+
}
|
|
13340
|
+
const commitMsg = await executeChainWithSchema(schema, llm, prompt, budgetedPrompt.variables, {
|
|
13341
|
+
logger,
|
|
13342
|
+
tokenizer,
|
|
13343
|
+
metadata: {
|
|
13344
|
+
task: USE_CONVENTIONAL_COMMITS ? 'commit-message-conventional' : 'commit-message',
|
|
13345
|
+
command: 'commit',
|
|
13346
|
+
provider,
|
|
13347
|
+
model: String(model),
|
|
13348
|
+
},
|
|
12367
13349
|
retryOptions: {
|
|
12368
13350
|
maxAttempts,
|
|
12369
13351
|
onRetry: (attempt, error) => {
|
|
@@ -12508,36 +13490,94 @@ IMPORTANT RULES:
|
|
|
12508
13490
|
handleResult({
|
|
12509
13491
|
result: commitMsg,
|
|
12510
13492
|
interactiveModeCallback: async (result) => {
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
13493
|
+
const noVerify = argv.noVerify || config.noVerify || false;
|
|
13494
|
+
const attemptCommit = async (skipHooks) => {
|
|
13495
|
+
try {
|
|
13496
|
+
await createCommit(result, git, () => {
|
|
13497
|
+
logger.log('⚠️ Pre-commit hook modified files. Staging changes and retrying commit...', { color: 'yellow' });
|
|
13498
|
+
}, { noVerify: skipHooks });
|
|
13499
|
+
logSuccess();
|
|
13500
|
+
}
|
|
13501
|
+
catch (error) {
|
|
13502
|
+
if (error instanceof PreCommitHookError) {
|
|
13503
|
+
// Display friendly hook failure output
|
|
13504
|
+
logger.log('\n✖ Commit blocked by pre-commit hook', { color: 'red' });
|
|
13505
|
+
logger.log('\nHook output:', { color: 'yellow' });
|
|
13506
|
+
logger.log(SEPERATOR);
|
|
13507
|
+
logger.log(error.hookOutput);
|
|
13508
|
+
logger.log(SEPERATOR);
|
|
13509
|
+
if (INTERACTIVE) {
|
|
13510
|
+
const { select } = await import('@inquirer/prompts');
|
|
13511
|
+
const choice = await select({
|
|
13512
|
+
message: 'How would you like to proceed?',
|
|
13513
|
+
choices: [
|
|
13514
|
+
{
|
|
13515
|
+
name: '🔄 Retry',
|
|
13516
|
+
value: 'retry',
|
|
13517
|
+
description: 'Fix the issues above and retry the commit',
|
|
13518
|
+
},
|
|
13519
|
+
{
|
|
13520
|
+
name: '⚠️ Skip hooks',
|
|
13521
|
+
value: 'skip',
|
|
13522
|
+
description: 'Retry with --no-verify to bypass pre-commit hooks (use with care)',
|
|
13523
|
+
},
|
|
13524
|
+
{
|
|
13525
|
+
name: '💣 Abort',
|
|
13526
|
+
value: 'abort',
|
|
13527
|
+
description: 'Abort the commit',
|
|
13528
|
+
},
|
|
13529
|
+
],
|
|
13530
|
+
});
|
|
13531
|
+
if (choice === 'retry') {
|
|
13532
|
+
await attemptCommit(false);
|
|
13533
|
+
}
|
|
13534
|
+
else if (choice === 'skip') {
|
|
13535
|
+
logger.log('⚠️ Skipping hooks with --no-verify...', { color: 'yellow' });
|
|
13536
|
+
await attemptCommit(true);
|
|
13537
|
+
}
|
|
13538
|
+
else {
|
|
13539
|
+
logger.log('\nCommit aborted.', { color: 'red' });
|
|
13540
|
+
process.exit(1);
|
|
13541
|
+
}
|
|
13542
|
+
}
|
|
13543
|
+
else {
|
|
13544
|
+
logger.log('\nFix the issues above and try again, or use --no-verify to skip hooks.', { color: 'yellow' });
|
|
13545
|
+
process.exit(1);
|
|
13546
|
+
}
|
|
13547
|
+
}
|
|
13548
|
+
else {
|
|
13549
|
+
throw error;
|
|
13550
|
+
}
|
|
13551
|
+
}
|
|
13552
|
+
};
|
|
13553
|
+
await attemptCommit(noVerify);
|
|
12515
13554
|
},
|
|
12516
13555
|
mode: MODE,
|
|
12517
13556
|
});
|
|
13557
|
+
logLlmTelemetrySummary(logger, 'commit');
|
|
12518
13558
|
};
|
|
12519
13559
|
|
|
12520
13560
|
var commit = {
|
|
12521
|
-
command: command$
|
|
13561
|
+
command: command$4,
|
|
12522
13562
|
desc: 'Summarize the staged changes in a commit message.',
|
|
12523
|
-
builder: builder$
|
|
12524
|
-
handler: commandExecutor(handler$
|
|
12525
|
-
options: options$
|
|
13563
|
+
builder: builder$4,
|
|
13564
|
+
handler: commandExecutor(handler$4),
|
|
13565
|
+
options: options$4,
|
|
12526
13566
|
};
|
|
12527
13567
|
|
|
12528
|
-
const command$
|
|
13568
|
+
const command$3 = 'init';
|
|
12529
13569
|
/**
|
|
12530
13570
|
* Command line options via yargs
|
|
12531
13571
|
*/
|
|
12532
|
-
const options$
|
|
13572
|
+
const options$3 = {
|
|
12533
13573
|
scope: {
|
|
12534
13574
|
type: 'string',
|
|
12535
13575
|
description: 'configure coco for the current user or project?',
|
|
12536
13576
|
choices: ['global', 'project'],
|
|
12537
13577
|
},
|
|
12538
13578
|
};
|
|
12539
|
-
const builder$
|
|
12540
|
-
return yargs.options(options$
|
|
13579
|
+
const builder$3 = (yargs) => {
|
|
13580
|
+
return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
|
|
12541
13581
|
};
|
|
12542
13582
|
|
|
12543
13583
|
/**
|
|
@@ -12888,7 +13928,7 @@ const questions = {
|
|
|
12888
13928
|
}),
|
|
12889
13929
|
};
|
|
12890
13930
|
|
|
12891
|
-
const handler$
|
|
13931
|
+
const handler$3 = async (argv, logger) => {
|
|
12892
13932
|
const options = loadConfig(argv);
|
|
12893
13933
|
logger.log(LOGO);
|
|
12894
13934
|
let scope = options?.scope;
|
|
@@ -13059,8 +14099,287 @@ async function installCommitlintPackages(scope, logger) {
|
|
|
13059
14099
|
}
|
|
13060
14100
|
|
|
13061
14101
|
var init = {
|
|
13062
|
-
command: command$
|
|
14102
|
+
command: command$3,
|
|
13063
14103
|
desc: 'install & configure coco globally or for the current project',
|
|
14104
|
+
builder: builder$3,
|
|
14105
|
+
handler: commandExecutor(handler$3),
|
|
14106
|
+
options: options$3,
|
|
14107
|
+
};
|
|
14108
|
+
|
|
14109
|
+
const command$2 = 'log';
|
|
14110
|
+
const options$2 = {
|
|
14111
|
+
all: {
|
|
14112
|
+
description: 'Show commits from all local and remote refs',
|
|
14113
|
+
type: 'boolean',
|
|
14114
|
+
default: false,
|
|
14115
|
+
},
|
|
14116
|
+
author: {
|
|
14117
|
+
description: 'Filter commits by author',
|
|
14118
|
+
type: 'string',
|
|
14119
|
+
},
|
|
14120
|
+
branch: {
|
|
14121
|
+
description: 'Show commits reachable from a branch or ref',
|
|
14122
|
+
type: 'string',
|
|
14123
|
+
alias: 'b',
|
|
14124
|
+
},
|
|
14125
|
+
commit: {
|
|
14126
|
+
description: 'Show details and changed files for a single commit',
|
|
14127
|
+
type: 'string',
|
|
14128
|
+
alias: 'c',
|
|
14129
|
+
},
|
|
14130
|
+
format: {
|
|
14131
|
+
description: 'Output format',
|
|
14132
|
+
choices: ['table', 'json'],
|
|
14133
|
+
default: 'table',
|
|
14134
|
+
},
|
|
14135
|
+
limit: {
|
|
14136
|
+
description: 'Maximum number of commits to show',
|
|
14137
|
+
type: 'number',
|
|
14138
|
+
default: 30,
|
|
14139
|
+
alias: 'n',
|
|
14140
|
+
},
|
|
14141
|
+
noMerges: {
|
|
14142
|
+
description: 'Exclude merge commits',
|
|
14143
|
+
type: 'boolean',
|
|
14144
|
+
default: false,
|
|
14145
|
+
},
|
|
14146
|
+
path: {
|
|
14147
|
+
description: 'Filter commits by changed path',
|
|
14148
|
+
type: 'array',
|
|
14149
|
+
},
|
|
14150
|
+
since: {
|
|
14151
|
+
description: 'Show commits more recent than a date',
|
|
14152
|
+
type: 'string',
|
|
14153
|
+
},
|
|
14154
|
+
until: {
|
|
14155
|
+
description: 'Show commits older than a date',
|
|
14156
|
+
type: 'string',
|
|
14157
|
+
},
|
|
14158
|
+
};
|
|
14159
|
+
const builder$2 = (yargs) => {
|
|
14160
|
+
return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
|
|
14161
|
+
};
|
|
14162
|
+
|
|
14163
|
+
const FIELD_SEPARATOR = '\x1f';
|
|
14164
|
+
const LOG_FORMAT = `%x1f%h%x1f%H%x1f%ad%x1f%an%x1f%d%x1f%s`;
|
|
14165
|
+
const DETAIL_FORMAT = `%H%x1f%h%x1f%ad%x1f%an%x1f%d%x1f%s%x1f%b`;
|
|
14166
|
+
function toArray(value) {
|
|
14167
|
+
if (!value) {
|
|
14168
|
+
return [];
|
|
14169
|
+
}
|
|
14170
|
+
return Array.isArray(value) ? value : [value];
|
|
14171
|
+
}
|
|
14172
|
+
function normalizeLimit(limit) {
|
|
14173
|
+
if (!limit || Number.isNaN(limit) || limit < 1) {
|
|
14174
|
+
return 30;
|
|
14175
|
+
}
|
|
14176
|
+
return Math.floor(limit);
|
|
14177
|
+
}
|
|
14178
|
+
function cleanRefs(refs) {
|
|
14179
|
+
const trimmed = refs.trim();
|
|
14180
|
+
if (!trimmed) {
|
|
14181
|
+
return [];
|
|
14182
|
+
}
|
|
14183
|
+
return trimmed
|
|
14184
|
+
.replace(/^\(/, '')
|
|
14185
|
+
.replace(/\)$/, '')
|
|
14186
|
+
.split(',')
|
|
14187
|
+
.map((ref) => ref.trim())
|
|
14188
|
+
.filter(Boolean);
|
|
14189
|
+
}
|
|
14190
|
+
function parseLogOutput(output) {
|
|
14191
|
+
return output
|
|
14192
|
+
.split('\n')
|
|
14193
|
+
.map((line) => line.trimEnd())
|
|
14194
|
+
.filter((line) => line.includes(FIELD_SEPARATOR))
|
|
14195
|
+
.map((line) => {
|
|
14196
|
+
const [graph, shortHash, hash, date, author, refs, message] = line.split(FIELD_SEPARATOR);
|
|
14197
|
+
return {
|
|
14198
|
+
graph: graph.trimEnd(),
|
|
14199
|
+
shortHash,
|
|
14200
|
+
hash,
|
|
14201
|
+
date,
|
|
14202
|
+
author,
|
|
14203
|
+
refs: cleanRefs(refs),
|
|
14204
|
+
message,
|
|
14205
|
+
};
|
|
14206
|
+
});
|
|
14207
|
+
}
|
|
14208
|
+
function parseNameStatus(output) {
|
|
14209
|
+
return output
|
|
14210
|
+
.split('\n')
|
|
14211
|
+
.map((line) => line.trim())
|
|
14212
|
+
.filter(Boolean)
|
|
14213
|
+
.map((line) => {
|
|
14214
|
+
const [status, firstPath, secondPath] = line.split('\t');
|
|
14215
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
14216
|
+
return {
|
|
14217
|
+
status,
|
|
14218
|
+
oldPath: firstPath,
|
|
14219
|
+
path: secondPath,
|
|
14220
|
+
};
|
|
14221
|
+
}
|
|
14222
|
+
return {
|
|
14223
|
+
status,
|
|
14224
|
+
path: firstPath,
|
|
14225
|
+
};
|
|
14226
|
+
});
|
|
14227
|
+
}
|
|
14228
|
+
function parseCommitDetail(metadata, files) {
|
|
14229
|
+
const [hash, shortHash, date, author, refs, message, body = ''] = metadata
|
|
14230
|
+
.trimEnd()
|
|
14231
|
+
.split(FIELD_SEPARATOR);
|
|
14232
|
+
return {
|
|
14233
|
+
shortHash,
|
|
14234
|
+
hash,
|
|
14235
|
+
date,
|
|
14236
|
+
author,
|
|
14237
|
+
refs: cleanRefs(refs),
|
|
14238
|
+
message,
|
|
14239
|
+
body: body.trim(),
|
|
14240
|
+
files: parseNameStatus(files),
|
|
14241
|
+
};
|
|
14242
|
+
}
|
|
14243
|
+
function truncate(value, width) {
|
|
14244
|
+
if (value.length <= width) {
|
|
14245
|
+
return value;
|
|
14246
|
+
}
|
|
14247
|
+
return `${value.slice(0, Math.max(0, width - 1))}.`;
|
|
14248
|
+
}
|
|
14249
|
+
function pad(value, width) {
|
|
14250
|
+
return truncate(value, width).padEnd(width, ' ');
|
|
14251
|
+
}
|
|
14252
|
+
function formatLogTable(entries) {
|
|
14253
|
+
if (entries.length === 0) {
|
|
14254
|
+
return 'No commits found.';
|
|
14255
|
+
}
|
|
14256
|
+
const rows = entries.map((entry) => {
|
|
14257
|
+
const refs = entry.refs.join(', ');
|
|
14258
|
+
return [
|
|
14259
|
+
pad(entry.graph || '*', 8),
|
|
14260
|
+
pad(entry.shortHash, 9),
|
|
14261
|
+
pad(entry.date, 10),
|
|
14262
|
+
pad(entry.author, 18),
|
|
14263
|
+
pad(refs, 26),
|
|
14264
|
+
entry.message,
|
|
14265
|
+
].join(' ');
|
|
14266
|
+
});
|
|
14267
|
+
return [
|
|
14268
|
+
[
|
|
14269
|
+
pad('Graph', 8),
|
|
14270
|
+
pad('Commit', 9),
|
|
14271
|
+
pad('Date', 10),
|
|
14272
|
+
pad('Author', 18),
|
|
14273
|
+
pad('Refs', 26),
|
|
14274
|
+
'Message',
|
|
14275
|
+
].join(' '),
|
|
14276
|
+
...rows,
|
|
14277
|
+
].join('\n');
|
|
14278
|
+
}
|
|
14279
|
+
function formatCommitDetail(detail, format) {
|
|
14280
|
+
if (format === 'json') {
|
|
14281
|
+
return JSON.stringify(detail, null, 2);
|
|
14282
|
+
}
|
|
14283
|
+
const refs = detail.refs.length ? ` (${detail.refs.join(', ')})` : '';
|
|
14284
|
+
const body = detail.body ? `\n\n${detail.body}` : '';
|
|
14285
|
+
const files = detail.files.length
|
|
14286
|
+
? detail.files
|
|
14287
|
+
.map((file) => {
|
|
14288
|
+
if (file.oldPath) {
|
|
14289
|
+
return ` ${file.status} ${file.oldPath} -> ${file.path}`;
|
|
14290
|
+
}
|
|
14291
|
+
return ` ${file.status} ${file.path}`;
|
|
14292
|
+
})
|
|
14293
|
+
.join('\n')
|
|
14294
|
+
: ' No changed files found.';
|
|
14295
|
+
return [
|
|
14296
|
+
`commit ${detail.hash}${refs}`,
|
|
14297
|
+
`Author: ${detail.author}`,
|
|
14298
|
+
`Date: ${detail.date}`,
|
|
14299
|
+
'',
|
|
14300
|
+
` ${detail.message}${body}`,
|
|
14301
|
+
'',
|
|
14302
|
+
'Changed files:',
|
|
14303
|
+
files,
|
|
14304
|
+
].join('\n');
|
|
14305
|
+
}
|
|
14306
|
+
function buildLogArgs(argv) {
|
|
14307
|
+
const args = [
|
|
14308
|
+
'log',
|
|
14309
|
+
'--graph',
|
|
14310
|
+
'--decorate=short',
|
|
14311
|
+
'--date=short',
|
|
14312
|
+
'--color=never',
|
|
14313
|
+
`--max-count=${normalizeLimit(argv.limit)}`,
|
|
14314
|
+
`--pretty=format:${LOG_FORMAT}`,
|
|
14315
|
+
];
|
|
14316
|
+
if (argv.noMerges) {
|
|
14317
|
+
args.push('--no-merges');
|
|
14318
|
+
}
|
|
14319
|
+
if (argv.author) {
|
|
14320
|
+
args.push(`--author=${argv.author}`);
|
|
14321
|
+
}
|
|
14322
|
+
if (argv.since) {
|
|
14323
|
+
args.push(`--since=${argv.since}`);
|
|
14324
|
+
}
|
|
14325
|
+
if (argv.until) {
|
|
14326
|
+
args.push(`--until=${argv.until}`);
|
|
14327
|
+
}
|
|
14328
|
+
if (argv.all) {
|
|
14329
|
+
args.push('--all');
|
|
14330
|
+
}
|
|
14331
|
+
else if (argv.branch) {
|
|
14332
|
+
args.push(argv.branch);
|
|
14333
|
+
}
|
|
14334
|
+
const paths = toArray(argv.path);
|
|
14335
|
+
if (paths.length > 0) {
|
|
14336
|
+
args.push('--', ...paths);
|
|
14337
|
+
}
|
|
14338
|
+
return args;
|
|
14339
|
+
}
|
|
14340
|
+
async function getCommitDetail(git, commit) {
|
|
14341
|
+
const metadata = await git.raw([
|
|
14342
|
+
'show',
|
|
14343
|
+
'--no-patch',
|
|
14344
|
+
'--date=short',
|
|
14345
|
+
'--color=never',
|
|
14346
|
+
`--pretty=format:${DETAIL_FORMAT}`,
|
|
14347
|
+
commit,
|
|
14348
|
+
]);
|
|
14349
|
+
const files = await git.raw([
|
|
14350
|
+
'show',
|
|
14351
|
+
'--name-status',
|
|
14352
|
+
'--format=',
|
|
14353
|
+
'--find-renames',
|
|
14354
|
+
'--color=never',
|
|
14355
|
+
commit,
|
|
14356
|
+
]);
|
|
14357
|
+
return parseCommitDetail(metadata, files);
|
|
14358
|
+
}
|
|
14359
|
+
const handler$2 = async (argv) => {
|
|
14360
|
+
const git = getRepo();
|
|
14361
|
+
const mode = argv.interactive ? 'interactive' : 'stdout';
|
|
14362
|
+
const format = argv.format === 'json' ? 'json' : 'table';
|
|
14363
|
+
if (argv.commit) {
|
|
14364
|
+
const detail = await getCommitDetail(git, argv.commit);
|
|
14365
|
+
await handleResult({
|
|
14366
|
+
result: formatCommitDetail(detail, format),
|
|
14367
|
+
mode,
|
|
14368
|
+
});
|
|
14369
|
+
return;
|
|
14370
|
+
}
|
|
14371
|
+
const output = await git.raw(buildLogArgs(argv));
|
|
14372
|
+
const entries = parseLogOutput(output);
|
|
14373
|
+
const result = format === 'json' ? JSON.stringify(entries, null, 2) : formatLogTable(entries);
|
|
14374
|
+
await handleResult({
|
|
14375
|
+
result,
|
|
14376
|
+
mode,
|
|
14377
|
+
});
|
|
14378
|
+
};
|
|
14379
|
+
|
|
14380
|
+
var log = {
|
|
14381
|
+
command: command$2,
|
|
14382
|
+
desc: 'Explore commit history with a branch graph, filters, and commit details.',
|
|
13064
14383
|
builder: builder$2,
|
|
13065
14384
|
handler: commandExecutor(handler$2),
|
|
13066
14385
|
options: options$2,
|
|
@@ -13138,13 +14457,17 @@ const handler$1 = async (argv, logger) => {
|
|
|
13138
14457
|
const git = getRepo();
|
|
13139
14458
|
const config = loadConfig(argv);
|
|
13140
14459
|
const key = getApiKeyForModel(config);
|
|
13141
|
-
const { provider
|
|
14460
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
14461
|
+
const recapService = resolveDynamicService(config, 'recap');
|
|
14462
|
+
const summaryService = resolveDynamicService(config, 'summarize');
|
|
14463
|
+
const model = recapService.model;
|
|
13142
14464
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
13143
14465
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
13144
14466
|
process.exit(1);
|
|
13145
14467
|
}
|
|
13146
14468
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
13147
|
-
const llm = getLlm(provider, model, config);
|
|
14469
|
+
const llm = getLlm(provider, model, { ...config, service: recapService });
|
|
14470
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
13148
14471
|
const INTERACTIVE = argv.interactive || isInteractive(config);
|
|
13149
14472
|
if (INTERACTIVE) {
|
|
13150
14473
|
if (!config.hideCocoBanner) {
|
|
@@ -13175,19 +14498,49 @@ const handler$1 = async (argv, logger) => {
|
|
|
13175
14498
|
const unstagedChanges = await fileChangeParser({
|
|
13176
14499
|
changes: unstaged || [],
|
|
13177
14500
|
commit: '--unstaged',
|
|
13178
|
-
options: {
|
|
14501
|
+
options: {
|
|
14502
|
+
tokenizer,
|
|
14503
|
+
git,
|
|
14504
|
+
llm: summaryLlm,
|
|
14505
|
+
logger,
|
|
14506
|
+
metadata: {
|
|
14507
|
+
command: 'recap',
|
|
14508
|
+
provider,
|
|
14509
|
+
model: String(summaryService.model),
|
|
14510
|
+
},
|
|
14511
|
+
},
|
|
13179
14512
|
});
|
|
13180
14513
|
const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
|
|
13181
14514
|
const untrackedChanges = await fileChangeParser({
|
|
13182
14515
|
changes: untracked || [],
|
|
13183
14516
|
commit: '--untracked',
|
|
13184
|
-
options: {
|
|
14517
|
+
options: {
|
|
14518
|
+
tokenizer,
|
|
14519
|
+
git,
|
|
14520
|
+
llm: summaryLlm,
|
|
14521
|
+
logger,
|
|
14522
|
+
metadata: {
|
|
14523
|
+
command: 'recap',
|
|
14524
|
+
provider,
|
|
14525
|
+
model: String(summaryService.model),
|
|
14526
|
+
},
|
|
14527
|
+
},
|
|
13185
14528
|
});
|
|
13186
14529
|
const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
|
|
13187
14530
|
const stagedChanges = await fileChangeParser({
|
|
13188
14531
|
changes: staged,
|
|
13189
14532
|
commit: '--staged',
|
|
13190
|
-
options: {
|
|
14533
|
+
options: {
|
|
14534
|
+
tokenizer,
|
|
14535
|
+
git,
|
|
14536
|
+
llm: summaryLlm,
|
|
14537
|
+
logger,
|
|
14538
|
+
metadata: {
|
|
14539
|
+
command: 'recap',
|
|
14540
|
+
provider,
|
|
14541
|
+
model: String(summaryService.model),
|
|
14542
|
+
},
|
|
14543
|
+
},
|
|
13191
14544
|
});
|
|
13192
14545
|
const stagedResponse = `Staged changes:\n${stagedChanges}`;
|
|
13193
14546
|
return [unstagedResponse, untrackedResponse, stagedResponse];
|
|
@@ -13222,7 +14575,17 @@ const handler$1 = async (argv, logger) => {
|
|
|
13222
14575
|
const branchChanges = await fileChangeParser({
|
|
13223
14576
|
changes: changes.staged,
|
|
13224
14577
|
commit: baseBranch,
|
|
13225
|
-
options: {
|
|
14578
|
+
options: {
|
|
14579
|
+
tokenizer,
|
|
14580
|
+
git,
|
|
14581
|
+
llm: summaryLlm,
|
|
14582
|
+
logger,
|
|
14583
|
+
metadata: {
|
|
14584
|
+
command: 'recap',
|
|
14585
|
+
provider,
|
|
14586
|
+
model: String(summaryService.model),
|
|
14587
|
+
},
|
|
14588
|
+
},
|
|
13226
14589
|
});
|
|
13227
14590
|
return [branchChanges];
|
|
13228
14591
|
default:
|
|
@@ -13267,15 +14630,34 @@ const handler$1 = async (argv, logger) => {
|
|
|
13267
14630
|
try {
|
|
13268
14631
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13269
14632
|
const parser = createSchemaParser(RecapLlmResponseSchema, llm);
|
|
14633
|
+
const variables = {
|
|
14634
|
+
changes: context,
|
|
14635
|
+
format_instructions: formatInstructions,
|
|
14636
|
+
timeframe,
|
|
14637
|
+
};
|
|
14638
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
14639
|
+
prompt,
|
|
14640
|
+
variables,
|
|
14641
|
+
tokenizer,
|
|
14642
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
14643
|
+
summaryKey: 'changes',
|
|
14644
|
+
});
|
|
14645
|
+
if (budgetedPrompt.truncated) {
|
|
14646
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
14647
|
+
}
|
|
13270
14648
|
const response = await executeChain({
|
|
13271
14649
|
llm,
|
|
13272
14650
|
prompt,
|
|
13273
|
-
variables:
|
|
13274
|
-
changes: context,
|
|
13275
|
-
format_instructions: formatInstructions,
|
|
13276
|
-
timeframe,
|
|
13277
|
-
},
|
|
14651
|
+
variables: budgetedPrompt.variables,
|
|
13278
14652
|
parser,
|
|
14653
|
+
logger,
|
|
14654
|
+
tokenizer,
|
|
14655
|
+
metadata: {
|
|
14656
|
+
task: 'recap',
|
|
14657
|
+
command: 'recap',
|
|
14658
|
+
provider,
|
|
14659
|
+
model: String(model),
|
|
14660
|
+
},
|
|
13279
14661
|
});
|
|
13280
14662
|
return response ? `${response.title}\n\n${response.summary}` : 'no response';
|
|
13281
14663
|
}
|
|
@@ -13312,6 +14694,7 @@ ${errorMessage}
|
|
|
13312
14694
|
},
|
|
13313
14695
|
mode: MODE,
|
|
13314
14696
|
});
|
|
14697
|
+
logLlmTelemetrySummary(logger, 'recap');
|
|
13315
14698
|
};
|
|
13316
14699
|
|
|
13317
14700
|
var recap = {
|
|
@@ -13667,13 +15050,17 @@ const handler = async (argv, logger) => {
|
|
|
13667
15050
|
const git = getRepo();
|
|
13668
15051
|
const config = loadConfig(argv);
|
|
13669
15052
|
const key = getApiKeyForModel(config);
|
|
13670
|
-
const { provider
|
|
15053
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
15054
|
+
const reviewService = resolveDynamicService(config, 'review');
|
|
15055
|
+
const summaryService = resolveDynamicService(config, argv.branch ? 'largeDiff' : 'summarize');
|
|
15056
|
+
const model = reviewService.model;
|
|
13671
15057
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
13672
15058
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
13673
15059
|
process.exit(1);
|
|
13674
15060
|
}
|
|
13675
15061
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
13676
|
-
const llm = getLlm(provider, model, config);
|
|
15062
|
+
const llm = getLlm(provider, model, { ...config, service: reviewService });
|
|
15063
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
13677
15064
|
const INTERACTIVE = isInteractive(config);
|
|
13678
15065
|
if (INTERACTIVE) {
|
|
13679
15066
|
if (!config.hideCocoBanner) {
|
|
@@ -13697,7 +15084,17 @@ const handler = async (argv, logger) => {
|
|
|
13697
15084
|
const branchChanges = await fileChangeParser({
|
|
13698
15085
|
changes: diff.staged,
|
|
13699
15086
|
commit: `--branch-diff-${argv.branch}`,
|
|
13700
|
-
options: {
|
|
15087
|
+
options: {
|
|
15088
|
+
tokenizer,
|
|
15089
|
+
git,
|
|
15090
|
+
llm: summaryLlm,
|
|
15091
|
+
logger,
|
|
15092
|
+
metadata: {
|
|
15093
|
+
command: 'review',
|
|
15094
|
+
provider,
|
|
15095
|
+
model: String(summaryService.model),
|
|
15096
|
+
},
|
|
15097
|
+
},
|
|
13701
15098
|
});
|
|
13702
15099
|
return [branchChanges];
|
|
13703
15100
|
}
|
|
@@ -13719,19 +15116,49 @@ const handler = async (argv, logger) => {
|
|
|
13719
15116
|
const unstagedChanges = await fileChangeParser({
|
|
13720
15117
|
changes: unstaged || [],
|
|
13721
15118
|
commit: '--unstaged',
|
|
13722
|
-
options: {
|
|
15119
|
+
options: {
|
|
15120
|
+
tokenizer,
|
|
15121
|
+
git,
|
|
15122
|
+
llm: summaryLlm,
|
|
15123
|
+
logger,
|
|
15124
|
+
metadata: {
|
|
15125
|
+
command: 'review',
|
|
15126
|
+
provider,
|
|
15127
|
+
model: String(summaryService.model),
|
|
15128
|
+
},
|
|
15129
|
+
},
|
|
13723
15130
|
});
|
|
13724
15131
|
const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
|
|
13725
15132
|
const untrackedChanges = await fileChangeParser({
|
|
13726
15133
|
changes: untracked || [],
|
|
13727
15134
|
commit: '--untracked',
|
|
13728
|
-
options: {
|
|
15135
|
+
options: {
|
|
15136
|
+
tokenizer,
|
|
15137
|
+
git,
|
|
15138
|
+
llm: summaryLlm,
|
|
15139
|
+
logger,
|
|
15140
|
+
metadata: {
|
|
15141
|
+
command: 'review',
|
|
15142
|
+
provider,
|
|
15143
|
+
model: String(summaryService.model),
|
|
15144
|
+
},
|
|
15145
|
+
},
|
|
13729
15146
|
});
|
|
13730
15147
|
const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
|
|
13731
15148
|
const stagedChanges = await fileChangeParser({
|
|
13732
15149
|
changes: staged,
|
|
13733
15150
|
commit: '--staged',
|
|
13734
|
-
options: {
|
|
15151
|
+
options: {
|
|
15152
|
+
tokenizer,
|
|
15153
|
+
git,
|
|
15154
|
+
llm: summaryLlm,
|
|
15155
|
+
logger,
|
|
15156
|
+
metadata: {
|
|
15157
|
+
command: 'review',
|
|
15158
|
+
provider,
|
|
15159
|
+
model: String(summaryService.model),
|
|
15160
|
+
},
|
|
15161
|
+
},
|
|
13735
15162
|
});
|
|
13736
15163
|
const stagedResponse = `Staged changes:\n${stagedChanges}`;
|
|
13737
15164
|
return [unstagedResponse, untrackedResponse, stagedResponse];
|
|
@@ -13771,14 +15198,33 @@ const handler = async (argv, logger) => {
|
|
|
13771
15198
|
variables: REVIEW_PROMPT.inputVariables,
|
|
13772
15199
|
fallback: REVIEW_PROMPT,
|
|
13773
15200
|
});
|
|
15201
|
+
const variables = {
|
|
15202
|
+
changes: context,
|
|
15203
|
+
format_instructions: formatInstructions,
|
|
15204
|
+
};
|
|
15205
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
15206
|
+
prompt,
|
|
15207
|
+
variables,
|
|
15208
|
+
tokenizer,
|
|
15209
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
15210
|
+
summaryKey: 'changes',
|
|
15211
|
+
});
|
|
15212
|
+
if (budgetedPrompt.truncated) {
|
|
15213
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
15214
|
+
}
|
|
13774
15215
|
const response = await executeChain({
|
|
13775
15216
|
llm,
|
|
13776
15217
|
prompt,
|
|
13777
|
-
variables:
|
|
13778
|
-
changes: context,
|
|
13779
|
-
format_instructions: formatInstructions,
|
|
13780
|
-
},
|
|
15218
|
+
variables: budgetedPrompt.variables,
|
|
13781
15219
|
parser,
|
|
15220
|
+
logger,
|
|
15221
|
+
tokenizer,
|
|
15222
|
+
metadata: {
|
|
15223
|
+
task: argv.branch ? 'review-branch' : 'review',
|
|
15224
|
+
command: 'review',
|
|
15225
|
+
provider,
|
|
15226
|
+
model: String(model),
|
|
15227
|
+
},
|
|
13782
15228
|
});
|
|
13783
15229
|
// sort by severity
|
|
13784
15230
|
return response.sort((a, b) => b.severity - a.severity);
|
|
@@ -13797,6 +15243,7 @@ const handler = async (argv, logger) => {
|
|
|
13797
15243
|
},
|
|
13798
15244
|
});
|
|
13799
15245
|
const reviewer = new TaskList(recap, { ...config, apiKey: key ?? undefined });
|
|
15246
|
+
logLlmTelemetrySummary(logger, 'review');
|
|
13800
15247
|
await reviewer.start();
|
|
13801
15248
|
};
|
|
13802
15249
|
|
|
@@ -13817,6 +15264,7 @@ y.command(changelog.command, changelog.desc, changelog.builder, changelog.handle
|
|
|
13817
15264
|
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
13818
15265
|
y.command(review.command, review.desc, review.builder, review.handler);
|
|
13819
15266
|
y.command(init.command, init.desc, init.builder, init.handler);
|
|
15267
|
+
y.command(log.command, log.desc, log.builder, log.handler);
|
|
13820
15268
|
y.help().parse(process.argv.slice(2));
|
|
13821
15269
|
|
|
13822
15270
|
/**
|
|
@@ -14268,4 +15716,4 @@ var commitValidationHandler = /*#__PURE__*/Object.freeze({
|
|
|
14268
15716
|
handleValidationErrors: handleValidationErrors
|
|
14269
15717
|
});
|
|
14270
15718
|
|
|
14271
|
-
export { changelog, commit, init, recap, types };
|
|
15719
|
+
export { changelog, commit, init, log, recap, types };
|