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.js
CHANGED
|
@@ -18,6 +18,7 @@ var anthropic = require('@langchain/anthropic');
|
|
|
18
18
|
var ollama = require('@langchain/ollama');
|
|
19
19
|
var openai = require('@langchain/openai');
|
|
20
20
|
var output_parsers = require('@langchain/core/output_parsers');
|
|
21
|
+
var minimatch = require('minimatch');
|
|
21
22
|
var simpleGit = require('simple-git');
|
|
22
23
|
var documents = require('@langchain/core/documents');
|
|
23
24
|
var diff = require('diff');
|
|
@@ -34,7 +35,6 @@ require('@langchain/core/utils/json_schema');
|
|
|
34
35
|
require('@langchain/core/utils/json_patch');
|
|
35
36
|
require('@langchain/core/utils/env');
|
|
36
37
|
require('@langchain/core/utils/async_caller');
|
|
37
|
-
var minimatch = require('minimatch');
|
|
38
38
|
var tiktoken = require('tiktoken');
|
|
39
39
|
var child_process = require('child_process');
|
|
40
40
|
var readline = require('readline');
|
|
@@ -68,7 +68,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
68
68
|
/**
|
|
69
69
|
* Current build version from package.json
|
|
70
70
|
*/
|
|
71
|
-
const BUILD_VERSION = "0.
|
|
71
|
+
const BUILD_VERSION = "0.33.0";
|
|
72
72
|
|
|
73
73
|
const isInteractive = (config) => {
|
|
74
74
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -468,7 +468,7 @@ function getDefaultServiceConfigFromAlias(provider, model) {
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
const DEFAULT_IGNORED_FILES = [
|
|
471
|
+
const DEFAULT_IGNORED_FILES$1 = [
|
|
472
472
|
'package-lock.json',
|
|
473
473
|
'yarn.lock',
|
|
474
474
|
'pnpm-lock.yaml',
|
|
@@ -476,7 +476,7 @@ const DEFAULT_IGNORED_FILES = [
|
|
|
476
476
|
'bun.lock',
|
|
477
477
|
'node_modules',
|
|
478
478
|
];
|
|
479
|
-
const DEFAULT_IGNORED_EXTENSIONS = ['.map', '.lock'];
|
|
479
|
+
const DEFAULT_IGNORED_EXTENSIONS$1 = ['.map', '.lock'];
|
|
480
480
|
const COCO_CONFIG_START_COMMENT = '# -- start coco config --';
|
|
481
481
|
const COCO_CONFIG_END_COMMENT = '# -- end coco config --';
|
|
482
482
|
/**
|
|
@@ -490,8 +490,8 @@ const DEFAULT_CONFIG = {
|
|
|
490
490
|
defaultBranch: 'main',
|
|
491
491
|
service: getDefaultServiceConfigFromAlias('openai'),
|
|
492
492
|
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
493
|
-
ignoredFiles: DEFAULT_IGNORED_FILES,
|
|
494
|
-
ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS,
|
|
493
|
+
ignoredFiles: DEFAULT_IGNORED_FILES$1,
|
|
494
|
+
ignoredExtensions: DEFAULT_IGNORED_EXTENSIONS$1,
|
|
495
495
|
};
|
|
496
496
|
/**
|
|
497
497
|
* Create a named export of all config keys for use in other modules.
|
|
@@ -523,6 +523,8 @@ function loadEnvConfig(config) {
|
|
|
523
523
|
'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT',
|
|
524
524
|
'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES',
|
|
525
525
|
'COCO_SERVICE_FIELDS',
|
|
526
|
+
'COCO_SERVICE_DYNAMIC_MODELS',
|
|
527
|
+
'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE',
|
|
526
528
|
];
|
|
527
529
|
envKeys.forEach((key) => {
|
|
528
530
|
const envVarName = toEnvVarName(key);
|
|
@@ -537,7 +539,9 @@ function loadEnvConfig(config) {
|
|
|
537
539
|
key === 'COCO_SERVICE_ENDPOINT' ||
|
|
538
540
|
key === 'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT' ||
|
|
539
541
|
key === 'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES' ||
|
|
540
|
-
key === 'COCO_SERVICE_FIELDS'
|
|
542
|
+
key === 'COCO_SERVICE_FIELDS' ||
|
|
543
|
+
key === 'COCO_SERVICE_DYNAMIC_MODELS' ||
|
|
544
|
+
key === 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE') {
|
|
541
545
|
// NOTE: We want to ensure that the service object is always defined
|
|
542
546
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
543
547
|
// @ts-ignore
|
|
@@ -592,6 +596,12 @@ function handleServiceEnvVar(service, key, value) {
|
|
|
592
596
|
case 'COCO_SERVICE_FIELDS':
|
|
593
597
|
service.fields = value;
|
|
594
598
|
break;
|
|
599
|
+
case 'COCO_SERVICE_DYNAMIC_MODELS':
|
|
600
|
+
service.dynamicModels = value;
|
|
601
|
+
break;
|
|
602
|
+
case 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE':
|
|
603
|
+
service.dynamicModelPreference = value;
|
|
604
|
+
break;
|
|
595
605
|
}
|
|
596
606
|
}
|
|
597
607
|
function parseEnvValue(key, value) {
|
|
@@ -1000,7 +1010,7 @@ const schema$1 = {
|
|
|
1000
1010
|
"$ref": "#/definitions/LLMProvider"
|
|
1001
1011
|
},
|
|
1002
1012
|
"model": {
|
|
1003
|
-
"$ref": "#/definitions/
|
|
1013
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
1004
1014
|
},
|
|
1005
1015
|
"baseURL": {
|
|
1006
1016
|
"type": "string",
|
|
@@ -1692,6 +1702,15 @@ const schema$1 = {
|
|
|
1692
1702
|
"type": "number",
|
|
1693
1703
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
1694
1704
|
"default": 3
|
|
1705
|
+
},
|
|
1706
|
+
"dynamicModels": {
|
|
1707
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
1708
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
1709
|
+
},
|
|
1710
|
+
"dynamicModelPreference": {
|
|
1711
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
1712
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
1713
|
+
"default": "balanced"
|
|
1695
1714
|
}
|
|
1696
1715
|
},
|
|
1697
1716
|
"required": [
|
|
@@ -1708,6 +1727,17 @@ const schema$1 = {
|
|
|
1708
1727
|
"anthropic"
|
|
1709
1728
|
]
|
|
1710
1729
|
},
|
|
1730
|
+
"ConfiguredLLMModel": {
|
|
1731
|
+
"anyOf": [
|
|
1732
|
+
{
|
|
1733
|
+
"$ref": "#/definitions/LLMModel"
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
"type": "string",
|
|
1737
|
+
"const": "dynamic"
|
|
1738
|
+
}
|
|
1739
|
+
]
|
|
1740
|
+
},
|
|
1711
1741
|
"LLMModel": {
|
|
1712
1742
|
"anyOf": [
|
|
1713
1743
|
{
|
|
@@ -2005,6 +2035,41 @@ const schema$1 = {
|
|
|
2005
2035
|
null
|
|
2006
2036
|
]
|
|
2007
2037
|
},
|
|
2038
|
+
"DynamicModelProfile": {
|
|
2039
|
+
"type": "object",
|
|
2040
|
+
"properties": {
|
|
2041
|
+
"summarize": {
|
|
2042
|
+
"$ref": "#/definitions/LLMModel"
|
|
2043
|
+
},
|
|
2044
|
+
"commit": {
|
|
2045
|
+
"$ref": "#/definitions/LLMModel"
|
|
2046
|
+
},
|
|
2047
|
+
"changelog": {
|
|
2048
|
+
"$ref": "#/definitions/LLMModel"
|
|
2049
|
+
},
|
|
2050
|
+
"review": {
|
|
2051
|
+
"$ref": "#/definitions/LLMModel"
|
|
2052
|
+
},
|
|
2053
|
+
"recap": {
|
|
2054
|
+
"$ref": "#/definitions/LLMModel"
|
|
2055
|
+
},
|
|
2056
|
+
"repair": {
|
|
2057
|
+
"$ref": "#/definitions/LLMModel"
|
|
2058
|
+
},
|
|
2059
|
+
"largeDiff": {
|
|
2060
|
+
"$ref": "#/definitions/LLMModel"
|
|
2061
|
+
}
|
|
2062
|
+
},
|
|
2063
|
+
"additionalProperties": false
|
|
2064
|
+
},
|
|
2065
|
+
"DynamicModelPreference": {
|
|
2066
|
+
"type": "string",
|
|
2067
|
+
"enum": [
|
|
2068
|
+
"cost",
|
|
2069
|
+
"balanced",
|
|
2070
|
+
"quality"
|
|
2071
|
+
]
|
|
2072
|
+
},
|
|
2008
2073
|
"OllamaLLMService": {
|
|
2009
2074
|
"type": "object",
|
|
2010
2075
|
"additionalProperties": false,
|
|
@@ -2013,7 +2078,7 @@ const schema$1 = {
|
|
|
2013
2078
|
"$ref": "#/definitions/LLMProvider"
|
|
2014
2079
|
},
|
|
2015
2080
|
"model": {
|
|
2016
|
-
"$ref": "#/definitions/
|
|
2081
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
2017
2082
|
},
|
|
2018
2083
|
"endpoint": {
|
|
2019
2084
|
"type": "string"
|
|
@@ -2733,6 +2798,15 @@ const schema$1 = {
|
|
|
2733
2798
|
"type": "number",
|
|
2734
2799
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
2735
2800
|
"default": 3
|
|
2801
|
+
},
|
|
2802
|
+
"dynamicModels": {
|
|
2803
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
2804
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
2805
|
+
},
|
|
2806
|
+
"dynamicModelPreference": {
|
|
2807
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
2808
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
2809
|
+
"default": "balanced"
|
|
2736
2810
|
}
|
|
2737
2811
|
},
|
|
2738
2812
|
"required": [
|
|
@@ -2755,7 +2829,7 @@ const schema$1 = {
|
|
|
2755
2829
|
"$ref": "#/definitions/LLMProvider"
|
|
2756
2830
|
},
|
|
2757
2831
|
"model": {
|
|
2758
|
-
"$ref": "#/definitions/
|
|
2832
|
+
"$ref": "#/definitions/ConfiguredLLMModel"
|
|
2759
2833
|
},
|
|
2760
2834
|
"fields": {
|
|
2761
2835
|
"type": "object",
|
|
@@ -2885,6 +2959,15 @@ const schema$1 = {
|
|
|
2885
2959
|
"type": "number",
|
|
2886
2960
|
"description": "The maximum number of attempts for schema parsing with retry logic.",
|
|
2887
2961
|
"default": 3
|
|
2962
|
+
},
|
|
2963
|
+
"dynamicModels": {
|
|
2964
|
+
"$ref": "#/definitions/DynamicModelProfile",
|
|
2965
|
+
"description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
|
|
2966
|
+
},
|
|
2967
|
+
"dynamicModelPreference": {
|
|
2968
|
+
"$ref": "#/definitions/DynamicModelPreference",
|
|
2969
|
+
"description": "Default dynamic routing preference when model is set to \"dynamic\".",
|
|
2970
|
+
"default": "balanced"
|
|
2888
2971
|
}
|
|
2889
2972
|
},
|
|
2890
2973
|
"required": [
|
|
@@ -6951,11 +7034,11 @@ const ChangelogResponseSchema = objectType({
|
|
|
6951
7034
|
title: stringType(),
|
|
6952
7035
|
content: stringType(),
|
|
6953
7036
|
});
|
|
6954
|
-
const command$
|
|
7037
|
+
const command$5 = 'changelog';
|
|
6955
7038
|
/**
|
|
6956
7039
|
* Command line options via yargs
|
|
6957
7040
|
*/
|
|
6958
|
-
const options$
|
|
7041
|
+
const options$5 = {
|
|
6959
7042
|
range: {
|
|
6960
7043
|
type: 'string',
|
|
6961
7044
|
alias: 'r',
|
|
@@ -7002,8 +7085,8 @@ const options$4 = {
|
|
|
7002
7085
|
description: 'Toggle interactive mode',
|
|
7003
7086
|
},
|
|
7004
7087
|
};
|
|
7005
|
-
const builder$
|
|
7006
|
-
return yargs.options(options$
|
|
7088
|
+
const builder$5 = (yargs) => {
|
|
7089
|
+
return yargs.options(options$5).usage(getCommandUsageHeader(command$5));
|
|
7007
7090
|
};
|
|
7008
7091
|
|
|
7009
7092
|
/**
|
|
@@ -7203,6 +7286,212 @@ function getLlm(provider, model, config) {
|
|
|
7203
7286
|
}
|
|
7204
7287
|
}
|
|
7205
7288
|
|
|
7289
|
+
const OPENAI_DYNAMIC_DEFAULTS = {
|
|
7290
|
+
cost: {
|
|
7291
|
+
summarize: 'gpt-4.1-nano',
|
|
7292
|
+
commit: 'gpt-4.1-mini',
|
|
7293
|
+
changelog: 'gpt-4.1-mini',
|
|
7294
|
+
review: 'gpt-4.1-mini',
|
|
7295
|
+
recap: 'gpt-4.1-nano',
|
|
7296
|
+
repair: 'gpt-4.1-mini',
|
|
7297
|
+
largeDiff: 'gpt-4.1',
|
|
7298
|
+
},
|
|
7299
|
+
balanced: {
|
|
7300
|
+
summarize: 'gpt-4.1-mini',
|
|
7301
|
+
commit: 'gpt-4.1-mini',
|
|
7302
|
+
changelog: 'gpt-4.1',
|
|
7303
|
+
review: 'gpt-4.1',
|
|
7304
|
+
recap: 'gpt-4.1-mini',
|
|
7305
|
+
repair: 'gpt-4.1',
|
|
7306
|
+
largeDiff: 'gpt-4.1',
|
|
7307
|
+
},
|
|
7308
|
+
quality: {
|
|
7309
|
+
summarize: 'gpt-4.1-mini',
|
|
7310
|
+
commit: 'gpt-4.1',
|
|
7311
|
+
changelog: 'gpt-4.1',
|
|
7312
|
+
review: 'gpt-4.1',
|
|
7313
|
+
recap: 'gpt-4.1',
|
|
7314
|
+
repair: 'gpt-4.1',
|
|
7315
|
+
largeDiff: 'gpt-4.1',
|
|
7316
|
+
},
|
|
7317
|
+
};
|
|
7318
|
+
const ANTHROPIC_DYNAMIC_DEFAULTS = {
|
|
7319
|
+
cost: {
|
|
7320
|
+
summarize: 'claude-3-5-haiku-latest',
|
|
7321
|
+
commit: 'claude-3-5-haiku-latest',
|
|
7322
|
+
changelog: 'claude-3-5-sonnet-latest',
|
|
7323
|
+
review: 'claude-3-5-sonnet-latest',
|
|
7324
|
+
recap: 'claude-3-5-haiku-latest',
|
|
7325
|
+
repair: 'claude-3-5-sonnet-latest',
|
|
7326
|
+
largeDiff: 'claude-3-5-sonnet-latest',
|
|
7327
|
+
},
|
|
7328
|
+
balanced: {
|
|
7329
|
+
summarize: 'claude-3-5-haiku-latest',
|
|
7330
|
+
commit: 'claude-3-5-sonnet-latest',
|
|
7331
|
+
changelog: 'claude-3-5-sonnet-latest',
|
|
7332
|
+
review: 'claude-3-7-sonnet-latest',
|
|
7333
|
+
recap: 'claude-3-5-sonnet-latest',
|
|
7334
|
+
repair: 'claude-3-7-sonnet-latest',
|
|
7335
|
+
largeDiff: 'claude-3-7-sonnet-latest',
|
|
7336
|
+
},
|
|
7337
|
+
quality: {
|
|
7338
|
+
summarize: 'claude-3-5-sonnet-latest',
|
|
7339
|
+
commit: 'claude-3-7-sonnet-latest',
|
|
7340
|
+
changelog: 'claude-3-7-sonnet-latest',
|
|
7341
|
+
review: 'claude-sonnet-4-0',
|
|
7342
|
+
recap: 'claude-3-7-sonnet-latest',
|
|
7343
|
+
repair: 'claude-sonnet-4-0',
|
|
7344
|
+
largeDiff: 'claude-sonnet-4-0',
|
|
7345
|
+
},
|
|
7346
|
+
};
|
|
7347
|
+
const OLLAMA_DYNAMIC_DEFAULTS = {
|
|
7348
|
+
cost: {
|
|
7349
|
+
summarize: 'llama3.2:3b',
|
|
7350
|
+
commit: 'llama3.1:8b',
|
|
7351
|
+
changelog: 'llama3.1:8b',
|
|
7352
|
+
review: 'qwen2.5-coder:7b',
|
|
7353
|
+
recap: 'llama3.2:3b',
|
|
7354
|
+
repair: 'qwen2.5-coder:7b',
|
|
7355
|
+
largeDiff: 'qwen2.5-coder:14b',
|
|
7356
|
+
},
|
|
7357
|
+
balanced: {
|
|
7358
|
+
summarize: 'llama3.1:8b',
|
|
7359
|
+
commit: 'qwen2.5-coder:14b',
|
|
7360
|
+
changelog: 'qwen2.5-coder:14b',
|
|
7361
|
+
review: 'qwen2.5-coder:32b',
|
|
7362
|
+
recap: 'llama3.1:8b',
|
|
7363
|
+
repair: 'qwen2.5-coder:32b',
|
|
7364
|
+
largeDiff: 'qwen2.5-coder:32b',
|
|
7365
|
+
},
|
|
7366
|
+
quality: {
|
|
7367
|
+
summarize: 'qwen2.5-coder:14b',
|
|
7368
|
+
commit: 'qwen2.5-coder:32b',
|
|
7369
|
+
changelog: 'qwen2.5-coder:32b',
|
|
7370
|
+
review: 'qwen2.5-coder:32b',
|
|
7371
|
+
recap: 'qwen2.5-coder:14b',
|
|
7372
|
+
repair: 'qwen2.5-coder:32b',
|
|
7373
|
+
largeDiff: 'qwen2.5-coder:32b',
|
|
7374
|
+
},
|
|
7375
|
+
};
|
|
7376
|
+
const DYNAMIC_DEFAULTS = {
|
|
7377
|
+
openai: OPENAI_DYNAMIC_DEFAULTS,
|
|
7378
|
+
anthropic: ANTHROPIC_DYNAMIC_DEFAULTS,
|
|
7379
|
+
ollama: OLLAMA_DYNAMIC_DEFAULTS,
|
|
7380
|
+
};
|
|
7381
|
+
const DYNAMIC_MODEL_TASKS = [
|
|
7382
|
+
'summarize',
|
|
7383
|
+
'commit',
|
|
7384
|
+
'changelog',
|
|
7385
|
+
'review',
|
|
7386
|
+
'recap',
|
|
7387
|
+
'repair',
|
|
7388
|
+
'largeDiff',
|
|
7389
|
+
];
|
|
7390
|
+
function validateDynamicModelProfile(service) {
|
|
7391
|
+
const dynamicModels = service.dynamicModels;
|
|
7392
|
+
if (!dynamicModels)
|
|
7393
|
+
return;
|
|
7394
|
+
const unknownTasks = Object.keys(dynamicModels).filter((task) => !DYNAMIC_MODEL_TASKS.includes(task));
|
|
7395
|
+
if (unknownTasks.length > 0) {
|
|
7396
|
+
throw new LangChainConfigurationError(`Unknown dynamic model task(s): ${unknownTasks.join(', ')}. Supported tasks: ${DYNAMIC_MODEL_TASKS.join(', ')}`, { unknownTasks, supportedTasks: DYNAMIC_MODEL_TASKS });
|
|
7397
|
+
}
|
|
7398
|
+
Object.entries(dynamicModels).forEach(([task, model]) => {
|
|
7399
|
+
if (typeof model !== 'string' || model.trim() === '' || model === 'dynamic') {
|
|
7400
|
+
throw new LangChainConfigurationError(`Dynamic model override for '${task}' must be a concrete model name`, { task, model });
|
|
7401
|
+
}
|
|
7402
|
+
});
|
|
7403
|
+
}
|
|
7404
|
+
function resolveDynamicModel(config, task) {
|
|
7405
|
+
const service = config.service;
|
|
7406
|
+
validateDynamicModelProfile(service);
|
|
7407
|
+
if (service.model !== 'dynamic') {
|
|
7408
|
+
return service.model;
|
|
7409
|
+
}
|
|
7410
|
+
const preference = service.dynamicModelPreference || 'balanced';
|
|
7411
|
+
const providerDefaults = DYNAMIC_DEFAULTS[service.provider];
|
|
7412
|
+
const defaultModel = providerDefaults[preference]?.[task];
|
|
7413
|
+
return service.dynamicModels?.[task] || defaultModel;
|
|
7414
|
+
}
|
|
7415
|
+
function resolveDynamicService(config, task) {
|
|
7416
|
+
const model = resolveDynamicModel(config, task);
|
|
7417
|
+
return {
|
|
7418
|
+
...config.service,
|
|
7419
|
+
model,
|
|
7420
|
+
};
|
|
7421
|
+
}
|
|
7422
|
+
|
|
7423
|
+
const telemetryByCommand = new Map();
|
|
7424
|
+
function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
7425
|
+
if (!tokenizer)
|
|
7426
|
+
return undefined;
|
|
7427
|
+
try {
|
|
7428
|
+
return tokenizer(renderedPrompt);
|
|
7429
|
+
}
|
|
7430
|
+
catch {
|
|
7431
|
+
return undefined;
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
function logLlmCall(logger, metadata) {
|
|
7435
|
+
if (!logger)
|
|
7436
|
+
return;
|
|
7437
|
+
recordLlmTelemetry(metadata);
|
|
7438
|
+
const fields = [
|
|
7439
|
+
`task=${metadata.task}`,
|
|
7440
|
+
metadata.command ? `command=${metadata.command}` : undefined,
|
|
7441
|
+
metadata.provider ? `provider=${metadata.provider}` : undefined,
|
|
7442
|
+
metadata.model ? `model=${metadata.model}` : undefined,
|
|
7443
|
+
metadata.retryAttempt ? `retryAttempt=${metadata.retryAttempt}` : undefined,
|
|
7444
|
+
metadata.promptTokens !== undefined ? `promptTokens=${metadata.promptTokens}` : undefined,
|
|
7445
|
+
metadata.elapsedMs !== undefined ? `elapsedMs=${metadata.elapsedMs}` : undefined,
|
|
7446
|
+
metadata.inputDocuments !== undefined ? `inputDocuments=${metadata.inputDocuments}` : undefined,
|
|
7447
|
+
metadata.inputChunks !== undefined ? `inputChunks=${metadata.inputChunks}` : undefined,
|
|
7448
|
+
metadata.parserType ? `parser=${metadata.parserType}` : undefined,
|
|
7449
|
+
metadata.variableKeys?.length ? `variableKeys=${metadata.variableKeys.join(',')}` : undefined,
|
|
7450
|
+
].filter(Boolean);
|
|
7451
|
+
logger.verbose(`[llm] ${fields.join(' ')}`, { color: 'cyan' });
|
|
7452
|
+
}
|
|
7453
|
+
function recordLlmTelemetry(metadata) {
|
|
7454
|
+
const command = metadata.command || 'unknown';
|
|
7455
|
+
const current = telemetryByCommand.get(command) || {
|
|
7456
|
+
calls: 0,
|
|
7457
|
+
promptTokens: 0,
|
|
7458
|
+
elapsedMs: 0,
|
|
7459
|
+
inputDocuments: 0,
|
|
7460
|
+
inputChunks: 0,
|
|
7461
|
+
tasks: new Set(),
|
|
7462
|
+
models: new Set(),
|
|
7463
|
+
};
|
|
7464
|
+
current.calls += 1;
|
|
7465
|
+
current.promptTokens += metadata.promptTokens || 0;
|
|
7466
|
+
current.elapsedMs += metadata.elapsedMs || 0;
|
|
7467
|
+
current.inputDocuments += metadata.inputDocuments || 0;
|
|
7468
|
+
current.inputChunks += metadata.inputChunks || 0;
|
|
7469
|
+
current.tasks.add(metadata.task);
|
|
7470
|
+
if (metadata.model) {
|
|
7471
|
+
current.models.add(metadata.model);
|
|
7472
|
+
}
|
|
7473
|
+
telemetryByCommand.set(command, current);
|
|
7474
|
+
}
|
|
7475
|
+
function logLlmTelemetrySummary(logger, command) {
|
|
7476
|
+
if (!logger)
|
|
7477
|
+
return;
|
|
7478
|
+
const summary = telemetryByCommand.get(command);
|
|
7479
|
+
if (!summary || summary.calls === 0)
|
|
7480
|
+
return;
|
|
7481
|
+
const fields = [
|
|
7482
|
+
`command=${command}`,
|
|
7483
|
+
`calls=${summary.calls}`,
|
|
7484
|
+
summary.promptTokens > 0 ? `promptTokens=${summary.promptTokens}` : undefined,
|
|
7485
|
+
summary.elapsedMs > 0 ? `elapsedMs=${summary.elapsedMs}` : undefined,
|
|
7486
|
+
summary.inputDocuments > 0 ? `inputDocuments=${summary.inputDocuments}` : undefined,
|
|
7487
|
+
summary.inputChunks > 0 ? `inputChunks=${summary.inputChunks}` : undefined,
|
|
7488
|
+
summary.tasks.size > 0 ? `tasks=${[...summary.tasks].join(',')}` : undefined,
|
|
7489
|
+
summary.models.size > 0 ? `models=${[...summary.models].join(',')}` : undefined,
|
|
7490
|
+
].filter(Boolean);
|
|
7491
|
+
logger.verbose(`[llm:summary] ${fields.join(' ')}`, { color: 'cyan' });
|
|
7492
|
+
telemetryByCommand.delete(command);
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7206
7495
|
/**
|
|
7207
7496
|
* Creates a PromptTemplate from a template string or returns a fallback template.
|
|
7208
7497
|
*
|
|
@@ -7291,6 +7580,75 @@ function createSchemaParser(schema, llm, options = {}
|
|
|
7291
7580
|
}
|
|
7292
7581
|
}
|
|
7293
7582
|
|
|
7583
|
+
async function renderPrompt(prompt, variables) {
|
|
7584
|
+
if (typeof prompt.format === 'function') {
|
|
7585
|
+
return await prompt.format(variables);
|
|
7586
|
+
}
|
|
7587
|
+
if (typeof prompt.template === 'string') {
|
|
7588
|
+
return Object.entries(variables).reduce((result, [key, value]) => {
|
|
7589
|
+
return result
|
|
7590
|
+
.replaceAll(`{{${key}}}`, value)
|
|
7591
|
+
.replaceAll(`{${key}}`, value);
|
|
7592
|
+
}, prompt.template);
|
|
7593
|
+
}
|
|
7594
|
+
throw new Error('Prompt must provide either a format function or template string');
|
|
7595
|
+
}
|
|
7596
|
+
/**
|
|
7597
|
+
* Ensure the fully rendered LLM prompt fits the configured request budget.
|
|
7598
|
+
*
|
|
7599
|
+
* Diff condensation budgets only cover the diff summary itself. This guard accounts
|
|
7600
|
+
* for the rest of the rendered prompt, then trims the summary as a deterministic
|
|
7601
|
+
* fallback when additional context pushes the request over budget.
|
|
7602
|
+
*/
|
|
7603
|
+
async function enforcePromptBudget({ prompt, variables, tokenizer, maxTokens, summaryKey = 'summary', responseTokenReserve = 512, }) {
|
|
7604
|
+
const renderedPrompt = await renderPrompt(prompt, variables);
|
|
7605
|
+
const promptTokenCount = tokenizer(renderedPrompt);
|
|
7606
|
+
if (promptTokenCount <= maxTokens) {
|
|
7607
|
+
return { variables, promptTokenCount, truncated: false };
|
|
7608
|
+
}
|
|
7609
|
+
const summary = variables[summaryKey] || '';
|
|
7610
|
+
const variablesWithoutSummary = { ...variables, [summaryKey]: '' };
|
|
7611
|
+
const overheadTokenCount = tokenizer(await renderPrompt(prompt, variablesWithoutSummary));
|
|
7612
|
+
const summaryBudget = Math.max(0, maxTokens - overheadTokenCount - responseTokenReserve);
|
|
7613
|
+
if (summaryBudget === 0) {
|
|
7614
|
+
const emptySummaryVariables = { ...variables, [summaryKey]: '' };
|
|
7615
|
+
const emptySummaryTokenCount = tokenizer(await renderPrompt(prompt, emptySummaryVariables));
|
|
7616
|
+
if (emptySummaryTokenCount > maxTokens) {
|
|
7617
|
+
throw new Error(`Rendered prompt exceeds token budget before adding ${summaryKey}: ` +
|
|
7618
|
+
`${emptySummaryTokenCount} > ${maxTokens}`);
|
|
7619
|
+
}
|
|
7620
|
+
return {
|
|
7621
|
+
variables: emptySummaryVariables,
|
|
7622
|
+
promptTokenCount: emptySummaryTokenCount,
|
|
7623
|
+
truncated: true,
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
7626
|
+
let low = 0;
|
|
7627
|
+
let high = summary.length;
|
|
7628
|
+
let bestSummary = '';
|
|
7629
|
+
let bestTokenCount = overheadTokenCount;
|
|
7630
|
+
while (low <= high) {
|
|
7631
|
+
const mid = Math.floor((low + high) / 2);
|
|
7632
|
+
const candidateSummary = summary.slice(0, mid);
|
|
7633
|
+
const candidateVariables = { ...variables, [summaryKey]: candidateSummary };
|
|
7634
|
+
const candidateTokenCount = tokenizer(await renderPrompt(prompt, candidateVariables));
|
|
7635
|
+
if (candidateTokenCount <= maxTokens - responseTokenReserve) {
|
|
7636
|
+
bestSummary = candidateSummary;
|
|
7637
|
+
bestTokenCount = candidateTokenCount;
|
|
7638
|
+
low = mid + 1;
|
|
7639
|
+
}
|
|
7640
|
+
else {
|
|
7641
|
+
high = mid - 1;
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
const trimmedVariables = { ...variables, [summaryKey]: bestSummary.trimEnd() };
|
|
7645
|
+
return {
|
|
7646
|
+
variables: trimmedVariables,
|
|
7647
|
+
promptTokenCount: bestTokenCount,
|
|
7648
|
+
truncated: true,
|
|
7649
|
+
};
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7294
7652
|
/**
|
|
7295
7653
|
* Extracts provider and endpoint info from LLM instance if available
|
|
7296
7654
|
*/
|
|
@@ -7323,7 +7681,7 @@ function extractLlmInfo(llm) {
|
|
|
7323
7681
|
* @throws LangChainExecutionError if the chain execution fails or returns empty results
|
|
7324
7682
|
* @throws LangChainNetworkError if a network/connection error occurs
|
|
7325
7683
|
*/
|
|
7326
|
-
const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, }) => {
|
|
7684
|
+
const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, logger, tokenizer, metadata, }) => {
|
|
7327
7685
|
validateRequired(llm, 'llm', 'executeChain');
|
|
7328
7686
|
validateRequired(prompt, 'prompt', 'executeChain');
|
|
7329
7687
|
validateRequired(variables, 'variables', 'executeChain');
|
|
@@ -7341,8 +7699,21 @@ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint
|
|
|
7341
7699
|
const effectiveProvider = provider || llmInfo.provider;
|
|
7342
7700
|
const effectiveEndpoint = endpoint || llmInfo.endpoint;
|
|
7343
7701
|
try {
|
|
7702
|
+
const renderedPrompt = await prompt.format(variables);
|
|
7703
|
+
const promptTokens = estimatePromptTokens(tokenizer, renderedPrompt);
|
|
7344
7704
|
const chain = prompt.pipe(llm).pipe(parser);
|
|
7705
|
+
const startedAt = Date.now();
|
|
7345
7706
|
const result = (await chain.invoke(variables));
|
|
7707
|
+
const elapsedMs = Date.now() - startedAt;
|
|
7708
|
+
logLlmCall(logger, {
|
|
7709
|
+
task: metadata?.task || 'chain',
|
|
7710
|
+
provider: effectiveProvider,
|
|
7711
|
+
parserType: parser.constructor.name,
|
|
7712
|
+
variableKeys: Object.keys(variables),
|
|
7713
|
+
promptTokens,
|
|
7714
|
+
elapsedMs,
|
|
7715
|
+
...metadata,
|
|
7716
|
+
});
|
|
7346
7717
|
if (result === null || result === undefined) {
|
|
7347
7718
|
throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
|
|
7348
7719
|
}
|
|
@@ -7580,6 +7951,148 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
|
|
|
7580
7951
|
return [];
|
|
7581
7952
|
}
|
|
7582
7953
|
|
|
7954
|
+
/**
|
|
7955
|
+
* Determines the status of a file based on its changes in the Git repository.
|
|
7956
|
+
*
|
|
7957
|
+
* @param file - The file to check the status of.
|
|
7958
|
+
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
7959
|
+
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
7960
|
+
* @throws Error if the file type is invalid.
|
|
7961
|
+
*/
|
|
7962
|
+
function getStatus(file, location = 'index') {
|
|
7963
|
+
if ('index' in file && 'working_dir' in file) {
|
|
7964
|
+
const statusCode = file[location];
|
|
7965
|
+
switch (statusCode) {
|
|
7966
|
+
case 'A':
|
|
7967
|
+
return 'added';
|
|
7968
|
+
case 'D':
|
|
7969
|
+
return 'deleted';
|
|
7970
|
+
case 'M':
|
|
7971
|
+
return 'modified';
|
|
7972
|
+
case 'R':
|
|
7973
|
+
return 'renamed';
|
|
7974
|
+
case '?':
|
|
7975
|
+
return 'untracked';
|
|
7976
|
+
default:
|
|
7977
|
+
return 'unknown';
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
else if ('binary' in file && file.binary === true) {
|
|
7981
|
+
// DiffResultBinaryFile: has before/after, no changes/insertions/deletions
|
|
7982
|
+
if (file.file.includes('=>'))
|
|
7983
|
+
return 'renamed';
|
|
7984
|
+
if (file.before === 0 && file.after > 0)
|
|
7985
|
+
return 'added';
|
|
7986
|
+
if (file.after === 0 && file.before > 0)
|
|
7987
|
+
return 'deleted';
|
|
7988
|
+
if (file.before > 0 && file.after > 0)
|
|
7989
|
+
return 'modified';
|
|
7990
|
+
return 'untracked';
|
|
7991
|
+
}
|
|
7992
|
+
else if ('changes' in file && 'binary' in file) {
|
|
7993
|
+
// DiffResultTextFile: has changes/insertions/deletions
|
|
7994
|
+
if (file.changes === 0)
|
|
7995
|
+
return 'untracked';
|
|
7996
|
+
if (file.file.includes('=>'))
|
|
7997
|
+
return 'renamed';
|
|
7998
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
7999
|
+
return 'added';
|
|
8000
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
8001
|
+
return 'deleted';
|
|
8002
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
8003
|
+
return 'modified';
|
|
8004
|
+
return 'unknown';
|
|
8005
|
+
}
|
|
8006
|
+
else {
|
|
8007
|
+
throw new Error('Invalid file type');
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
|
|
8011
|
+
/**
|
|
8012
|
+
* Returns the summary text for a file change.
|
|
8013
|
+
*
|
|
8014
|
+
* @param file - The file status or diff result.
|
|
8015
|
+
* @param change - The partial file change object.
|
|
8016
|
+
* @returns The summary text for the file change.
|
|
8017
|
+
* @throws Error if the file type is invalid.
|
|
8018
|
+
*/
|
|
8019
|
+
function getSummaryText(file, change) {
|
|
8020
|
+
const status = change.status || getStatus(file);
|
|
8021
|
+
let filePath;
|
|
8022
|
+
if ('path' in file) {
|
|
8023
|
+
filePath = file.path;
|
|
8024
|
+
}
|
|
8025
|
+
else if ('file' in file) {
|
|
8026
|
+
filePath = change?.filePath || file.file;
|
|
8027
|
+
}
|
|
8028
|
+
else {
|
|
8029
|
+
throw new Error('Invalid file type');
|
|
8030
|
+
}
|
|
8031
|
+
if (change.oldFilePath) {
|
|
8032
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
8033
|
+
}
|
|
8034
|
+
return `${status}: ${filePath}`;
|
|
8035
|
+
}
|
|
8036
|
+
|
|
8037
|
+
/**
|
|
8038
|
+
* Parses a file string and returns the parsed file paths.
|
|
8039
|
+
* If the file string contains a separator, it splits the string into root path, file path, and old file path.
|
|
8040
|
+
* 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.
|
|
8041
|
+
* @param file The file string to parse.
|
|
8042
|
+
* @returns The parsed file paths.
|
|
8043
|
+
*/
|
|
8044
|
+
function parseFileString(file) {
|
|
8045
|
+
const separator = ' => ';
|
|
8046
|
+
if (file.includes(separator)) {
|
|
8047
|
+
const [oldFilePathWithRoot, filePath] = file.split(separator);
|
|
8048
|
+
const [rootPath, oldFilePath] = oldFilePathWithRoot.split('{');
|
|
8049
|
+
return {
|
|
8050
|
+
filePath: rootPath + filePath.trim().replace('{', '').replace('}', ''),
|
|
8051
|
+
oldFilePath: rootPath + oldFilePath.trim().replace('{', '').replace('}', ''),
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
else {
|
|
8055
|
+
return {
|
|
8056
|
+
filePath: file.trim(),
|
|
8057
|
+
oldFilePath: undefined,
|
|
8058
|
+
};
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
|
|
8062
|
+
const config = loadConfig();
|
|
8063
|
+
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
8064
|
+
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
8065
|
+
/**
|
|
8066
|
+
* Retrieves the changes made in a commit.
|
|
8067
|
+
*
|
|
8068
|
+
* @deprecated use `getChanges` instead
|
|
8069
|
+
*
|
|
8070
|
+
* @param commit - The commit hash.
|
|
8071
|
+
* @param options - Optional parameters for customization.
|
|
8072
|
+
* @returns A promise that resolves to an array of FileChange objects representing the changes made in the commit.
|
|
8073
|
+
*/
|
|
8074
|
+
async function getChangesByCommit({ commit, options: { git, ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS, }, }) {
|
|
8075
|
+
const changes = [];
|
|
8076
|
+
const diffSummary = await git.diffSummary([`${commit}^..${commit}`]);
|
|
8077
|
+
diffSummary.files.forEach((file) => {
|
|
8078
|
+
const { filePath, oldFilePath } = parseFileString(file.file);
|
|
8079
|
+
const fileChange = {
|
|
8080
|
+
filePath,
|
|
8081
|
+
oldFilePath,
|
|
8082
|
+
status: getStatus(file),
|
|
8083
|
+
};
|
|
8084
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
8085
|
+
changes.push(fileChange);
|
|
8086
|
+
});
|
|
8087
|
+
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
8088
|
+
const filteredChanges = changes.filter((file) => {
|
|
8089
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
8090
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
8091
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
8092
|
+
});
|
|
8093
|
+
return filteredChanges;
|
|
8094
|
+
}
|
|
8095
|
+
|
|
7583
8096
|
/**
|
|
7584
8097
|
* Retrieves the SimpleGit instance for the repository.
|
|
7585
8098
|
* @returns {SimpleGit} The SimpleGit instance.
|
|
@@ -7941,116 +8454,18 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
|
|
|
7941
8454
|
}
|
|
7942
8455
|
|
|
7943
8456
|
/**
|
|
7944
|
-
*
|
|
8457
|
+
* Retrieves the diff between the current branch and a specified target branch.
|
|
7945
8458
|
*
|
|
7946
|
-
* @param
|
|
7947
|
-
* @
|
|
8459
|
+
* @param {Object} options - The options for retrieving the diff.
|
|
8460
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
8461
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
8462
|
+
* @param {string} options.baseBranch - The base branch to compare against.
|
|
8463
|
+
* @param {string} options.headBranch - The head branch to compare.
|
|
8464
|
+
* @param {string[]} options.ignoredFiles - Array of specific files to ignore.
|
|
8465
|
+
* @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
|
|
8466
|
+
* @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
|
|
7948
8467
|
*/
|
|
7949
|
-
async function
|
|
7950
|
-
try {
|
|
7951
|
-
return await git.diff(['-p', `${commitId}^..${commitId}`]);
|
|
7952
|
-
}
|
|
7953
|
-
catch (error) {
|
|
7954
|
-
throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
|
|
7955
|
-
}
|
|
7956
|
-
}
|
|
7957
|
-
|
|
7958
|
-
/**
|
|
7959
|
-
* Determines the status of a file based on its changes in the Git repository.
|
|
7960
|
-
*
|
|
7961
|
-
* @param file - The file to check the status of.
|
|
7962
|
-
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
7963
|
-
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
7964
|
-
* @throws Error if the file type is invalid.
|
|
7965
|
-
*/
|
|
7966
|
-
function getStatus(file, location = 'index') {
|
|
7967
|
-
if ('index' in file && 'working_dir' in file) {
|
|
7968
|
-
const statusCode = file[location];
|
|
7969
|
-
switch (statusCode) {
|
|
7970
|
-
case 'A':
|
|
7971
|
-
return 'added';
|
|
7972
|
-
case 'D':
|
|
7973
|
-
return 'deleted';
|
|
7974
|
-
case 'M':
|
|
7975
|
-
return 'modified';
|
|
7976
|
-
case 'R':
|
|
7977
|
-
return 'renamed';
|
|
7978
|
-
case '?':
|
|
7979
|
-
return 'untracked';
|
|
7980
|
-
default:
|
|
7981
|
-
return 'unknown';
|
|
7982
|
-
}
|
|
7983
|
-
}
|
|
7984
|
-
else if ('binary' in file && file.binary === true) {
|
|
7985
|
-
// DiffResultBinaryFile: has before/after, no changes/insertions/deletions
|
|
7986
|
-
if (file.file.includes('=>'))
|
|
7987
|
-
return 'renamed';
|
|
7988
|
-
if (file.before === 0 && file.after > 0)
|
|
7989
|
-
return 'added';
|
|
7990
|
-
if (file.after === 0 && file.before > 0)
|
|
7991
|
-
return 'deleted';
|
|
7992
|
-
if (file.before > 0 && file.after > 0)
|
|
7993
|
-
return 'modified';
|
|
7994
|
-
return 'untracked';
|
|
7995
|
-
}
|
|
7996
|
-
else if ('changes' in file && 'binary' in file) {
|
|
7997
|
-
// DiffResultTextFile: has changes/insertions/deletions
|
|
7998
|
-
if (file.changes === 0)
|
|
7999
|
-
return 'untracked';
|
|
8000
|
-
if (file.file.includes('=>'))
|
|
8001
|
-
return 'renamed';
|
|
8002
|
-
if (file.deletions === 0 && file.insertions > 0)
|
|
8003
|
-
return 'added';
|
|
8004
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
8005
|
-
return 'deleted';
|
|
8006
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
8007
|
-
return 'modified';
|
|
8008
|
-
return 'unknown';
|
|
8009
|
-
}
|
|
8010
|
-
else {
|
|
8011
|
-
throw new Error('Invalid file type');
|
|
8012
|
-
}
|
|
8013
|
-
}
|
|
8014
|
-
|
|
8015
|
-
/**
|
|
8016
|
-
* Returns the summary text for a file change.
|
|
8017
|
-
*
|
|
8018
|
-
* @param file - The file status or diff result.
|
|
8019
|
-
* @param change - The partial file change object.
|
|
8020
|
-
* @returns The summary text for the file change.
|
|
8021
|
-
* @throws Error if the file type is invalid.
|
|
8022
|
-
*/
|
|
8023
|
-
function getSummaryText(file, change) {
|
|
8024
|
-
const status = change.status || getStatus(file);
|
|
8025
|
-
let filePath;
|
|
8026
|
-
if ('path' in file) {
|
|
8027
|
-
filePath = file.path;
|
|
8028
|
-
}
|
|
8029
|
-
else if ('file' in file) {
|
|
8030
|
-
filePath = change?.filePath || file.file;
|
|
8031
|
-
}
|
|
8032
|
-
else {
|
|
8033
|
-
throw new Error('Invalid file type');
|
|
8034
|
-
}
|
|
8035
|
-
if (change.oldFilePath) {
|
|
8036
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
8037
|
-
}
|
|
8038
|
-
return `${status}: ${filePath}`;
|
|
8039
|
-
}
|
|
8040
|
-
|
|
8041
|
-
/**
|
|
8042
|
-
* Retrieves the diff between the current branch and a specified target branch.
|
|
8043
|
-
*
|
|
8044
|
-
* @param {Object} options - The options for retrieving the diff.
|
|
8045
|
-
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
8046
|
-
* @param {Logger} options.logger - The logger for logging messages.
|
|
8047
|
-
* @param {string} options.baseBranch - The base branch to compare against.
|
|
8048
|
-
* @param {string} options.headBranch - The head branch to compare.
|
|
8049
|
-
* @param {string[]} options.ignoredFiles - Array of specific files to ignore.
|
|
8050
|
-
* @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
|
|
8051
|
-
* @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
|
|
8052
|
-
*/
|
|
8053
|
-
async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
|
|
8468
|
+
async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
|
|
8054
8469
|
try {
|
|
8055
8470
|
logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
|
|
8056
8471
|
color: 'blue',
|
|
@@ -8118,726 +8533,239 @@ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options,
|
|
|
8118
8533
|
}
|
|
8119
8534
|
}
|
|
8120
8535
|
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
## Rules
|
|
8130
|
-
- Create a descriptive title for the changelog that gives a high-level overview of the changes.
|
|
8131
|
-
- **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
|
|
8132
|
-
- **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
|
|
8133
|
-
- **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
|
|
8134
|
-
- **Summaries**: For each change, provide a concise summary.
|
|
8135
|
-
- **Attribution**: {{author_instructions}}
|
|
8136
|
-
- **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
|
|
8137
|
-
- **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
|
|
8138
|
-
- **Formatting**: Your entire response must be valid Markdown.
|
|
8139
|
-
|
|
8140
|
-
## Formatting Instructions
|
|
8141
|
-
{{format_instructions}}
|
|
8142
|
-
|
|
8143
|
-
{{additional_context}}
|
|
8536
|
+
/**
|
|
8537
|
+
* Extract the path from a file path string.
|
|
8538
|
+
* @param {string} filePath - The full file path.
|
|
8539
|
+
* @returns {string} The path portion of the file path.
|
|
8540
|
+
*/
|
|
8541
|
+
function getPathFromFilePath(filePath) {
|
|
8542
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
8543
|
+
}
|
|
8144
8544
|
|
|
8145
|
-
|
|
8146
|
-
const
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
const
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
});
|
|
8545
|
+
async function summarize(documents$1, { chain, textSplitter, options, logger, tokenizer, metadata }) {
|
|
8546
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
8547
|
+
const docs = await textSplitter.splitDocuments(documents$1.map((doc) => new documents.Document(doc)));
|
|
8548
|
+
const promptTokens = tokenizer
|
|
8549
|
+
? docs.reduce((sum, doc) => sum + tokenizer(doc.pageContent), 0)
|
|
8550
|
+
: undefined;
|
|
8551
|
+
const startedAt = Date.now();
|
|
8552
|
+
const res = await chain.invoke({
|
|
8553
|
+
input_documents: docs,
|
|
8554
|
+
returnIntermediateSteps,
|
|
8555
|
+
});
|
|
8556
|
+
const elapsedMs = Date.now() - startedAt;
|
|
8557
|
+
logLlmCall(logger, {
|
|
8558
|
+
task: 'summarize',
|
|
8559
|
+
promptTokens,
|
|
8560
|
+
elapsedMs,
|
|
8561
|
+
inputDocuments: documents$1.length,
|
|
8562
|
+
inputChunks: docs.length,
|
|
8563
|
+
...metadata,
|
|
8564
|
+
});
|
|
8565
|
+
if (res.error)
|
|
8566
|
+
throw new Error(res.error);
|
|
8567
|
+
return res.text && res.text.trim();
|
|
8568
|
+
}
|
|
8156
8569
|
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8570
|
+
/**
|
|
8571
|
+
* Summarize a single file diff that exceeds the token threshold.
|
|
8572
|
+
*/
|
|
8573
|
+
async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer, logger, metadata, }) {
|
|
8574
|
+
try {
|
|
8575
|
+
const fileSummary = await summarize([
|
|
8576
|
+
{
|
|
8577
|
+
pageContent: fileDiff.diff,
|
|
8578
|
+
metadata: {
|
|
8579
|
+
file: fileDiff.file,
|
|
8580
|
+
summary: fileDiff.summary,
|
|
8581
|
+
},
|
|
8582
|
+
},
|
|
8583
|
+
], {
|
|
8584
|
+
chain,
|
|
8585
|
+
textSplitter,
|
|
8586
|
+
tokenizer,
|
|
8587
|
+
logger,
|
|
8588
|
+
metadata: {
|
|
8589
|
+
...metadata,
|
|
8590
|
+
task: 'summarize-large-file',
|
|
8591
|
+
},
|
|
8592
|
+
options: {
|
|
8593
|
+
returnIntermediateSteps: false,
|
|
8594
|
+
},
|
|
8595
|
+
});
|
|
8596
|
+
const newTokenCount = tokenizer(fileSummary);
|
|
8597
|
+
return {
|
|
8598
|
+
...fileDiff,
|
|
8599
|
+
diff: fileSummary,
|
|
8600
|
+
tokenCount: newTokenCount,
|
|
8601
|
+
};
|
|
8170
8602
|
}
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8603
|
+
catch (error) {
|
|
8604
|
+
// On error, return original diff unchanged
|
|
8605
|
+
console.error(`Failed to summarize file ${fileDiff.file}:`, error);
|
|
8606
|
+
return fileDiff;
|
|
8174
8607
|
}
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8608
|
+
}
|
|
8609
|
+
/**
|
|
8610
|
+
* Process files in waves to respect concurrency limits.
|
|
8611
|
+
*/
|
|
8612
|
+
async function processInWaves$1(items, processor, maxConcurrent) {
|
|
8613
|
+
const results = [];
|
|
8614
|
+
for (let i = 0; i < items.length; i += maxConcurrent) {
|
|
8615
|
+
const wave = items.slice(i, i + maxConcurrent);
|
|
8616
|
+
const waveResults = await Promise.all(wave.map(processor));
|
|
8617
|
+
results.push(...waveResults);
|
|
8181
8618
|
}
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
const [from, to] = config.range.split(':');
|
|
8202
|
-
if (!from || !to) {
|
|
8203
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
8204
|
-
process.exit(1);
|
|
8205
|
-
}
|
|
8206
|
-
commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
|
|
8207
|
-
}
|
|
8208
|
-
else if (argv.branch) {
|
|
8209
|
-
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
8210
|
-
commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
|
|
8211
|
-
}
|
|
8212
|
-
else if (argv.tag) {
|
|
8213
|
-
logger.verbose(`Generating commit log against tag: ${argv.tag}`, { color: 'yellow' });
|
|
8214
|
-
commits = await getCommitLogAgainstTag({ git, logger, targetTag: argv.tag });
|
|
8215
|
-
}
|
|
8216
|
-
else {
|
|
8217
|
-
logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
|
|
8218
|
-
color: 'yellow',
|
|
8219
|
-
});
|
|
8220
|
-
commits = await getCommitLogCurrentBranch({ git, logger });
|
|
8221
|
-
}
|
|
8222
|
-
let commitsWithDiffText = commits;
|
|
8223
|
-
if (argv.withDiff) {
|
|
8224
|
-
commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
|
|
8225
|
-
...commit,
|
|
8226
|
-
diffText: await getDiffForCommit(commit.hash, { git }),
|
|
8227
|
-
})));
|
|
8619
|
+
return results;
|
|
8620
|
+
}
|
|
8621
|
+
/**
|
|
8622
|
+
* Pre-summarize individual files that exceed the maxFileTokens threshold.
|
|
8623
|
+
* This prevents large files from dominating the token budget and biasing
|
|
8624
|
+
* the final commit message toward a single file's changes.
|
|
8625
|
+
*
|
|
8626
|
+
* @param diffs - Array of file diffs to process
|
|
8627
|
+
* @param options - Configuration options for summarization
|
|
8628
|
+
* @returns Array of file diffs with large files summarized
|
|
8629
|
+
*/
|
|
8630
|
+
async function summarizeLargeFiles(diffs, options) {
|
|
8631
|
+
const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter, metadata } = options;
|
|
8632
|
+
// Identify files that need summarization
|
|
8633
|
+
const filesToSummarize = [];
|
|
8634
|
+
const results = [...diffs];
|
|
8635
|
+
diffs.forEach((diff, index) => {
|
|
8636
|
+
if (diff.tokenCount > maxFileTokens && diff.tokenCount >= minTokensForSummary) {
|
|
8637
|
+
filesToSummarize.push({ index, diff });
|
|
8228
8638
|
}
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
withDiff: argv.withDiff,
|
|
8233
|
-
};
|
|
8639
|
+
});
|
|
8640
|
+
if (filesToSummarize.length === 0) {
|
|
8641
|
+
return results;
|
|
8234
8642
|
}
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
if (data.withDiff && commit.diffText) {
|
|
8246
|
-
commitStr += `\nDiff:\n${commit.diffText}`;
|
|
8247
|
-
}
|
|
8248
|
-
return commitStr.trim();
|
|
8249
|
-
}).join('\n\n---\n\n');
|
|
8250
|
-
return result;
|
|
8251
|
-
}
|
|
8252
|
-
const changelogMsg = await generateAndReviewLoop({
|
|
8253
|
-
label: 'changelog',
|
|
8254
|
-
options: {
|
|
8255
|
-
...config,
|
|
8256
|
-
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
8257
|
-
logger,
|
|
8258
|
-
interactive: INTERACTIVE,
|
|
8259
|
-
review: {
|
|
8260
|
-
enableFullRetry: false,
|
|
8261
|
-
},
|
|
8262
|
-
},
|
|
8263
|
-
factory,
|
|
8264
|
-
parser,
|
|
8265
|
-
agent: async (context, options) => {
|
|
8266
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8267
|
-
const parser = createSchemaParser(ChangelogResponseSchema, llm);
|
|
8268
|
-
const prompt = getPrompt({
|
|
8269
|
-
template: options.prompt,
|
|
8270
|
-
variables: CHANGELOG_PROMPT.inputVariables,
|
|
8271
|
-
fallback: CHANGELOG_PROMPT,
|
|
8272
|
-
});
|
|
8273
|
-
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.";
|
|
8274
|
-
let additional_context = '';
|
|
8275
|
-
if (argv.additional) {
|
|
8276
|
-
additional_context = `## Additional Context\n${argv.additional}`;
|
|
8277
|
-
}
|
|
8278
|
-
const author_instructions = argv.author
|
|
8279
|
-
? '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.'
|
|
8280
|
-
: 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
|
|
8281
|
-
const changelog = await executeChain({
|
|
8282
|
-
llm,
|
|
8283
|
-
prompt,
|
|
8284
|
-
variables: {
|
|
8285
|
-
summary: context,
|
|
8286
|
-
format_instructions: formatInstructions,
|
|
8287
|
-
additional_context: additional_context,
|
|
8288
|
-
author_instructions: author_instructions,
|
|
8289
|
-
},
|
|
8290
|
-
parser,
|
|
8291
|
-
});
|
|
8292
|
-
const branchName = await getCurrentBranchName({ git });
|
|
8293
|
-
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
8294
|
-
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
8295
|
-
return `${changelog.title}\n\n${changelog.content}${footer}`;
|
|
8296
|
-
},
|
|
8297
|
-
noResult: async () => {
|
|
8298
|
-
if (config.range) {
|
|
8299
|
-
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
8300
|
-
process.exit(0);
|
|
8301
|
-
}
|
|
8302
|
-
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
8303
|
-
process.exit(0);
|
|
8304
|
-
},
|
|
8305
|
-
});
|
|
8306
|
-
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
8307
|
-
handleResult({
|
|
8308
|
-
result: changelogMsg,
|
|
8309
|
-
interactiveModeCallback: async () => {
|
|
8310
|
-
logSuccess();
|
|
8311
|
-
},
|
|
8312
|
-
mode: MODE,
|
|
8643
|
+
logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
|
|
8644
|
+
// Process large files in waves
|
|
8645
|
+
const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer, logger, metadata }), maxConcurrent);
|
|
8646
|
+
// Update results with summarized files
|
|
8647
|
+
summarizedFiles.forEach((summarizedDiff, i) => {
|
|
8648
|
+
const originalIndex = filesToSummarize[i].index;
|
|
8649
|
+
const originalTokens = results[originalIndex].tokenCount;
|
|
8650
|
+
const newTokens = summarizedDiff.tokenCount;
|
|
8651
|
+
logger.verbose(` - ${summarizedDiff.file}: ${originalTokens} -> ${newTokens} tokens`, { color: 'magenta' });
|
|
8652
|
+
results[originalIndex] = summarizedDiff;
|
|
8313
8653
|
});
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
var changelog = {
|
|
8317
|
-
command: command$4,
|
|
8318
|
-
desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
|
|
8319
|
-
builder: builder$4,
|
|
8320
|
-
handler: commandExecutor(handler$4),
|
|
8321
|
-
options: options$4,
|
|
8322
|
-
};
|
|
8323
|
-
|
|
8324
|
-
const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
|
|
8325
|
-
// Regular commit message schema with basic validation
|
|
8326
|
-
const CommitMessageResponseSchema = objectType({
|
|
8327
|
-
title: stringType().describe("Title of the commit message"),
|
|
8328
|
-
body: stringType().describe("Body of the commit message"),
|
|
8329
|
-
}).describe("Object with commit message 'title' and 'body'");
|
|
8330
|
-
// Conventional commit message schema with strict formatting rules
|
|
8331
|
-
const ConventionalCommitMessageResponseSchema = objectType({
|
|
8332
|
-
title: stringType()
|
|
8333
|
-
.max(50, "Title must be 50 characters or less")
|
|
8334
|
-
.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"),
|
|
8335
|
-
body: stringType().describe("Body of the commit message")
|
|
8336
|
-
// .max(280, "Body must be 280 characters or less"),
|
|
8337
|
-
}).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
|
|
8338
|
-
const command$3 = 'commit';
|
|
8654
|
+
return results;
|
|
8655
|
+
}
|
|
8339
8656
|
/**
|
|
8340
|
-
*
|
|
8657
|
+
* Pre-process a DiffNode tree, summarizing large files at the leaf level.
|
|
8658
|
+
* Returns a new DiffNode with updated token counts.
|
|
8341
8659
|
*/
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
additional: {
|
|
8366
|
-
description: 'Add extra contextual information to the prompt',
|
|
8367
|
-
type: 'string',
|
|
8368
|
-
alias: 'a',
|
|
8369
|
-
},
|
|
8370
|
-
withPreviousCommits: {
|
|
8371
|
-
description: 'Include previous commits as context (specify number of commits, 0 for none)',
|
|
8372
|
-
type: 'number',
|
|
8373
|
-
default: 0,
|
|
8374
|
-
alias: 'p',
|
|
8375
|
-
},
|
|
8376
|
-
conventional: {
|
|
8377
|
-
description: 'Generate commit message in Conventional Commits format',
|
|
8378
|
-
type: 'boolean',
|
|
8379
|
-
default: false,
|
|
8380
|
-
alias: 'c',
|
|
8381
|
-
},
|
|
8382
|
-
includeBranchName: {
|
|
8383
|
-
description: 'Include the current branch name in the commit prompt for context',
|
|
8384
|
-
type: 'boolean',
|
|
8385
|
-
default: true,
|
|
8386
|
-
},
|
|
8387
|
-
noDiff: {
|
|
8388
|
-
description: 'Only pass basic "git status" result instead of providing entire diff',
|
|
8389
|
-
type: 'boolean',
|
|
8390
|
-
default: false,
|
|
8391
|
-
},
|
|
8392
|
-
};
|
|
8393
|
-
const builder$3 = (yargs) => {
|
|
8394
|
-
return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
|
|
8395
|
-
};
|
|
8660
|
+
async function preprocessLargeFiles(rootNode, options) {
|
|
8661
|
+
// Collect all diffs from the tree
|
|
8662
|
+
const allDiffs = [];
|
|
8663
|
+
function collectDiffs(node) {
|
|
8664
|
+
allDiffs.push(...node.diffs);
|
|
8665
|
+
node.children.forEach(collectDiffs);
|
|
8666
|
+
}
|
|
8667
|
+
collectDiffs(rootNode);
|
|
8668
|
+
// Summarize large files
|
|
8669
|
+
const processedDiffs = await summarizeLargeFiles(allDiffs, options);
|
|
8670
|
+
// Create a map for quick lookup
|
|
8671
|
+
const diffMap = new Map();
|
|
8672
|
+
processedDiffs.forEach((diff) => diffMap.set(diff.file, diff));
|
|
8673
|
+
// Rebuild tree with processed diffs
|
|
8674
|
+
function rebuildNode(node) {
|
|
8675
|
+
return {
|
|
8676
|
+
path: node.path,
|
|
8677
|
+
diffs: node.diffs.map((diff) => diffMap.get(diff.file) || diff),
|
|
8678
|
+
children: node.children.map(rebuildNode),
|
|
8679
|
+
};
|
|
8680
|
+
}
|
|
8681
|
+
return rebuildNode(rootNode);
|
|
8682
|
+
}
|
|
8396
8683
|
|
|
8397
8684
|
/**
|
|
8398
|
-
*
|
|
8399
|
-
*
|
|
8400
|
-
* @
|
|
8401
|
-
* @param llm - LLM instance
|
|
8402
|
-
* @param prompt - Prompt template
|
|
8403
|
-
* @param variables - Variables for the prompt
|
|
8404
|
-
* @param options - Configuration options
|
|
8405
|
-
* @returns Parsed result matching the schema type
|
|
8685
|
+
* Create groups from a given node info.
|
|
8686
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
8687
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
8406
8688
|
*/
|
|
8407
|
-
|
|
8408
|
-
const
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
prompt,
|
|
8415
|
-
variables,
|
|
8416
|
-
parser,
|
|
8417
|
-
});
|
|
8418
|
-
return result;
|
|
8419
|
-
};
|
|
8420
|
-
try {
|
|
8421
|
-
return await withRetry(operation, retryOptions);
|
|
8422
|
-
}
|
|
8423
|
-
catch (error) {
|
|
8424
|
-
if (fallbackParser) {
|
|
8425
|
-
if (onFallback) {
|
|
8426
|
-
onFallback();
|
|
8689
|
+
function createDirectoryDiffs(node) {
|
|
8690
|
+
const groupByPath = {};
|
|
8691
|
+
function traverse(node) {
|
|
8692
|
+
node.diffs.forEach((diff) => {
|
|
8693
|
+
const path = getPathFromFilePath(diff.file);
|
|
8694
|
+
if (!groupByPath[path]) {
|
|
8695
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
8427
8696
|
}
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
parser: new output_parsers.StringOutputParser(),
|
|
8433
|
-
});
|
|
8434
|
-
const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
|
|
8435
|
-
return fallbackParser(fallbackText);
|
|
8436
|
-
}
|
|
8437
|
-
// No fallback available, re-throw the error
|
|
8438
|
-
throw error;
|
|
8697
|
+
groupByPath[path].diffs.push(diff);
|
|
8698
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
8699
|
+
});
|
|
8700
|
+
node.children.forEach(traverse);
|
|
8439
8701
|
}
|
|
8702
|
+
traverse(node);
|
|
8703
|
+
return Object.values(groupByPath);
|
|
8440
8704
|
}
|
|
8441
|
-
|
|
8442
8705
|
/**
|
|
8443
|
-
*
|
|
8444
|
-
* Specifically handles cases where string values are not properly quoted
|
|
8706
|
+
* Summarize a directory diff asynchronously.
|
|
8445
8707
|
*/
|
|
8446
|
-
function
|
|
8447
|
-
// Remove any markdown code block wrapping
|
|
8448
|
-
let cleaned = jsonString.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, '$1').trim();
|
|
8449
|
-
// Remove inline code block wrapping
|
|
8450
|
-
cleaned = cleaned.replace(/^`(.*)`$/, '$1').trim();
|
|
8451
|
-
// If it doesn't look like JSON, return as-is
|
|
8452
|
-
if (!cleaned.startsWith('{') || !cleaned.endsWith('}')) {
|
|
8453
|
-
return jsonString;
|
|
8454
|
-
}
|
|
8708
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer, logger, metadata }) {
|
|
8455
8709
|
try {
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
// Try to repair common issues
|
|
8462
|
-
let repaired = cleaned;
|
|
8463
|
-
// Fix unquoted string values in title and body fields
|
|
8464
|
-
// Pattern: "title": unquoted_value, -> "title": "unquoted_value",
|
|
8465
|
-
repaired = repaired.replace(/"(title|body)":\s*([^",\{\}\[\]]+?)(?=\s*[,\}])/g, (match, field, value) => {
|
|
8466
|
-
// Clean up the value (remove leading/trailing whitespace)
|
|
8467
|
-
const cleanValue = value.trim();
|
|
8468
|
-
// If it's already quoted or looks like a number/boolean, leave it
|
|
8469
|
-
if (cleanValue.startsWith('"') || /^(true|false|\d+)$/.test(cleanValue)) {
|
|
8470
|
-
return match;
|
|
8471
|
-
}
|
|
8472
|
-
// Quote the value
|
|
8473
|
-
return `"${field}": "${cleanValue}"`;
|
|
8474
|
-
});
|
|
8475
|
-
// Fix missing quotes around field names (though this should be rare)
|
|
8476
|
-
repaired = repaired.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
8477
|
-
// Remove trailing commas before closing braces
|
|
8478
|
-
repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
|
|
8479
|
-
try {
|
|
8480
|
-
// Test if the repair worked
|
|
8481
|
-
JSON.parse(repaired);
|
|
8482
|
-
return repaired;
|
|
8483
|
-
}
|
|
8484
|
-
catch {
|
|
8485
|
-
// If repair failed, return original
|
|
8486
|
-
return jsonString;
|
|
8487
|
-
}
|
|
8488
|
-
}
|
|
8489
|
-
}
|
|
8490
|
-
|
|
8491
|
-
/**
|
|
8492
|
-
* Extract the first complete JSON object from a string by tracking balanced braces
|
|
8493
|
-
*/
|
|
8494
|
-
function extractFirstJsonObject(text) {
|
|
8495
|
-
const startIndex = text.indexOf('{');
|
|
8496
|
-
if (startIndex === -1)
|
|
8497
|
-
return null;
|
|
8498
|
-
let braceCount = 0;
|
|
8499
|
-
let inString = false;
|
|
8500
|
-
let escapeNext = false;
|
|
8501
|
-
for (let i = startIndex; i < text.length; i++) {
|
|
8502
|
-
const char = text[i];
|
|
8503
|
-
if (escapeNext) {
|
|
8504
|
-
escapeNext = false;
|
|
8505
|
-
continue;
|
|
8506
|
-
}
|
|
8507
|
-
if (char === '\\') {
|
|
8508
|
-
escapeNext = true;
|
|
8509
|
-
continue;
|
|
8510
|
-
}
|
|
8511
|
-
if (char === '"') {
|
|
8512
|
-
inString = !inString;
|
|
8513
|
-
continue;
|
|
8514
|
-
}
|
|
8515
|
-
if (inString)
|
|
8516
|
-
continue;
|
|
8517
|
-
if (char === '{') {
|
|
8518
|
-
braceCount++;
|
|
8519
|
-
}
|
|
8520
|
-
else if (char === '}') {
|
|
8521
|
-
braceCount--;
|
|
8522
|
-
if (braceCount === 0) {
|
|
8523
|
-
// Found the end of the first complete JSON object
|
|
8524
|
-
return text.substring(startIndex, i + 1);
|
|
8525
|
-
}
|
|
8526
|
-
}
|
|
8527
|
-
}
|
|
8528
|
-
return null;
|
|
8529
|
-
}
|
|
8530
|
-
/**
|
|
8531
|
-
* Utility function to ensure commit messages are properly formatted as strings
|
|
8532
|
-
* rather than JSON objects, whether they come as parsed objects or stringified JSON
|
|
8533
|
-
*/
|
|
8534
|
-
function formatCommitMessage(result, options = {}) {
|
|
8535
|
-
const { append, ticketId, appendTicket } = options;
|
|
8536
|
-
// Helper function to construct the final message with appends
|
|
8537
|
-
const constructMessage = (title, body) => {
|
|
8538
|
-
const appendedText = append ? `\n\n${append}` : '';
|
|
8539
|
-
const ticketFooter = appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
8540
|
-
return `${title}\n\n${body}${appendedText}${ticketFooter}`;
|
|
8541
|
-
};
|
|
8542
|
-
// If it's a string, check if it contains a JSON object (including markdown code blocks)
|
|
8543
|
-
if (typeof result === 'string') {
|
|
8544
|
-
// Early return if string clearly doesn't contain JSON-like content
|
|
8545
|
-
if (!result.includes('{') && !result.includes('"title"')) {
|
|
8546
|
-
return result;
|
|
8547
|
-
}
|
|
8548
|
-
// Handle multiple markdown code block formats and embedded JSON
|
|
8549
|
-
const extractionPatterns = [
|
|
8550
|
-
/```(?:json)?\s*(\{[\s\S]*?\})\s*```/, // Standard markdown blocks
|
|
8551
|
-
/`(\{[\s\S]*?\})`/, // Inline code blocks
|
|
8552
|
-
/^\s*(\{[\s\S]*\})\s*$/, // Raw JSON without blocks (entire string)
|
|
8553
|
-
/(\{[\s\S]*?\})/ // JSON anywhere in text (fallback)
|
|
8554
|
-
];
|
|
8555
|
-
let jsonString = result;
|
|
8556
|
-
let foundMatch = false;
|
|
8557
|
-
// Try each pattern to extract JSON
|
|
8558
|
-
for (const pattern of extractionPatterns) {
|
|
8559
|
-
const match = result.match(pattern);
|
|
8560
|
-
if (match && match[1]) {
|
|
8561
|
-
jsonString = match[1].trim();
|
|
8562
|
-
foundMatch = true;
|
|
8563
|
-
break;
|
|
8564
|
-
}
|
|
8565
|
-
}
|
|
8566
|
-
// Only attempt JSON parsing if we found potential JSON content
|
|
8567
|
-
if (foundMatch || jsonString.startsWith('{')) {
|
|
8568
|
-
try {
|
|
8569
|
-
// Try to parse as JSON to see if it's a stringified object
|
|
8570
|
-
const parsed = JSON.parse(jsonString);
|
|
8571
|
-
if (parsed &&
|
|
8572
|
-
typeof parsed === 'object' &&
|
|
8573
|
-
typeof parsed.title === 'string' &&
|
|
8574
|
-
typeof parsed.body === 'string' &&
|
|
8575
|
-
parsed.title.length > 0 &&
|
|
8576
|
-
parsed.body.length > 0) {
|
|
8577
|
-
// It's a valid stringified JSON object, format it properly
|
|
8578
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8579
|
-
}
|
|
8580
|
-
}
|
|
8581
|
-
catch {
|
|
8582
|
-
// Try to repair the JSON and parse again
|
|
8583
|
-
try {
|
|
8584
|
-
const repairedJson = repairJson(jsonString);
|
|
8585
|
-
const parsed = JSON.parse(repairedJson);
|
|
8586
|
-
if (parsed &&
|
|
8587
|
-
typeof parsed === 'object' &&
|
|
8588
|
-
typeof parsed.title === 'string' &&
|
|
8589
|
-
typeof parsed.body === 'string' &&
|
|
8590
|
-
parsed.title.length > 0 &&
|
|
8591
|
-
parsed.body.length > 0) {
|
|
8592
|
-
// Successfully repaired and parsed JSON
|
|
8593
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8594
|
-
}
|
|
8595
|
-
}
|
|
8596
|
-
catch {
|
|
8597
|
-
// Repair failed, try extracting just the first complete JSON object
|
|
8598
|
-
const firstObject = extractFirstJsonObject(jsonString);
|
|
8599
|
-
if (firstObject) {
|
|
8600
|
-
try {
|
|
8601
|
-
const parsed = JSON.parse(firstObject);
|
|
8602
|
-
if (parsed &&
|
|
8603
|
-
typeof parsed === 'object' &&
|
|
8604
|
-
typeof parsed.title === 'string' &&
|
|
8605
|
-
typeof parsed.body === 'string' &&
|
|
8606
|
-
parsed.title.length > 0 &&
|
|
8607
|
-
parsed.body.length > 0) {
|
|
8608
|
-
return constructMessage(parsed.title, parsed.body);
|
|
8609
|
-
}
|
|
8610
|
-
}
|
|
8611
|
-
catch {
|
|
8612
|
-
// Even first object extraction failed, continue to fallback
|
|
8613
|
-
}
|
|
8614
|
-
}
|
|
8615
|
-
}
|
|
8616
|
-
}
|
|
8617
|
-
}
|
|
8618
|
-
// If no JSON found and it's already formatted, return as-is
|
|
8619
|
-
return result;
|
|
8620
|
-
}
|
|
8621
|
-
// If it's already an object with title and body, format it
|
|
8622
|
-
if (typeof result === 'object' && result !== null &&
|
|
8623
|
-
'title' in result && 'body' in result) {
|
|
8624
|
-
const commitMsgObj = result;
|
|
8625
|
-
if (typeof commitMsgObj.title === 'string' && typeof commitMsgObj.body === 'string') {
|
|
8626
|
-
return constructMessage(commitMsgObj.title, commitMsgObj.body);
|
|
8627
|
-
}
|
|
8628
|
-
}
|
|
8629
|
-
// Fallback - convert to string and return as-is
|
|
8630
|
-
return String(result);
|
|
8631
|
-
}
|
|
8632
|
-
|
|
8633
|
-
/**
|
|
8634
|
-
* Extract the path from a file path string.
|
|
8635
|
-
* @param {string} filePath - The full file path.
|
|
8636
|
-
* @returns {string} The path portion of the file path.
|
|
8637
|
-
*/
|
|
8638
|
-
function getPathFromFilePath(filePath) {
|
|
8639
|
-
return filePath.split('/').slice(0, -1).join('/');
|
|
8640
|
-
}
|
|
8641
|
-
|
|
8642
|
-
async function summarize(documents$1, { chain, textSplitter, options }) {
|
|
8643
|
-
const { returnIntermediateSteps = false } = options || {};
|
|
8644
|
-
const docs = await textSplitter.splitDocuments(documents$1.map((doc) => new documents.Document(doc)));
|
|
8645
|
-
const res = await chain.invoke({
|
|
8646
|
-
input_documents: docs,
|
|
8647
|
-
returnIntermediateSteps,
|
|
8648
|
-
});
|
|
8649
|
-
if (res.error)
|
|
8650
|
-
throw new Error(res.error);
|
|
8651
|
-
return res.text && res.text.trim();
|
|
8652
|
-
}
|
|
8653
|
-
|
|
8654
|
-
/**
|
|
8655
|
-
* Summarize a single file diff that exceeds the token threshold.
|
|
8656
|
-
*/
|
|
8657
|
-
async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
|
|
8658
|
-
try {
|
|
8659
|
-
const fileSummary = await summarize([
|
|
8660
|
-
{
|
|
8661
|
-
pageContent: fileDiff.diff,
|
|
8662
|
-
metadata: {
|
|
8663
|
-
file: fileDiff.file,
|
|
8664
|
-
summary: fileDiff.summary,
|
|
8665
|
-
},
|
|
8710
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
8711
|
+
pageContent: diff.diff,
|
|
8712
|
+
metadata: {
|
|
8713
|
+
file: diff.file,
|
|
8714
|
+
summary: diff.summary,
|
|
8666
8715
|
},
|
|
8667
|
-
|
|
8716
|
+
})), {
|
|
8668
8717
|
chain,
|
|
8669
8718
|
textSplitter,
|
|
8719
|
+
tokenizer,
|
|
8720
|
+
logger,
|
|
8721
|
+
metadata: {
|
|
8722
|
+
...metadata,
|
|
8723
|
+
task: 'summarize-directory-diff',
|
|
8724
|
+
},
|
|
8670
8725
|
options: {
|
|
8671
|
-
returnIntermediateSteps:
|
|
8726
|
+
returnIntermediateSteps: true,
|
|
8672
8727
|
},
|
|
8673
8728
|
});
|
|
8674
|
-
const
|
|
8729
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
8675
8730
|
return {
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8731
|
+
diffs: directory.diffs,
|
|
8732
|
+
path: directory.path,
|
|
8733
|
+
summary: directorySummary,
|
|
8734
|
+
tokenCount: newTokenTotal,
|
|
8679
8735
|
};
|
|
8680
8736
|
}
|
|
8681
8737
|
catch (error) {
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
return fileDiff;
|
|
8738
|
+
console.error(error);
|
|
8739
|
+
return directory;
|
|
8685
8740
|
}
|
|
8686
8741
|
}
|
|
8687
8742
|
/**
|
|
8688
|
-
*
|
|
8743
|
+
* Default output formatter for directory diffs.
|
|
8744
|
+
*
|
|
8745
|
+
* TODO: Future improvements to consider:
|
|
8746
|
+
* - Hierarchical output showing file -> directory -> overall summary
|
|
8747
|
+
* - Configurable verbosity levels (compact, standard, detailed)
|
|
8748
|
+
* - Machine-readable format option (JSON) for programmatic use
|
|
8749
|
+
* - Semantic grouping by change type (added/modified/deleted) or feature area
|
|
8750
|
+
* - Visual diff indicators showing magnitude of changes
|
|
8689
8751
|
*/
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
results.push(...waveResults);
|
|
8752
|
+
const defaultOutputCallback = (group) => {
|
|
8753
|
+
let output = `
|
|
8754
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
8755
|
+
if (group.summary) {
|
|
8756
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
8696
8757
|
}
|
|
8697
|
-
|
|
8698
|
-
}
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
* the final commit message toward a single file's changes.
|
|
8703
|
-
*
|
|
8704
|
-
* @param diffs - Array of file diffs to process
|
|
8705
|
-
* @param options - Configuration options for summarization
|
|
8706
|
-
* @returns Array of file diffs with large files summarized
|
|
8707
|
-
*/
|
|
8708
|
-
async function summarizeLargeFiles(diffs, options) {
|
|
8709
|
-
const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter } = options;
|
|
8710
|
-
// Identify files that need summarization
|
|
8711
|
-
const filesToSummarize = [];
|
|
8712
|
-
const results = [...diffs];
|
|
8713
|
-
diffs.forEach((diff, index) => {
|
|
8714
|
-
if (diff.tokenCount > maxFileTokens && diff.tokenCount >= minTokensForSummary) {
|
|
8715
|
-
filesToSummarize.push({ index, diff });
|
|
8716
|
-
}
|
|
8717
|
-
});
|
|
8718
|
-
if (filesToSummarize.length === 0) {
|
|
8719
|
-
return results;
|
|
8720
|
-
}
|
|
8721
|
-
logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
|
|
8722
|
-
// Process large files in waves
|
|
8723
|
-
const summarizedFiles = await processInWaves(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer }), maxConcurrent);
|
|
8724
|
-
// Update results with summarized files
|
|
8725
|
-
summarizedFiles.forEach((summarizedDiff, i) => {
|
|
8726
|
-
const originalIndex = filesToSummarize[i].index;
|
|
8727
|
-
const originalTokens = results[originalIndex].tokenCount;
|
|
8728
|
-
const newTokens = summarizedDiff.tokenCount;
|
|
8729
|
-
logger.verbose(` - ${summarizedDiff.file}: ${originalTokens} -> ${newTokens} tokens`, { color: 'magenta' });
|
|
8730
|
-
results[originalIndex] = summarizedDiff;
|
|
8731
|
-
});
|
|
8732
|
-
return results;
|
|
8733
|
-
}
|
|
8734
|
-
/**
|
|
8735
|
-
* Pre-process a DiffNode tree, summarizing large files at the leaf level.
|
|
8736
|
-
* Returns a new DiffNode with updated token counts.
|
|
8737
|
-
*/
|
|
8738
|
-
async function preprocessLargeFiles(rootNode, options) {
|
|
8739
|
-
// Collect all diffs from the tree
|
|
8740
|
-
const allDiffs = [];
|
|
8741
|
-
function collectDiffs(node) {
|
|
8742
|
-
allDiffs.push(...node.diffs);
|
|
8743
|
-
node.children.forEach(collectDiffs);
|
|
8744
|
-
}
|
|
8745
|
-
collectDiffs(rootNode);
|
|
8746
|
-
// Summarize large files
|
|
8747
|
-
const processedDiffs = await summarizeLargeFiles(allDiffs, options);
|
|
8748
|
-
// Create a map for quick lookup
|
|
8749
|
-
const diffMap = new Map();
|
|
8750
|
-
processedDiffs.forEach((diff) => diffMap.set(diff.file, diff));
|
|
8751
|
-
// Rebuild tree with processed diffs
|
|
8752
|
-
function rebuildNode(node) {
|
|
8753
|
-
return {
|
|
8754
|
-
path: node.path,
|
|
8755
|
-
diffs: node.diffs.map((diff) => diffMap.get(diff.file) || diff),
|
|
8756
|
-
children: node.children.map(rebuildNode),
|
|
8757
|
-
};
|
|
8758
|
-
}
|
|
8759
|
-
return rebuildNode(rootNode);
|
|
8760
|
-
}
|
|
8761
|
-
|
|
8762
|
-
/**
|
|
8763
|
-
* Create groups from a given node info.
|
|
8764
|
-
* @param {DiffNode} node - The node info to start grouping.
|
|
8765
|
-
* @returns {DirectoryDiff[]} The groups created.
|
|
8766
|
-
*/
|
|
8767
|
-
function createDirectoryDiffs(node) {
|
|
8768
|
-
const groupByPath = {};
|
|
8769
|
-
function traverse(node) {
|
|
8770
|
-
node.diffs.forEach((diff) => {
|
|
8771
|
-
const path = getPathFromFilePath(diff.file);
|
|
8772
|
-
if (!groupByPath[path]) {
|
|
8773
|
-
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
8774
|
-
}
|
|
8775
|
-
groupByPath[path].diffs.push(diff);
|
|
8776
|
-
groupByPath[path].tokenCount += diff.tokenCount;
|
|
8777
|
-
});
|
|
8778
|
-
node.children.forEach(traverse);
|
|
8779
|
-
}
|
|
8780
|
-
traverse(node);
|
|
8781
|
-
return Object.values(groupByPath);
|
|
8782
|
-
}
|
|
8783
|
-
/**
|
|
8784
|
-
* Summarize a directory diff asynchronously.
|
|
8785
|
-
*/
|
|
8786
|
-
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
8787
|
-
try {
|
|
8788
|
-
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
8789
|
-
pageContent: diff.diff,
|
|
8790
|
-
metadata: {
|
|
8791
|
-
file: diff.file,
|
|
8792
|
-
summary: diff.summary,
|
|
8793
|
-
},
|
|
8794
|
-
})), {
|
|
8795
|
-
chain,
|
|
8796
|
-
textSplitter,
|
|
8797
|
-
options: {
|
|
8798
|
-
returnIntermediateSteps: true,
|
|
8799
|
-
},
|
|
8800
|
-
});
|
|
8801
|
-
const newTokenTotal = tokenizer(directorySummary);
|
|
8802
|
-
return {
|
|
8803
|
-
diffs: directory.diffs,
|
|
8804
|
-
path: directory.path,
|
|
8805
|
-
summary: directorySummary,
|
|
8806
|
-
tokenCount: newTokenTotal,
|
|
8807
|
-
};
|
|
8808
|
-
}
|
|
8809
|
-
catch (error) {
|
|
8810
|
-
console.error(error);
|
|
8811
|
-
return directory;
|
|
8812
|
-
}
|
|
8813
|
-
}
|
|
8814
|
-
/**
|
|
8815
|
-
* Default output formatter for directory diffs.
|
|
8816
|
-
*
|
|
8817
|
-
* TODO: Future improvements to consider:
|
|
8818
|
-
* - Hierarchical output showing file -> directory -> overall summary
|
|
8819
|
-
* - Configurable verbosity levels (compact, standard, detailed)
|
|
8820
|
-
* - Machine-readable format option (JSON) for programmatic use
|
|
8821
|
-
* - Semantic grouping by change type (added/modified/deleted) or feature area
|
|
8822
|
-
* - Visual diff indicators showing magnitude of changes
|
|
8823
|
-
*/
|
|
8824
|
-
const defaultOutputCallback = (group) => {
|
|
8825
|
-
let output = `
|
|
8826
|
-
-------\n* changes in "/${group.path}"\n\n`;
|
|
8827
|
-
if (group.summary) {
|
|
8828
|
-
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
8829
|
-
}
|
|
8830
|
-
else {
|
|
8831
|
-
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
8832
|
-
}
|
|
8833
|
-
return output;
|
|
8834
|
-
};
|
|
8758
|
+
else {
|
|
8759
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
8760
|
+
}
|
|
8761
|
+
return output;
|
|
8762
|
+
};
|
|
8835
8763
|
/**
|
|
8836
8764
|
* Process directory summarization in waves to respect concurrency limits
|
|
8837
8765
|
* while maintaining predictable behavior.
|
|
8838
8766
|
*/
|
|
8839
8767
|
async function summarizeInWaves(directories, options) {
|
|
8840
|
-
const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, } = options;
|
|
8768
|
+
const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, metadata, } = options;
|
|
8841
8769
|
let totalTokenCount = initialTotal;
|
|
8842
8770
|
const results = [...directories];
|
|
8843
8771
|
// Create sorted indices by token count (descending) for prioritized processing
|
|
@@ -8869,7 +8797,7 @@ async function summarizeInWaves(directories, options) {
|
|
|
8869
8797
|
}
|
|
8870
8798
|
logger.verbose(`\nProcessing wave of ${wave.length} directories...`, { color: 'blue' });
|
|
8871
8799
|
// Process wave in parallel
|
|
8872
|
-
const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer })));
|
|
8800
|
+
const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer, logger, metadata })));
|
|
8873
8801
|
// Update results and recalculate total
|
|
8874
8802
|
waveResults.forEach((result, i) => {
|
|
8875
8803
|
const idx = wave[i];
|
|
@@ -8906,10 +8834,25 @@ async function summarizeInWaves(directories, options) {
|
|
|
8906
8834
|
* - Efficient parallel processing with predictable behavior
|
|
8907
8835
|
* - Early exit when under token budget
|
|
8908
8836
|
*/
|
|
8909
|
-
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
8837
|
+
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, metadata, handleOutput = defaultOutputCallback, }) {
|
|
8910
8838
|
// Calculate maxFileTokens as 25% of maxTokens if not specified
|
|
8911
8839
|
const effectiveMaxFileTokens = maxFileTokens ?? Math.floor(maxTokens * 0.25);
|
|
8912
|
-
// PHASE 1:
|
|
8840
|
+
// PHASE 1: Directory grouping & assessment
|
|
8841
|
+
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
8842
|
+
let directoryDiffs = createDirectoryDiffs(rootDiffNode);
|
|
8843
|
+
// Sort by token count descending for consistent output ordering
|
|
8844
|
+
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
8845
|
+
let totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
8846
|
+
logger.stopSpinner('Diffs Organized').stopTimer();
|
|
8847
|
+
logger.verbose(`Total token count: ${totalTokenCount}, max allowed: ${maxTokens}`, {
|
|
8848
|
+
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
8849
|
+
});
|
|
8850
|
+
// Early exit if already under budget
|
|
8851
|
+
if (totalTokenCount <= maxTokens) {
|
|
8852
|
+
logger.verbose(`Already under token budget, skipping summarization.`, { color: 'green' });
|
|
8853
|
+
return directoryDiffs.map(handleOutput).join('');
|
|
8854
|
+
}
|
|
8855
|
+
// PHASE 2: Pre-process large files only when the raw diff is over budget
|
|
8913
8856
|
logger.startTimer().startSpinner(`Pre-processing large files...`, { color: 'blue' });
|
|
8914
8857
|
const preprocessedNode = await preprocessLargeFiles(rootDiffNode, {
|
|
8915
8858
|
maxFileTokens: effectiveMaxFileTokens,
|
|
@@ -8919,21 +8862,17 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
|
|
|
8919
8862
|
logger,
|
|
8920
8863
|
chain,
|
|
8921
8864
|
textSplitter,
|
|
8865
|
+
metadata,
|
|
8922
8866
|
});
|
|
8923
8867
|
logger.stopSpinner('Files pre-processed').stopTimer();
|
|
8924
|
-
|
|
8925
|
-
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
8926
|
-
const directoryDiffs = createDirectoryDiffs(preprocessedNode);
|
|
8927
|
-
// Sort by token count descending for consistent output ordering
|
|
8868
|
+
directoryDiffs = createDirectoryDiffs(preprocessedNode);
|
|
8928
8869
|
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
8929
|
-
|
|
8930
|
-
logger.
|
|
8931
|
-
logger.verbose(`Total token count: ${totalTokenCount}, max allowed: ${maxTokens}`, {
|
|
8870
|
+
totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
8871
|
+
logger.verbose(`Total token count after file pre-processing: ${totalTokenCount}`, {
|
|
8932
8872
|
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
8933
8873
|
});
|
|
8934
|
-
// Early exit if already under budget
|
|
8935
8874
|
if (totalTokenCount <= maxTokens) {
|
|
8936
|
-
logger.verbose(`
|
|
8875
|
+
logger.verbose(`Under token budget after file pre-processing.`, { color: 'green' });
|
|
8937
8876
|
return directoryDiffs.map(handleOutput).join('');
|
|
8938
8877
|
}
|
|
8939
8878
|
// PHASE 3: Wave-based summarization
|
|
@@ -8947,17 +8886,41 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
|
|
|
8947
8886
|
chain,
|
|
8948
8887
|
textSplitter,
|
|
8949
8888
|
tokenizer,
|
|
8889
|
+
metadata,
|
|
8950
8890
|
});
|
|
8951
8891
|
logger.stopSpinner(`Diffs Consolidated`).stopTimer();
|
|
8952
8892
|
return summarizedDiffs.map(handleOutput).join('');
|
|
8953
8893
|
}
|
|
8954
8894
|
|
|
8895
|
+
function createLimit(maxConcurrent) {
|
|
8896
|
+
const limit = Math.max(1, maxConcurrent);
|
|
8897
|
+
let active = 0;
|
|
8898
|
+
const queue = [];
|
|
8899
|
+
const runNext = () => {
|
|
8900
|
+
active--;
|
|
8901
|
+
const next = queue.shift();
|
|
8902
|
+
if (next)
|
|
8903
|
+
next();
|
|
8904
|
+
};
|
|
8905
|
+
return async (operation) => {
|
|
8906
|
+
if (active >= limit) {
|
|
8907
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
8908
|
+
}
|
|
8909
|
+
active++;
|
|
8910
|
+
try {
|
|
8911
|
+
return await operation();
|
|
8912
|
+
}
|
|
8913
|
+
finally {
|
|
8914
|
+
runNext();
|
|
8915
|
+
}
|
|
8916
|
+
};
|
|
8917
|
+
}
|
|
8955
8918
|
/**
|
|
8956
8919
|
* Asynchronously collect diffs for a given node and its children.
|
|
8957
8920
|
*/
|
|
8958
|
-
async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
8921
|
+
async function collectDiffs(node, getFileDiff, tokenizer, logger, maxConcurrent = 6, limit = createLimit(maxConcurrent)) {
|
|
8959
8922
|
// Collect diffs for the files of the current node
|
|
8960
|
-
const diffPromises = node.files.map(async (
|
|
8923
|
+
const diffPromises = node.files.map((nodeFile) => limit(async () => {
|
|
8961
8924
|
const diff = await getFileDiff(nodeFile);
|
|
8962
8925
|
const tokenCount = tokenizer(diff);
|
|
8963
8926
|
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
@@ -8969,9 +8932,9 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
8969
8932
|
diff,
|
|
8970
8933
|
tokenCount,
|
|
8971
8934
|
};
|
|
8972
|
-
});
|
|
8935
|
+
}));
|
|
8973
8936
|
// Collect diffs for the children of the current node
|
|
8974
|
-
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger));
|
|
8937
|
+
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger, maxConcurrent, limit));
|
|
8975
8938
|
const [diffs, children] = await Promise.all([
|
|
8976
8939
|
Promise.all(diffPromises),
|
|
8977
8940
|
Promise.all(childrenPromises),
|
|
@@ -10767,7 +10730,7 @@ var RecursiveCharacterTextSplitter = class RecursiveCharacterTextSplitter extend
|
|
|
10767
10730
|
};
|
|
10768
10731
|
|
|
10769
10732
|
//#region src/chains/summarization/stuff_prompts.ts
|
|
10770
|
-
const template$
|
|
10733
|
+
const template$3 = `Write a concise summary of the following:
|
|
10771
10734
|
|
|
10772
10735
|
|
|
10773
10736
|
"{text}"
|
|
@@ -10775,7 +10738,7 @@ const template$2 = `Write a concise summary of the following:
|
|
|
10775
10738
|
|
|
10776
10739
|
CONCISE SUMMARY:`;
|
|
10777
10740
|
const DEFAULT_PROMPT = /* @__PURE__ */ new prompts$1.PromptTemplate({
|
|
10778
|
-
template: template$
|
|
10741
|
+
template: template$3,
|
|
10779
10742
|
inputVariables: ["text"]
|
|
10780
10743
|
});
|
|
10781
10744
|
|
|
@@ -10865,7 +10828,7 @@ function isObject(subject) {
|
|
|
10865
10828
|
}
|
|
10866
10829
|
|
|
10867
10830
|
|
|
10868
|
-
function toArray(sequence) {
|
|
10831
|
+
function toArray$1(sequence) {
|
|
10869
10832
|
if (Array.isArray(sequence)) return sequence;
|
|
10870
10833
|
else if (isNothing(sequence)) return [];
|
|
10871
10834
|
|
|
@@ -10907,7 +10870,7 @@ function isNegativeZero(number) {
|
|
|
10907
10870
|
|
|
10908
10871
|
var isNothing_1 = isNothing;
|
|
10909
10872
|
var isObject_1 = isObject;
|
|
10910
|
-
var toArray_1 = toArray;
|
|
10873
|
+
var toArray_1 = toArray$1;
|
|
10911
10874
|
var repeat_1 = repeat;
|
|
10912
10875
|
var isNegativeZero_1 = isNegativeZero;
|
|
10913
10876
|
var extend_1 = extend;
|
|
@@ -11830,87 +11793,753 @@ function constructYamlSet(data) {
|
|
|
11830
11793
|
return data !== null ? data : {};
|
|
11831
11794
|
}
|
|
11832
11795
|
|
|
11833
|
-
var set = new type('tag:yaml.org,2002:set', {
|
|
11834
|
-
kind: 'mapping',
|
|
11835
|
-
resolve: resolveYamlSet,
|
|
11836
|
-
construct: constructYamlSet
|
|
11837
|
-
});
|
|
11838
|
-
|
|
11839
|
-
core.extend({
|
|
11840
|
-
implicit: [
|
|
11841
|
-
timestamp,
|
|
11842
|
-
merge
|
|
11843
|
-
],
|
|
11844
|
-
explicit: [
|
|
11845
|
-
binary,
|
|
11846
|
-
omap,
|
|
11847
|
-
pairs,
|
|
11848
|
-
set
|
|
11849
|
-
]
|
|
11850
|
-
});
|
|
11851
|
-
|
|
11852
|
-
function simpleEscapeSequence(c) {
|
|
11853
|
-
/* eslint-disable indent */
|
|
11854
|
-
return (c === 0x30/* 0 */) ? '\x00' :
|
|
11855
|
-
(c === 0x61/* a */) ? '\x07' :
|
|
11856
|
-
(c === 0x62/* b */) ? '\x08' :
|
|
11857
|
-
(c === 0x74/* t */) ? '\x09' :
|
|
11858
|
-
(c === 0x09/* Tab */) ? '\x09' :
|
|
11859
|
-
(c === 0x6E/* n */) ? '\x0A' :
|
|
11860
|
-
(c === 0x76/* v */) ? '\x0B' :
|
|
11861
|
-
(c === 0x66/* f */) ? '\x0C' :
|
|
11862
|
-
(c === 0x72/* r */) ? '\x0D' :
|
|
11863
|
-
(c === 0x65/* e */) ? '\x1B' :
|
|
11864
|
-
(c === 0x20/* Space */) ? ' ' :
|
|
11865
|
-
(c === 0x22/* " */) ? '\x22' :
|
|
11866
|
-
(c === 0x2F/* / */) ? '/' :
|
|
11867
|
-
(c === 0x5C/* \ */) ? '\x5C' :
|
|
11868
|
-
(c === 0x4E/* N */) ? '\x85' :
|
|
11869
|
-
(c === 0x5F/* _ */) ? '\xA0' :
|
|
11870
|
-
(c === 0x4C/* L */) ? '\u2028' :
|
|
11871
|
-
(c === 0x50/* P */) ? '\u2029' : '';
|
|
11796
|
+
var set = new type('tag:yaml.org,2002:set', {
|
|
11797
|
+
kind: 'mapping',
|
|
11798
|
+
resolve: resolveYamlSet,
|
|
11799
|
+
construct: constructYamlSet
|
|
11800
|
+
});
|
|
11801
|
+
|
|
11802
|
+
core.extend({
|
|
11803
|
+
implicit: [
|
|
11804
|
+
timestamp,
|
|
11805
|
+
merge
|
|
11806
|
+
],
|
|
11807
|
+
explicit: [
|
|
11808
|
+
binary,
|
|
11809
|
+
omap,
|
|
11810
|
+
pairs,
|
|
11811
|
+
set
|
|
11812
|
+
]
|
|
11813
|
+
});
|
|
11814
|
+
|
|
11815
|
+
function simpleEscapeSequence(c) {
|
|
11816
|
+
/* eslint-disable indent */
|
|
11817
|
+
return (c === 0x30/* 0 */) ? '\x00' :
|
|
11818
|
+
(c === 0x61/* a */) ? '\x07' :
|
|
11819
|
+
(c === 0x62/* b */) ? '\x08' :
|
|
11820
|
+
(c === 0x74/* t */) ? '\x09' :
|
|
11821
|
+
(c === 0x09/* Tab */) ? '\x09' :
|
|
11822
|
+
(c === 0x6E/* n */) ? '\x0A' :
|
|
11823
|
+
(c === 0x76/* v */) ? '\x0B' :
|
|
11824
|
+
(c === 0x66/* f */) ? '\x0C' :
|
|
11825
|
+
(c === 0x72/* r */) ? '\x0D' :
|
|
11826
|
+
(c === 0x65/* e */) ? '\x1B' :
|
|
11827
|
+
(c === 0x20/* Space */) ? ' ' :
|
|
11828
|
+
(c === 0x22/* " */) ? '\x22' :
|
|
11829
|
+
(c === 0x2F/* / */) ? '/' :
|
|
11830
|
+
(c === 0x5C/* \ */) ? '\x5C' :
|
|
11831
|
+
(c === 0x4E/* N */) ? '\x85' :
|
|
11832
|
+
(c === 0x5F/* _ */) ? '\xA0' :
|
|
11833
|
+
(c === 0x4C/* L */) ? '\u2028' :
|
|
11834
|
+
(c === 0x50/* P */) ? '\u2029' : '';
|
|
11835
|
+
}
|
|
11836
|
+
|
|
11837
|
+
var simpleEscapeCheck = new Array(256); // integer, for fast access
|
|
11838
|
+
var simpleEscapeMap = new Array(256);
|
|
11839
|
+
for (var i = 0; i < 256; i++) {
|
|
11840
|
+
simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0;
|
|
11841
|
+
simpleEscapeMap[i] = simpleEscapeSequence(i);
|
|
11842
|
+
}
|
|
11843
|
+
|
|
11844
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, metadata, }, }) {
|
|
11845
|
+
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
11846
|
+
const summarizationChain = loadSummarizationChain(model, {
|
|
11847
|
+
type: 'map_reduce',
|
|
11848
|
+
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
11849
|
+
combinePrompt: SUMMARIZE_PROMPT,
|
|
11850
|
+
});
|
|
11851
|
+
logger.startTimer();
|
|
11852
|
+
const rootTreeNode = createDiffTree(changes);
|
|
11853
|
+
logger.stopTimer('Created file hierarchy');
|
|
11854
|
+
// Collect diffs
|
|
11855
|
+
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
11856
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger, maxConcurrent);
|
|
11857
|
+
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
11858
|
+
// Summarize diffs using three-phase approach:
|
|
11859
|
+
// 1. Pre-process large files to prevent bias
|
|
11860
|
+
// 2. Group by directory and assess token count
|
|
11861
|
+
// 3. Wave-based parallel summarization until under budget
|
|
11862
|
+
logger.startTimer();
|
|
11863
|
+
const summary = await summarizeDiffs(diffs, {
|
|
11864
|
+
tokenizer,
|
|
11865
|
+
maxTokens: maxTokens || 2048,
|
|
11866
|
+
minTokensForSummary,
|
|
11867
|
+
maxFileTokens,
|
|
11868
|
+
maxConcurrent,
|
|
11869
|
+
textSplitter,
|
|
11870
|
+
chain: summarizationChain,
|
|
11871
|
+
logger,
|
|
11872
|
+
metadata,
|
|
11873
|
+
});
|
|
11874
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
11875
|
+
return summary;
|
|
11876
|
+
}
|
|
11877
|
+
|
|
11878
|
+
/**
|
|
11879
|
+
* Retrieves a TikToken for the specified model.
|
|
11880
|
+
*
|
|
11881
|
+
* @param {TiktokenModel} modelName - The name of the TiktokenModel.
|
|
11882
|
+
* @returns A Promise that resolves to the TikToken.
|
|
11883
|
+
*/
|
|
11884
|
+
const getTikToken = async (modelName) => {
|
|
11885
|
+
return await tiktoken.encoding_for_model(modelName);
|
|
11886
|
+
};
|
|
11887
|
+
/**
|
|
11888
|
+
* Retrieves the token counter for a given model name.
|
|
11889
|
+
*
|
|
11890
|
+
* @param {TikTokenModel} modelName - The name of the Tiktoken model.
|
|
11891
|
+
* @returns A promise that resolves to a function that calculates the number of tokens in a given text.
|
|
11892
|
+
*/
|
|
11893
|
+
const getTokenCounter = async (modelName) => {
|
|
11894
|
+
return getTikToken(modelName).then((tokenizer) => (text) => {
|
|
11895
|
+
const tokens = tokenizer.encode(text);
|
|
11896
|
+
return tokens.length;
|
|
11897
|
+
});
|
|
11898
|
+
};
|
|
11899
|
+
|
|
11900
|
+
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.
|
|
11901
|
+
|
|
11902
|
+
## Input
|
|
11903
|
+
You will be provided with a summary of changes. This summary can be one of the following:
|
|
11904
|
+
1. A list of commits, each with its author, hash, message, and body.
|
|
11905
|
+
2. A list of commits, each with its details AND the full diff of the changes.
|
|
11906
|
+
3. A single, comprehensive diff for an entire branch.
|
|
11907
|
+
|
|
11908
|
+
## Rules
|
|
11909
|
+
- Create a descriptive title for the changelog that gives a high-level overview of the changes.
|
|
11910
|
+
- **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
|
|
11911
|
+
- **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
|
|
11912
|
+
- **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
|
|
11913
|
+
- **Summaries**: For each change, provide a concise summary.
|
|
11914
|
+
- **Attribution**: {{author_instructions}}
|
|
11915
|
+
- **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
|
|
11916
|
+
- **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
|
|
11917
|
+
- **Formatting**: Your entire response must be valid Markdown.
|
|
11918
|
+
|
|
11919
|
+
## Formatting Instructions
|
|
11920
|
+
{{format_instructions}}
|
|
11921
|
+
|
|
11922
|
+
{{additional_context}}
|
|
11923
|
+
|
|
11924
|
+
"""{{summary}}"""`;
|
|
11925
|
+
const inputVariables$2 = [
|
|
11926
|
+
'format_instructions',
|
|
11927
|
+
'summary',
|
|
11928
|
+
'additional_context',
|
|
11929
|
+
'author_instructions',
|
|
11930
|
+
];
|
|
11931
|
+
const CHANGELOG_PROMPT = new prompts$1.PromptTemplate({
|
|
11932
|
+
template: template$2,
|
|
11933
|
+
inputVariables: inputVariables$2,
|
|
11934
|
+
});
|
|
11935
|
+
|
|
11936
|
+
async function processInWaves(items, processor, maxConcurrent = 6) {
|
|
11937
|
+
const results = [];
|
|
11938
|
+
const limit = Math.max(1, maxConcurrent);
|
|
11939
|
+
for (let i = 0; i < items.length; i += limit) {
|
|
11940
|
+
const waveResults = await Promise.all(items.slice(i, i + limit).map(processor));
|
|
11941
|
+
results.push(...waveResults);
|
|
11942
|
+
}
|
|
11943
|
+
return results;
|
|
11944
|
+
}
|
|
11945
|
+
const handler$5 = async (argv, logger) => {
|
|
11946
|
+
const config = loadConfig(argv);
|
|
11947
|
+
const git = getRepo();
|
|
11948
|
+
const key = getApiKeyForModel(config);
|
|
11949
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
11950
|
+
const changelogService = resolveDynamicService(config, 'changelog');
|
|
11951
|
+
const summaryService = resolveDynamicService(config, argv.withDiff || argv.onlyDiff ? 'largeDiff' : 'summarize');
|
|
11952
|
+
const model = changelogService.model;
|
|
11953
|
+
const exclusiveOptions = [
|
|
11954
|
+
argv.branch ? '--branch' : null,
|
|
11955
|
+
argv.tag ? '--tag' : null,
|
|
11956
|
+
config.sinceLastTag ? '--since-last-tag' : null,
|
|
11957
|
+
].filter(Boolean);
|
|
11958
|
+
if (exclusiveOptions.length > 1) {
|
|
11959
|
+
logger.log(`Options ${exclusiveOptions.join(', ')} cannot be used together.`, { color: 'red' });
|
|
11960
|
+
process.exit(1);
|
|
11961
|
+
}
|
|
11962
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
11963
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
11964
|
+
process.exit(1);
|
|
11965
|
+
}
|
|
11966
|
+
const llm = getLlm(provider, model, { ...config, service: changelogService });
|
|
11967
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
11968
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
11969
|
+
const INTERACTIVE = isInteractive(config);
|
|
11970
|
+
if (INTERACTIVE) {
|
|
11971
|
+
if (!config.hideCocoBanner) {
|
|
11972
|
+
logger.log(LOGO);
|
|
11973
|
+
}
|
|
11974
|
+
}
|
|
11975
|
+
async function factory() {
|
|
11976
|
+
const branchName = await getCurrentBranchName({ git });
|
|
11977
|
+
if (argv.onlyDiff) {
|
|
11978
|
+
const baseBranch = argv.branch || config.defaultBranch || 'main';
|
|
11979
|
+
logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
|
|
11980
|
+
const diff = await getDiffForBranch({ git, logger, baseBranch, headBranch: branchName });
|
|
11981
|
+
return {
|
|
11982
|
+
branch: branchName,
|
|
11983
|
+
diffChanges: diff.staged,
|
|
11984
|
+
diffCommit: `${baseBranch}..${branchName}`,
|
|
11985
|
+
};
|
|
11986
|
+
}
|
|
11987
|
+
let commits = [];
|
|
11988
|
+
if (config.sinceLastTag) {
|
|
11989
|
+
logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
|
|
11990
|
+
// This function returns string[], needs to be adapted or replaced
|
|
11991
|
+
// For now, this path will have limited details.
|
|
11992
|
+
const commitMessages = await getChangesSinceLastTag({ git});
|
|
11993
|
+
commits = commitMessages.map(msg => ({ message: msg }));
|
|
11994
|
+
}
|
|
11995
|
+
else if (config.range && config.range.includes(':')) {
|
|
11996
|
+
const [from, to] = config.range.split(':');
|
|
11997
|
+
if (!from || !to) {
|
|
11998
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
11999
|
+
process.exit(1);
|
|
12000
|
+
}
|
|
12001
|
+
commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
|
|
12002
|
+
}
|
|
12003
|
+
else if (argv.branch) {
|
|
12004
|
+
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
12005
|
+
commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
|
|
12006
|
+
}
|
|
12007
|
+
else if (argv.tag) {
|
|
12008
|
+
logger.verbose(`Generating commit log against tag: ${argv.tag}`, { color: 'yellow' });
|
|
12009
|
+
commits = await getCommitLogAgainstTag({ git, logger, targetTag: argv.tag });
|
|
12010
|
+
}
|
|
12011
|
+
else {
|
|
12012
|
+
logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
|
|
12013
|
+
color: 'yellow',
|
|
12014
|
+
});
|
|
12015
|
+
commits = await getCommitLogCurrentBranch({ git, logger });
|
|
12016
|
+
}
|
|
12017
|
+
let commitsWithDiffText = commits;
|
|
12018
|
+
if (argv.withDiff) {
|
|
12019
|
+
commitsWithDiffText = await processInWaves(commits, async (commit) => {
|
|
12020
|
+
const changes = await getChangesByCommit({
|
|
12021
|
+
commit: commit.hash,
|
|
12022
|
+
options: {
|
|
12023
|
+
git,
|
|
12024
|
+
ignoredFiles: config.ignoredFiles || undefined,
|
|
12025
|
+
ignoredExtensions: config.ignoredExtensions || undefined,
|
|
12026
|
+
},
|
|
12027
|
+
});
|
|
12028
|
+
return {
|
|
12029
|
+
...commit,
|
|
12030
|
+
diffText: changes.length > 0
|
|
12031
|
+
? await fileChangeParser({
|
|
12032
|
+
changes,
|
|
12033
|
+
commit: `${commit.hash}^..${commit.hash}`,
|
|
12034
|
+
options: {
|
|
12035
|
+
tokenizer,
|
|
12036
|
+
git,
|
|
12037
|
+
llm: summaryLlm,
|
|
12038
|
+
logger,
|
|
12039
|
+
maxTokens: config.service.tokenLimit,
|
|
12040
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
12041
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
12042
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
12043
|
+
metadata: {
|
|
12044
|
+
command: 'changelog',
|
|
12045
|
+
provider,
|
|
12046
|
+
model: String(summaryService.model),
|
|
12047
|
+
},
|
|
12048
|
+
},
|
|
12049
|
+
})
|
|
12050
|
+
: undefined,
|
|
12051
|
+
};
|
|
12052
|
+
}, config.service.maxConcurrent);
|
|
12053
|
+
}
|
|
12054
|
+
return {
|
|
12055
|
+
branch: branchName,
|
|
12056
|
+
commits: commitsWithDiffText,
|
|
12057
|
+
withDiff: argv.withDiff,
|
|
12058
|
+
};
|
|
12059
|
+
}
|
|
12060
|
+
async function parser(data) {
|
|
12061
|
+
if (data.diffChanges && data.diffCommit) {
|
|
12062
|
+
const diffSummary = await fileChangeParser({
|
|
12063
|
+
changes: data.diffChanges,
|
|
12064
|
+
commit: data.diffCommit,
|
|
12065
|
+
options: {
|
|
12066
|
+
tokenizer,
|
|
12067
|
+
git,
|
|
12068
|
+
llm: summaryLlm,
|
|
12069
|
+
logger,
|
|
12070
|
+
maxTokens: config.service.tokenLimit,
|
|
12071
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
12072
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
12073
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
12074
|
+
metadata: {
|
|
12075
|
+
command: 'changelog',
|
|
12076
|
+
provider,
|
|
12077
|
+
model: String(summaryService.model),
|
|
12078
|
+
},
|
|
12079
|
+
},
|
|
12080
|
+
});
|
|
12081
|
+
return `## Diff for ${data.branch}\n\n${diffSummary}`;
|
|
12082
|
+
}
|
|
12083
|
+
if (!data.commits || data.commits.length === 0) {
|
|
12084
|
+
return `## ${data.branch}\n\nNo commits found.`;
|
|
12085
|
+
}
|
|
12086
|
+
let result = `## ${data.branch}\n\n`;
|
|
12087
|
+
result += data.commits.map(commit => {
|
|
12088
|
+
let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
|
|
12089
|
+
if (data.withDiff && commit.diffText) {
|
|
12090
|
+
commitStr += `\nDiff:\n${commit.diffText}`;
|
|
12091
|
+
}
|
|
12092
|
+
return commitStr.trim();
|
|
12093
|
+
}).join('\n\n---\n\n');
|
|
12094
|
+
return result;
|
|
12095
|
+
}
|
|
12096
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
12097
|
+
label: 'changelog',
|
|
12098
|
+
options: {
|
|
12099
|
+
...config,
|
|
12100
|
+
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
12101
|
+
logger,
|
|
12102
|
+
interactive: INTERACTIVE,
|
|
12103
|
+
review: {
|
|
12104
|
+
enableFullRetry: false,
|
|
12105
|
+
},
|
|
12106
|
+
},
|
|
12107
|
+
factory,
|
|
12108
|
+
parser,
|
|
12109
|
+
agent: async (context, options) => {
|
|
12110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12111
|
+
const parser = createSchemaParser(ChangelogResponseSchema, llm);
|
|
12112
|
+
const prompt = getPrompt({
|
|
12113
|
+
template: options.prompt,
|
|
12114
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
12115
|
+
fallback: CHANGELOG_PROMPT,
|
|
12116
|
+
});
|
|
12117
|
+
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.";
|
|
12118
|
+
let additional_context = '';
|
|
12119
|
+
if (argv.additional) {
|
|
12120
|
+
additional_context = `## Additional Context\n${argv.additional}`;
|
|
12121
|
+
}
|
|
12122
|
+
const author_instructions = argv.author
|
|
12123
|
+
? '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.'
|
|
12124
|
+
: 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
|
|
12125
|
+
const variables = {
|
|
12126
|
+
summary: context,
|
|
12127
|
+
format_instructions: formatInstructions,
|
|
12128
|
+
additional_context: additional_context,
|
|
12129
|
+
author_instructions: author_instructions,
|
|
12130
|
+
};
|
|
12131
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
12132
|
+
prompt,
|
|
12133
|
+
variables,
|
|
12134
|
+
tokenizer,
|
|
12135
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
12136
|
+
});
|
|
12137
|
+
if (budgetedPrompt.truncated) {
|
|
12138
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
12139
|
+
}
|
|
12140
|
+
const changelog = await executeChain({
|
|
12141
|
+
llm,
|
|
12142
|
+
prompt,
|
|
12143
|
+
variables: budgetedPrompt.variables,
|
|
12144
|
+
parser,
|
|
12145
|
+
logger,
|
|
12146
|
+
tokenizer,
|
|
12147
|
+
metadata: {
|
|
12148
|
+
task: argv.withDiff ? 'changelog-with-diff' : argv.onlyDiff ? 'changelog-only-diff' : 'changelog',
|
|
12149
|
+
command: 'changelog',
|
|
12150
|
+
provider,
|
|
12151
|
+
model: String(model),
|
|
12152
|
+
},
|
|
12153
|
+
});
|
|
12154
|
+
const branchName = await getCurrentBranchName({ git });
|
|
12155
|
+
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
12156
|
+
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
12157
|
+
return `${changelog.title}\n\n${changelog.content}${footer}`;
|
|
12158
|
+
},
|
|
12159
|
+
noResult: async () => {
|
|
12160
|
+
if (config.range) {
|
|
12161
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
12162
|
+
process.exit(0);
|
|
12163
|
+
}
|
|
12164
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
12165
|
+
process.exit(0);
|
|
12166
|
+
},
|
|
12167
|
+
});
|
|
12168
|
+
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
12169
|
+
handleResult({
|
|
12170
|
+
result: changelogMsg,
|
|
12171
|
+
interactiveModeCallback: async () => {
|
|
12172
|
+
logSuccess();
|
|
12173
|
+
},
|
|
12174
|
+
mode: MODE,
|
|
12175
|
+
});
|
|
12176
|
+
logLlmTelemetrySummary(logger, 'changelog');
|
|
12177
|
+
};
|
|
12178
|
+
|
|
12179
|
+
var changelog = {
|
|
12180
|
+
command: command$5,
|
|
12181
|
+
desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
|
|
12182
|
+
builder: builder$5,
|
|
12183
|
+
handler: commandExecutor(handler$5),
|
|
12184
|
+
options: options$5,
|
|
12185
|
+
};
|
|
12186
|
+
|
|
12187
|
+
const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
|
|
12188
|
+
// Regular commit message schema with basic validation
|
|
12189
|
+
const CommitMessageResponseSchema = objectType({
|
|
12190
|
+
title: stringType().describe("Title of the commit message"),
|
|
12191
|
+
body: stringType().describe("Body of the commit message"),
|
|
12192
|
+
}).describe("Object with commit message 'title' and 'body'");
|
|
12193
|
+
// Conventional commit message schema with strict formatting rules
|
|
12194
|
+
const ConventionalCommitMessageResponseSchema = objectType({
|
|
12195
|
+
title: stringType()
|
|
12196
|
+
.max(50, "Title must be 50 characters or less")
|
|
12197
|
+
.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"),
|
|
12198
|
+
body: stringType().describe("Body of the commit message")
|
|
12199
|
+
// .max(280, "Body must be 280 characters or less"),
|
|
12200
|
+
}).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
|
|
12201
|
+
const command$4 = 'commit';
|
|
12202
|
+
/**
|
|
12203
|
+
* Command line options via yargs
|
|
12204
|
+
*/
|
|
12205
|
+
const options$4 = {
|
|
12206
|
+
i: {
|
|
12207
|
+
alias: 'interactive',
|
|
12208
|
+
description: 'Toggle interactive mode',
|
|
12209
|
+
type: 'boolean',
|
|
12210
|
+
},
|
|
12211
|
+
ignoredFiles: {
|
|
12212
|
+
description: 'Ignored files',
|
|
12213
|
+
type: 'array',
|
|
12214
|
+
},
|
|
12215
|
+
ignoredExtensions: {
|
|
12216
|
+
description: 'Ignored extensions',
|
|
12217
|
+
type: 'array',
|
|
12218
|
+
},
|
|
12219
|
+
append: {
|
|
12220
|
+
description: 'Add content to the end of the generated commit message',
|
|
12221
|
+
type: 'string',
|
|
12222
|
+
},
|
|
12223
|
+
appendTicket: {
|
|
12224
|
+
description: 'Append ticket ID from branch name to the commit message',
|
|
12225
|
+
type: 'boolean',
|
|
12226
|
+
alias: 't',
|
|
12227
|
+
},
|
|
12228
|
+
additional: {
|
|
12229
|
+
description: 'Add extra contextual information to the prompt',
|
|
12230
|
+
type: 'string',
|
|
12231
|
+
alias: 'a',
|
|
12232
|
+
},
|
|
12233
|
+
withPreviousCommits: {
|
|
12234
|
+
description: 'Include previous commits as context (specify number of commits, 0 for none)',
|
|
12235
|
+
type: 'number',
|
|
12236
|
+
default: 0,
|
|
12237
|
+
alias: 'p',
|
|
12238
|
+
},
|
|
12239
|
+
conventional: {
|
|
12240
|
+
description: 'Generate commit message in Conventional Commits format',
|
|
12241
|
+
type: 'boolean',
|
|
12242
|
+
default: false,
|
|
12243
|
+
alias: 'c',
|
|
12244
|
+
},
|
|
12245
|
+
includeBranchName: {
|
|
12246
|
+
description: 'Include the current branch name in the commit prompt for context',
|
|
12247
|
+
type: 'boolean',
|
|
12248
|
+
default: true,
|
|
12249
|
+
},
|
|
12250
|
+
noDiff: {
|
|
12251
|
+
description: 'Only pass basic "git status" result instead of providing entire diff',
|
|
12252
|
+
type: 'boolean',
|
|
12253
|
+
default: false,
|
|
12254
|
+
},
|
|
12255
|
+
noVerify: {
|
|
12256
|
+
description: 'Skip pre-commit and commit-msg hooks (passes --no-verify to git commit)',
|
|
12257
|
+
type: 'boolean',
|
|
12258
|
+
default: false,
|
|
12259
|
+
alias: 'n',
|
|
12260
|
+
},
|
|
12261
|
+
split: {
|
|
12262
|
+
description: 'Group staged changes into multiple related commit proposals',
|
|
12263
|
+
type: 'boolean',
|
|
12264
|
+
default: false,
|
|
12265
|
+
},
|
|
12266
|
+
plan: {
|
|
12267
|
+
description: 'Only print a commit split plan without changing git state',
|
|
12268
|
+
type: 'boolean',
|
|
12269
|
+
default: false,
|
|
12270
|
+
},
|
|
12271
|
+
apply: {
|
|
12272
|
+
description: 'Apply a generated file-level or hunk-level commit split plan and create commits',
|
|
12273
|
+
type: 'boolean',
|
|
12274
|
+
default: false,
|
|
12275
|
+
},
|
|
12276
|
+
};
|
|
12277
|
+
const builder$4 = (yargs) => {
|
|
12278
|
+
return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
|
|
12279
|
+
};
|
|
12280
|
+
|
|
12281
|
+
/**
|
|
12282
|
+
* High-level function that combines chain execution with schema-based parsing
|
|
12283
|
+
* Includes automatic retry logic and graceful degradation
|
|
12284
|
+
* @param schema - Zod schema for the expected output structure
|
|
12285
|
+
* @param llm - LLM instance
|
|
12286
|
+
* @param prompt - Prompt template
|
|
12287
|
+
* @param variables - Variables for the prompt
|
|
12288
|
+
* @param options - Configuration options
|
|
12289
|
+
* @returns Parsed result matching the schema type
|
|
12290
|
+
*/
|
|
12291
|
+
async function executeChainWithSchema(schema, llm, prompt, variables, options = {}) {
|
|
12292
|
+
const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, logger, tokenizer, metadata, ...parserOptions } = options;
|
|
12293
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12294
|
+
const parser = createSchemaParser(schema, llm, parserOptions);
|
|
12295
|
+
let attempt = 0;
|
|
12296
|
+
const operation = async () => {
|
|
12297
|
+
attempt++;
|
|
12298
|
+
const result = await executeChain({
|
|
12299
|
+
llm,
|
|
12300
|
+
prompt,
|
|
12301
|
+
variables,
|
|
12302
|
+
parser,
|
|
12303
|
+
logger,
|
|
12304
|
+
tokenizer,
|
|
12305
|
+
metadata: {
|
|
12306
|
+
task: 'schema-chain',
|
|
12307
|
+
...metadata,
|
|
12308
|
+
retryAttempt: attempt,
|
|
12309
|
+
},
|
|
12310
|
+
});
|
|
12311
|
+
return result;
|
|
12312
|
+
};
|
|
12313
|
+
try {
|
|
12314
|
+
return await withRetry(operation, retryOptions);
|
|
12315
|
+
}
|
|
12316
|
+
catch (error) {
|
|
12317
|
+
if (fallbackParser) {
|
|
12318
|
+
if (onFallback) {
|
|
12319
|
+
onFallback();
|
|
12320
|
+
}
|
|
12321
|
+
const fallbackResult = await executeChain({
|
|
12322
|
+
llm,
|
|
12323
|
+
prompt,
|
|
12324
|
+
variables,
|
|
12325
|
+
parser: new output_parsers.StringOutputParser(),
|
|
12326
|
+
logger,
|
|
12327
|
+
tokenizer,
|
|
12328
|
+
metadata: {
|
|
12329
|
+
task: 'schema-chain-fallback',
|
|
12330
|
+
...metadata,
|
|
12331
|
+
},
|
|
12332
|
+
});
|
|
12333
|
+
const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
|
|
12334
|
+
return fallbackParser(fallbackText);
|
|
12335
|
+
}
|
|
12336
|
+
// No fallback available, re-throw the error
|
|
12337
|
+
throw error;
|
|
12338
|
+
}
|
|
12339
|
+
}
|
|
12340
|
+
|
|
12341
|
+
/**
|
|
12342
|
+
* Utility to repair common JSON formatting issues that LLMs make
|
|
12343
|
+
* Specifically handles cases where string values are not properly quoted
|
|
12344
|
+
*/
|
|
12345
|
+
function repairJson(jsonString) {
|
|
12346
|
+
// Remove any markdown code block wrapping
|
|
12347
|
+
let cleaned = jsonString.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, '$1').trim();
|
|
12348
|
+
// Remove inline code block wrapping
|
|
12349
|
+
cleaned = cleaned.replace(/^`(.*)`$/, '$1').trim();
|
|
12350
|
+
// If it doesn't look like JSON, return as-is
|
|
12351
|
+
if (!cleaned.startsWith('{') || !cleaned.endsWith('}')) {
|
|
12352
|
+
return jsonString;
|
|
12353
|
+
}
|
|
12354
|
+
try {
|
|
12355
|
+
// First try parsing as-is
|
|
12356
|
+
JSON.parse(cleaned);
|
|
12357
|
+
return cleaned;
|
|
12358
|
+
}
|
|
12359
|
+
catch {
|
|
12360
|
+
// Try to repair common issues
|
|
12361
|
+
let repaired = cleaned;
|
|
12362
|
+
// Fix unquoted string values in title and body fields
|
|
12363
|
+
// Pattern: "title": unquoted_value, -> "title": "unquoted_value",
|
|
12364
|
+
repaired = repaired.replace(/"(title|body)":\s*([^",\{\}\[\]]+?)(?=\s*[,\}])/g, (match, field, value) => {
|
|
12365
|
+
// Clean up the value (remove leading/trailing whitespace)
|
|
12366
|
+
const cleanValue = value.trim();
|
|
12367
|
+
// If it's already quoted or looks like a number/boolean, leave it
|
|
12368
|
+
if (cleanValue.startsWith('"') || /^(true|false|\d+)$/.test(cleanValue)) {
|
|
12369
|
+
return match;
|
|
12370
|
+
}
|
|
12371
|
+
// Quote the value
|
|
12372
|
+
return `"${field}": "${cleanValue}"`;
|
|
12373
|
+
});
|
|
12374
|
+
// Fix missing quotes around field names (though this should be rare)
|
|
12375
|
+
repaired = repaired.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
12376
|
+
// Remove trailing commas before closing braces
|
|
12377
|
+
repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
|
|
12378
|
+
try {
|
|
12379
|
+
// Test if the repair worked
|
|
12380
|
+
JSON.parse(repaired);
|
|
12381
|
+
return repaired;
|
|
12382
|
+
}
|
|
12383
|
+
catch {
|
|
12384
|
+
// If repair failed, return original
|
|
12385
|
+
return jsonString;
|
|
12386
|
+
}
|
|
12387
|
+
}
|
|
12388
|
+
}
|
|
12389
|
+
|
|
12390
|
+
/**
|
|
12391
|
+
* Extract the first complete JSON object from a string by tracking balanced braces
|
|
12392
|
+
*/
|
|
12393
|
+
function extractFirstJsonObject(text) {
|
|
12394
|
+
const startIndex = text.indexOf('{');
|
|
12395
|
+
if (startIndex === -1)
|
|
12396
|
+
return null;
|
|
12397
|
+
let braceCount = 0;
|
|
12398
|
+
let inString = false;
|
|
12399
|
+
let escapeNext = false;
|
|
12400
|
+
for (let i = startIndex; i < text.length; i++) {
|
|
12401
|
+
const char = text[i];
|
|
12402
|
+
if (escapeNext) {
|
|
12403
|
+
escapeNext = false;
|
|
12404
|
+
continue;
|
|
12405
|
+
}
|
|
12406
|
+
if (char === '\\') {
|
|
12407
|
+
escapeNext = true;
|
|
12408
|
+
continue;
|
|
12409
|
+
}
|
|
12410
|
+
if (char === '"') {
|
|
12411
|
+
inString = !inString;
|
|
12412
|
+
continue;
|
|
12413
|
+
}
|
|
12414
|
+
if (inString)
|
|
12415
|
+
continue;
|
|
12416
|
+
if (char === '{') {
|
|
12417
|
+
braceCount++;
|
|
12418
|
+
}
|
|
12419
|
+
else if (char === '}') {
|
|
12420
|
+
braceCount--;
|
|
12421
|
+
if (braceCount === 0) {
|
|
12422
|
+
// Found the end of the first complete JSON object
|
|
12423
|
+
return text.substring(startIndex, i + 1);
|
|
12424
|
+
}
|
|
12425
|
+
}
|
|
12426
|
+
}
|
|
12427
|
+
return null;
|
|
11872
12428
|
}
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
12429
|
+
/**
|
|
12430
|
+
* Utility function to ensure commit messages are properly formatted as strings
|
|
12431
|
+
* rather than JSON objects, whether they come as parsed objects or stringified JSON
|
|
12432
|
+
*/
|
|
12433
|
+
function formatCommitMessage(result, options = {}) {
|
|
12434
|
+
const { append, ticketId, appendTicket } = options;
|
|
12435
|
+
// Helper function to construct the final message with appends
|
|
12436
|
+
const constructMessage = (title, body) => {
|
|
12437
|
+
const appendedText = append ? `\n\n${append}` : '';
|
|
12438
|
+
const ticketFooter = appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
12439
|
+
return `${title}\n\n${body}${appendedText}${ticketFooter}`;
|
|
12440
|
+
};
|
|
12441
|
+
// If it's a string, check if it contains a JSON object (including markdown code blocks)
|
|
12442
|
+
if (typeof result === 'string') {
|
|
12443
|
+
// Early return if string clearly doesn't contain JSON-like content
|
|
12444
|
+
if (!result.includes('{') && !result.includes('"title"')) {
|
|
12445
|
+
return result;
|
|
12446
|
+
}
|
|
12447
|
+
// Handle multiple markdown code block formats and embedded JSON
|
|
12448
|
+
const extractionPatterns = [
|
|
12449
|
+
/```(?:json)?\s*(\{[\s\S]*?\})\s*```/, // Standard markdown blocks
|
|
12450
|
+
/`(\{[\s\S]*?\})`/, // Inline code blocks
|
|
12451
|
+
/^\s*(\{[\s\S]*\})\s*$/, // Raw JSON without blocks (entire string)
|
|
12452
|
+
/(\{[\s\S]*?\})/ // JSON anywhere in text (fallback)
|
|
12453
|
+
];
|
|
12454
|
+
let jsonString = result;
|
|
12455
|
+
let foundMatch = false;
|
|
12456
|
+
// Try each pattern to extract JSON
|
|
12457
|
+
for (const pattern of extractionPatterns) {
|
|
12458
|
+
const match = result.match(pattern);
|
|
12459
|
+
if (match && match[1]) {
|
|
12460
|
+
jsonString = match[1].trim();
|
|
12461
|
+
foundMatch = true;
|
|
12462
|
+
break;
|
|
12463
|
+
}
|
|
12464
|
+
}
|
|
12465
|
+
// Only attempt JSON parsing if we found potential JSON content
|
|
12466
|
+
if (foundMatch || jsonString.startsWith('{')) {
|
|
12467
|
+
try {
|
|
12468
|
+
// Try to parse as JSON to see if it's a stringified object
|
|
12469
|
+
const parsed = JSON.parse(jsonString);
|
|
12470
|
+
if (parsed &&
|
|
12471
|
+
typeof parsed === 'object' &&
|
|
12472
|
+
typeof parsed.title === 'string' &&
|
|
12473
|
+
typeof parsed.body === 'string' &&
|
|
12474
|
+
parsed.title.length > 0 &&
|
|
12475
|
+
parsed.body.length > 0) {
|
|
12476
|
+
// It's a valid stringified JSON object, format it properly
|
|
12477
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12480
|
+
catch {
|
|
12481
|
+
// Try to repair the JSON and parse again
|
|
12482
|
+
try {
|
|
12483
|
+
const repairedJson = repairJson(jsonString);
|
|
12484
|
+
const parsed = JSON.parse(repairedJson);
|
|
12485
|
+
if (parsed &&
|
|
12486
|
+
typeof parsed === 'object' &&
|
|
12487
|
+
typeof parsed.title === 'string' &&
|
|
12488
|
+
typeof parsed.body === 'string' &&
|
|
12489
|
+
parsed.title.length > 0 &&
|
|
12490
|
+
parsed.body.length > 0) {
|
|
12491
|
+
// Successfully repaired and parsed JSON
|
|
12492
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12493
|
+
}
|
|
12494
|
+
}
|
|
12495
|
+
catch {
|
|
12496
|
+
// Repair failed, try extracting just the first complete JSON object
|
|
12497
|
+
const firstObject = extractFirstJsonObject(jsonString);
|
|
12498
|
+
if (firstObject) {
|
|
12499
|
+
try {
|
|
12500
|
+
const parsed = JSON.parse(firstObject);
|
|
12501
|
+
if (parsed &&
|
|
12502
|
+
typeof parsed === 'object' &&
|
|
12503
|
+
typeof parsed.title === 'string' &&
|
|
12504
|
+
typeof parsed.body === 'string' &&
|
|
12505
|
+
parsed.title.length > 0 &&
|
|
12506
|
+
parsed.body.length > 0) {
|
|
12507
|
+
return constructMessage(parsed.title, parsed.body);
|
|
12508
|
+
}
|
|
12509
|
+
}
|
|
12510
|
+
catch {
|
|
12511
|
+
// Even first object extraction failed, continue to fallback
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12517
|
+
// If no JSON found and it's already formatted, return as-is
|
|
12518
|
+
return result;
|
|
12519
|
+
}
|
|
12520
|
+
// If it's already an object with title and body, format it
|
|
12521
|
+
if (typeof result === 'object' && result !== null &&
|
|
12522
|
+
'title' in result && 'body' in result) {
|
|
12523
|
+
const commitMsgObj = result;
|
|
12524
|
+
if (typeof commitMsgObj.title === 'string' && typeof commitMsgObj.body === 'string') {
|
|
12525
|
+
return constructMessage(commitMsgObj.title, commitMsgObj.body);
|
|
12526
|
+
}
|
|
12527
|
+
}
|
|
12528
|
+
// Fallback - convert to string and return as-is
|
|
12529
|
+
return String(result);
|
|
11879
12530
|
}
|
|
11880
12531
|
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
// Collect diffs
|
|
11892
|
-
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
11893
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
11894
|
-
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
11895
|
-
// Summarize diffs using three-phase approach:
|
|
11896
|
-
// 1. Pre-process large files to prevent bias
|
|
11897
|
-
// 2. Group by directory and assess token count
|
|
11898
|
-
// 3. Wave-based parallel summarization until under budget
|
|
11899
|
-
logger.startTimer();
|
|
11900
|
-
const summary = await summarizeDiffs(diffs, {
|
|
11901
|
-
tokenizer,
|
|
11902
|
-
maxTokens: maxTokens || 2048,
|
|
11903
|
-
minTokensForSummary,
|
|
11904
|
-
maxFileTokens,
|
|
11905
|
-
maxConcurrent,
|
|
11906
|
-
textSplitter,
|
|
11907
|
-
chain: summarizationChain,
|
|
11908
|
-
logger,
|
|
11909
|
-
});
|
|
11910
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
11911
|
-
return summary;
|
|
12532
|
+
/**
|
|
12533
|
+
* Error thrown when a pre-commit hook blocks a commit (e.g. a linter exits non-zero).
|
|
12534
|
+
* Contains the raw hook output so callers can present it cleanly to the user.
|
|
12535
|
+
*/
|
|
12536
|
+
class PreCommitHookError extends Error {
|
|
12537
|
+
constructor(hookOutput) {
|
|
12538
|
+
super('Pre-commit hook failed');
|
|
12539
|
+
this.name = 'PreCommitHookError';
|
|
12540
|
+
this.hookOutput = hookOutput;
|
|
12541
|
+
}
|
|
11912
12542
|
}
|
|
11913
|
-
|
|
11914
12543
|
/**
|
|
11915
12544
|
* Detects whether a GitError was caused by a pre-commit hook that modified files.
|
|
11916
12545
|
* These hooks (e.g. black, prettier) reformat files and exit non-zero on the first run,
|
|
@@ -11930,15 +12559,19 @@ function isPreCommitHookModifiedFilesError(error) {
|
|
|
11930
12559
|
* Creates a commit with the specified commit message.
|
|
11931
12560
|
* Handles the case where pre-commit hooks modify files (e.g. black, prettier):
|
|
11932
12561
|
* when detected, stages the hook-modified files and retries the commit once.
|
|
12562
|
+
* Any other GitError (e.g. hook lint failure) is wrapped in a PreCommitHookError
|
|
12563
|
+
* so callers can present it cleanly instead of showing a raw stack trace.
|
|
11933
12564
|
*
|
|
11934
12565
|
* @param message The commit message.
|
|
11935
12566
|
* @param git The SimpleGit instance.
|
|
11936
12567
|
* @param onHookModifiedFiles Optional callback invoked before the auto-retry so callers can notify the user.
|
|
12568
|
+
* @param options Optional commit options (e.g. noVerify).
|
|
11937
12569
|
* @returns A Promise that resolves to the CommitResult.
|
|
11938
12570
|
*/
|
|
11939
|
-
async function createCommit(message, git, onHookModifiedFiles) {
|
|
12571
|
+
async function createCommit(message, git, onHookModifiedFiles, options) {
|
|
12572
|
+
const flags = options?.noVerify ? ['--no-verify'] : [];
|
|
11940
12573
|
try {
|
|
11941
|
-
return await git.commit(message);
|
|
12574
|
+
return await git.commit(message, flags);
|
|
11942
12575
|
}
|
|
11943
12576
|
catch (error) {
|
|
11944
12577
|
if (isPreCommitHookModifiedFilesError(error)) {
|
|
@@ -11948,7 +12581,12 @@ async function createCommit(message, git, onHookModifiedFiles) {
|
|
|
11948
12581
|
}
|
|
11949
12582
|
// Stage all hook-modified files and retry the commit once
|
|
11950
12583
|
await git.add('.');
|
|
11951
|
-
return await git.commit(message);
|
|
12584
|
+
return await git.commit(message, flags);
|
|
12585
|
+
}
|
|
12586
|
+
// Wrap any other GitError so callers can present it cleanly rather than
|
|
12587
|
+
// showing a raw Node.js stack trace originating from simple-git internals.
|
|
12588
|
+
if (error instanceof simpleGit.GitError) {
|
|
12589
|
+
throw new PreCommitHookError(error.message);
|
|
11952
12590
|
}
|
|
11953
12591
|
throw error;
|
|
11954
12592
|
}
|
|
@@ -11961,7 +12599,7 @@ async function createCommit(message, git, onHookModifiedFiles) {
|
|
|
11961
12599
|
* @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
|
|
11962
12600
|
*/
|
|
11963
12601
|
async function getChanges({ git, options }) {
|
|
11964
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
12602
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES$1, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS$1 } = options || {};
|
|
11965
12603
|
const staged = [];
|
|
11966
12604
|
const unstaged = [];
|
|
11967
12605
|
const untracked = [];
|
|
@@ -12041,41 +12679,19 @@ async function getPreviousCommits(options) {
|
|
|
12041
12679
|
try {
|
|
12042
12680
|
const logs = await git.log({ maxCount: count });
|
|
12043
12681
|
if (!logs || logs.total === 0) {
|
|
12044
|
-
return '';
|
|
12045
|
-
}
|
|
12046
|
-
// Format the commit logs
|
|
12047
|
-
const formattedLogs = logs.all.map((commit) => {
|
|
12048
|
-
return formatSingleCommit(commit);
|
|
12049
|
-
}).join('\n\n');
|
|
12050
|
-
return formattedLogs;
|
|
12051
|
-
}
|
|
12052
|
-
catch (error) {
|
|
12053
|
-
console.error(`Error getting previous commits: ${error.message}`);
|
|
12054
|
-
return '';
|
|
12055
|
-
}
|
|
12056
|
-
}
|
|
12057
|
-
|
|
12058
|
-
/**
|
|
12059
|
-
* Retrieves a TikToken for the specified model.
|
|
12060
|
-
*
|
|
12061
|
-
* @param {TiktokenModel} modelName - The name of the TiktokenModel.
|
|
12062
|
-
* @returns A Promise that resolves to the TikToken.
|
|
12063
|
-
*/
|
|
12064
|
-
const getTikToken = async (modelName) => {
|
|
12065
|
-
return await tiktoken.encoding_for_model(modelName);
|
|
12066
|
-
};
|
|
12067
|
-
/**
|
|
12068
|
-
* Retrieves the token counter for a given model name.
|
|
12069
|
-
*
|
|
12070
|
-
* @param {TikTokenModel} modelName - The name of the Tiktoken model.
|
|
12071
|
-
* @returns A promise that resolves to a function that calculates the number of tokens in a given text.
|
|
12072
|
-
*/
|
|
12073
|
-
const getTokenCounter = async (modelName) => {
|
|
12074
|
-
return getTikToken(modelName).then((tokenizer) => (text) => {
|
|
12075
|
-
const tokens = tokenizer.encode(text);
|
|
12076
|
-
return tokens.length;
|
|
12077
|
-
});
|
|
12078
|
-
};
|
|
12682
|
+
return '';
|
|
12683
|
+
}
|
|
12684
|
+
// Format the commit logs
|
|
12685
|
+
const formattedLogs = logs.all.map((commit) => {
|
|
12686
|
+
return formatSingleCommit(commit);
|
|
12687
|
+
}).join('\n\n');
|
|
12688
|
+
return formattedLogs;
|
|
12689
|
+
}
|
|
12690
|
+
catch (error) {
|
|
12691
|
+
console.error(`Error getting previous commits: ${error.message}`);
|
|
12692
|
+
return '';
|
|
12693
|
+
}
|
|
12694
|
+
}
|
|
12079
12695
|
|
|
12080
12696
|
const COMMITLINT_CONFIG_FILES = [
|
|
12081
12697
|
'.commitlintrc',
|
|
@@ -12175,17 +12791,345 @@ async function noResult$2({ git, logger }) {
|
|
|
12175
12791
|
}
|
|
12176
12792
|
}
|
|
12177
12793
|
|
|
12178
|
-
const
|
|
12794
|
+
const CommitSplitPlanSchema = objectType({
|
|
12795
|
+
groups: arrayType(objectType({
|
|
12796
|
+
title: stringType().min(1),
|
|
12797
|
+
body: stringType().optional(),
|
|
12798
|
+
rationale: stringType().optional(),
|
|
12799
|
+
files: arrayType(stringType()),
|
|
12800
|
+
hunks: arrayType(stringType()),
|
|
12801
|
+
})
|
|
12802
|
+
.refine((group) => group.files.length > 0 || group.hunks.length > 0, {
|
|
12803
|
+
message: 'Each group must include at least one file or hunk',
|
|
12804
|
+
}))
|
|
12805
|
+
.min(1),
|
|
12806
|
+
});
|
|
12807
|
+
const COMMIT_SPLIT_PROMPT = prompts$1.PromptTemplate.fromTemplate(`You are helping split staged git changes into a small sequence of coherent commits.
|
|
12808
|
+
|
|
12809
|
+
Return ONLY valid JSON matching this schema:
|
|
12810
|
+
{{
|
|
12811
|
+
"groups": [
|
|
12812
|
+
{{
|
|
12813
|
+
"title": "conventional commit style title",
|
|
12814
|
+
"body": "commit body",
|
|
12815
|
+
"rationale": "why these files belong together",
|
|
12816
|
+
"files": ["relative/path.ts"],
|
|
12817
|
+
"hunks": ["relative/path.ts::hunk-1"]
|
|
12818
|
+
}}
|
|
12819
|
+
]
|
|
12820
|
+
}}
|
|
12821
|
+
|
|
12822
|
+
Rules:
|
|
12823
|
+
- Use each staged file exactly once.
|
|
12824
|
+
- If a file has hunk IDs and contains unrelated changes, assign every hunk ID exactly once instead of assigning the whole file.
|
|
12825
|
+
- Do not list the same file in "files" when assigning that file through "hunks".
|
|
12826
|
+
- Only use file paths listed in the staged file inventory.
|
|
12827
|
+
- Only use hunk IDs listed in the staged hunk inventory.
|
|
12828
|
+
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
12829
|
+
- Keep commit titles concise and understandable.
|
|
12830
|
+
- Do not invent files.
|
|
12831
|
+
|
|
12832
|
+
Staged file inventory:
|
|
12833
|
+
{file_inventory}
|
|
12834
|
+
|
|
12835
|
+
Staged hunk inventory:
|
|
12836
|
+
{hunk_inventory}
|
|
12837
|
+
|
|
12838
|
+
Condensed staged diff:
|
|
12839
|
+
{summary}
|
|
12840
|
+
|
|
12841
|
+
Additional context:
|
|
12842
|
+
{additional_context}`);
|
|
12843
|
+
function isCommitSplitCommand(argv) {
|
|
12844
|
+
return Boolean(argv.split || argv.plan || argv.apply || argv._.includes('split'));
|
|
12845
|
+
}
|
|
12846
|
+
function formatCommitSplitPlan(plan) {
|
|
12847
|
+
return plan.groups
|
|
12848
|
+
.map((group, index) => {
|
|
12849
|
+
const body = group.body ? `\n\n${group.body}` : '';
|
|
12850
|
+
const rationale = group.rationale ? `\n\nRationale: ${group.rationale}` : '';
|
|
12851
|
+
const files = (group.files || []).map((file) => `- ${file}`).join('\n');
|
|
12852
|
+
const hunks = (group.hunks || []).map((hunk) => `- ${hunk}`).join('\n');
|
|
12853
|
+
const sections = [
|
|
12854
|
+
files ? `Files:\n${files}` : undefined,
|
|
12855
|
+
hunks ? `Hunks:\n${hunks}` : undefined,
|
|
12856
|
+
].filter(Boolean);
|
|
12857
|
+
return `## ${index + 1}. ${group.title}${body}${rationale}\n\n${sections.join('\n\n')}`;
|
|
12858
|
+
})
|
|
12859
|
+
.join('\n\n---\n\n');
|
|
12860
|
+
}
|
|
12861
|
+
function getStagedFileSet(changes) {
|
|
12862
|
+
return new Set(changes.map((change) => change.filePath));
|
|
12863
|
+
}
|
|
12864
|
+
function getGroupFiles(group) {
|
|
12865
|
+
return group.files || [];
|
|
12866
|
+
}
|
|
12867
|
+
function getGroupHunks(group) {
|
|
12868
|
+
return group.hunks || [];
|
|
12869
|
+
}
|
|
12870
|
+
function hunkHeader(hunk) {
|
|
12871
|
+
return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
12872
|
+
}
|
|
12873
|
+
function hunkPreview(hunk) {
|
|
12874
|
+
return hunk.lines
|
|
12875
|
+
.filter((line) => line.startsWith('+') || line.startsWith('-'))
|
|
12876
|
+
.slice(0, 6)
|
|
12877
|
+
.join('\n');
|
|
12878
|
+
}
|
|
12879
|
+
async function collectHunkInventory(staged, git) {
|
|
12880
|
+
const hunks = [];
|
|
12881
|
+
const byId = new Map();
|
|
12882
|
+
const byFile = new Map();
|
|
12883
|
+
for (const change of staged) {
|
|
12884
|
+
if (change.status !== 'modified') {
|
|
12885
|
+
continue;
|
|
12886
|
+
}
|
|
12887
|
+
const diff$1 = await git.diff(['--staged', '--', change.filePath]);
|
|
12888
|
+
const [patch] = diff.parsePatch(diff$1);
|
|
12889
|
+
if (!patch || patch.hunks.length === 0) {
|
|
12890
|
+
continue;
|
|
12891
|
+
}
|
|
12892
|
+
patch.hunks.forEach((hunk, index) => {
|
|
12893
|
+
const stagedHunk = {
|
|
12894
|
+
id: `${change.filePath}::hunk-${index + 1}`,
|
|
12895
|
+
filePath: change.filePath,
|
|
12896
|
+
patch,
|
|
12897
|
+
hunk,
|
|
12898
|
+
header: hunkHeader(hunk),
|
|
12899
|
+
preview: hunkPreview(hunk),
|
|
12900
|
+
};
|
|
12901
|
+
hunks.push(stagedHunk);
|
|
12902
|
+
byId.set(stagedHunk.id, stagedHunk);
|
|
12903
|
+
byFile.set(change.filePath, [...(byFile.get(change.filePath) || []), stagedHunk]);
|
|
12904
|
+
});
|
|
12905
|
+
}
|
|
12906
|
+
return { hunks, byId, byFile };
|
|
12907
|
+
}
|
|
12908
|
+
function formatHunkInventory(inventory) {
|
|
12909
|
+
if (inventory.hunks.length === 0) {
|
|
12910
|
+
return 'No hunk-level inventory available. Use file-level groups.';
|
|
12911
|
+
}
|
|
12912
|
+
return inventory.hunks
|
|
12913
|
+
.map((hunk) => {
|
|
12914
|
+
const preview = hunk.preview ? `\n${hunk.preview}` : '';
|
|
12915
|
+
return `- ${hunk.id}: ${hunk.header}${preview}`;
|
|
12916
|
+
})
|
|
12917
|
+
.join('\n');
|
|
12918
|
+
}
|
|
12919
|
+
function validatePlanForStagedFiles(plan, staged, hunkInventory) {
|
|
12920
|
+
const stagedFiles = getStagedFileSet(staged);
|
|
12921
|
+
const seen = new Set();
|
|
12922
|
+
const seenHunks = new Set();
|
|
12923
|
+
const unknown = [];
|
|
12924
|
+
const duplicate = [];
|
|
12925
|
+
const unknownHunks = [];
|
|
12926
|
+
const duplicateHunks = [];
|
|
12927
|
+
plan.groups.forEach((group) => {
|
|
12928
|
+
getGroupFiles(group).forEach((file) => {
|
|
12929
|
+
if (!stagedFiles.has(file)) {
|
|
12930
|
+
unknown.push(file);
|
|
12931
|
+
return;
|
|
12932
|
+
}
|
|
12933
|
+
if (seen.has(file)) {
|
|
12934
|
+
duplicate.push(file);
|
|
12935
|
+
return;
|
|
12936
|
+
}
|
|
12937
|
+
seen.add(file);
|
|
12938
|
+
});
|
|
12939
|
+
getGroupHunks(group).forEach((hunkId) => {
|
|
12940
|
+
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12941
|
+
if (!hunk) {
|
|
12942
|
+
unknownHunks.push(hunkId);
|
|
12943
|
+
return;
|
|
12944
|
+
}
|
|
12945
|
+
if (seenHunks.has(hunkId)) {
|
|
12946
|
+
duplicateHunks.push(hunkId);
|
|
12947
|
+
return;
|
|
12948
|
+
}
|
|
12949
|
+
seenHunks.add(hunkId);
|
|
12950
|
+
});
|
|
12951
|
+
});
|
|
12952
|
+
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12953
|
+
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12954
|
+
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12955
|
+
.filter((file) => Boolean(file))
|
|
12956
|
+
.filter((file) => {
|
|
12957
|
+
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12958
|
+
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12959
|
+
});
|
|
12960
|
+
const missing = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12961
|
+
if (unknown.length ||
|
|
12962
|
+
duplicate.length ||
|
|
12963
|
+
unknownHunks.length ||
|
|
12964
|
+
duplicateHunks.length ||
|
|
12965
|
+
mixedFiles.length ||
|
|
12966
|
+
partiallyCoveredFiles.length ||
|
|
12967
|
+
missing.length) {
|
|
12968
|
+
throw new Error([
|
|
12969
|
+
unknown.length ? `unknown files: ${unknown.join(', ')}` : undefined,
|
|
12970
|
+
duplicate.length ? `duplicate files: ${duplicate.join(', ')}` : undefined,
|
|
12971
|
+
unknownHunks.length ? `unknown hunks: ${unknownHunks.join(', ')}` : undefined,
|
|
12972
|
+
duplicateHunks.length ? `duplicate hunks: ${duplicateHunks.join(', ')}` : undefined,
|
|
12973
|
+
mixedFiles.length ? `files assigned both as whole files and hunks: ${mixedFiles.join(', ')}` : undefined,
|
|
12974
|
+
partiallyCoveredFiles.length
|
|
12975
|
+
? `files with only some hunks assigned: ${partiallyCoveredFiles.join(', ')}`
|
|
12976
|
+
: undefined,
|
|
12977
|
+
missing.length ? `missing files: ${missing.join(', ')}` : undefined,
|
|
12978
|
+
]
|
|
12979
|
+
.filter(Boolean)
|
|
12980
|
+
.join('; '));
|
|
12981
|
+
}
|
|
12982
|
+
}
|
|
12983
|
+
function assertNoUnstagedOverlap(plan, changes, hunkInventory) {
|
|
12984
|
+
const hunkFiles = new Set(plan.groups.flatMap((group) => getGroupHunks(group)
|
|
12985
|
+
.map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath)
|
|
12986
|
+
.filter((file) => Boolean(file))));
|
|
12987
|
+
const plannedFiles = new Set(plan.groups
|
|
12988
|
+
.flatMap((group) => getGroupFiles(group))
|
|
12989
|
+
.filter((file) => !hunkFiles.has(file)));
|
|
12990
|
+
const overlapping = [...(changes.unstaged || []), ...(changes.untracked || [])]
|
|
12991
|
+
.map((change) => change.filePath)
|
|
12992
|
+
.filter((file) => plannedFiles.has(file));
|
|
12993
|
+
if (overlapping.length > 0) {
|
|
12994
|
+
throw new Error(`Cannot apply split plan because these files also have unstaged or untracked changes: ${overlapping.join(', ')}`);
|
|
12995
|
+
}
|
|
12996
|
+
}
|
|
12997
|
+
function buildPatchForHunks(hunks) {
|
|
12998
|
+
const byFile = new Map();
|
|
12999
|
+
hunks.forEach((hunk) => {
|
|
13000
|
+
byFile.set(hunk.filePath, [...(byFile.get(hunk.filePath) || []), hunk]);
|
|
13001
|
+
});
|
|
13002
|
+
return [...byFile.values()]
|
|
13003
|
+
.map((fileHunks) => {
|
|
13004
|
+
const [firstHunk] = fileHunks;
|
|
13005
|
+
return diff.formatPatch({
|
|
13006
|
+
...firstHunk.patch,
|
|
13007
|
+
hunks: fileHunks.map((hunk) => hunk.hunk),
|
|
13008
|
+
});
|
|
13009
|
+
})
|
|
13010
|
+
.join('\n');
|
|
13011
|
+
}
|
|
13012
|
+
async function applyPatchToIndex(patch, git) {
|
|
13013
|
+
const cwd = await git.revparse(['--show-toplevel']);
|
|
13014
|
+
await new Promise((resolve, reject) => {
|
|
13015
|
+
const child = child_process.spawn('git', ['apply', '--cached', '-'], {
|
|
13016
|
+
cwd,
|
|
13017
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
13018
|
+
});
|
|
13019
|
+
let stderr = '';
|
|
13020
|
+
child.stderr.on('data', (chunk) => {
|
|
13021
|
+
stderr += String(chunk);
|
|
13022
|
+
});
|
|
13023
|
+
child.on('error', reject);
|
|
13024
|
+
child.on('close', (code) => {
|
|
13025
|
+
if (code === 0) {
|
|
13026
|
+
resolve();
|
|
13027
|
+
return;
|
|
13028
|
+
}
|
|
13029
|
+
reject(new Error(`Failed to apply hunk patch to index: ${stderr.trim()}`));
|
|
13030
|
+
});
|
|
13031
|
+
child.stdin.write(patch);
|
|
13032
|
+
child.stdin.end();
|
|
13033
|
+
});
|
|
13034
|
+
}
|
|
13035
|
+
async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger, noVerify, }) {
|
|
13036
|
+
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
13037
|
+
assertNoUnstagedOverlap(plan, changes, hunkInventory);
|
|
13038
|
+
await git.raw(['reset']);
|
|
13039
|
+
for (const group of plan.groups) {
|
|
13040
|
+
const groupFiles = getGroupFiles(group);
|
|
13041
|
+
const groupHunks = getGroupHunks(group).map((hunkId) => hunkInventory.byId.get(hunkId));
|
|
13042
|
+
if (groupFiles.length > 0) {
|
|
13043
|
+
await git.add(groupFiles);
|
|
13044
|
+
}
|
|
13045
|
+
if (groupHunks.length > 0) {
|
|
13046
|
+
const patch = buildPatchForHunks(groupHunks.filter((hunk) => Boolean(hunk)));
|
|
13047
|
+
await applyPatchToIndex(patch, git);
|
|
13048
|
+
}
|
|
13049
|
+
await createCommit(`${group.title}\n\n${group.body}`.trim(), git, undefined, { noVerify });
|
|
13050
|
+
logger.verbose(`Created split commit: ${group.title}`, { color: 'green' });
|
|
13051
|
+
}
|
|
13052
|
+
return `Created ${plan.groups.length} split commit(s).`;
|
|
13053
|
+
}
|
|
13054
|
+
async function handleCommitSplit({ argv, config, git, logger, tokenizer, llm, }) {
|
|
13055
|
+
const changes = await getChanges({
|
|
13056
|
+
git,
|
|
13057
|
+
options: {
|
|
13058
|
+
ignoredFiles: config.ignoredFiles || undefined,
|
|
13059
|
+
ignoredExtensions: config.ignoredExtensions || undefined,
|
|
13060
|
+
},
|
|
13061
|
+
});
|
|
13062
|
+
if (changes.staged.length === 0) {
|
|
13063
|
+
return 'No staged changes found.';
|
|
13064
|
+
}
|
|
13065
|
+
const hunkInventory = await collectHunkInventory(changes.staged, git);
|
|
13066
|
+
const summary = await fileChangeParser({
|
|
13067
|
+
changes: changes.staged,
|
|
13068
|
+
commit: '--staged',
|
|
13069
|
+
options: {
|
|
13070
|
+
tokenizer,
|
|
13071
|
+
git,
|
|
13072
|
+
llm,
|
|
13073
|
+
logger,
|
|
13074
|
+
maxTokens: config.service.tokenLimit,
|
|
13075
|
+
minTokensForSummary: config.service.minTokensForSummary,
|
|
13076
|
+
maxFileTokens: config.service.maxFileTokens,
|
|
13077
|
+
maxConcurrent: config.service.maxConcurrent,
|
|
13078
|
+
metadata: {
|
|
13079
|
+
command: 'commit',
|
|
13080
|
+
provider: config.service.provider,
|
|
13081
|
+
model: String(config.service.model),
|
|
13082
|
+
},
|
|
13083
|
+
},
|
|
13084
|
+
});
|
|
13085
|
+
const fileInventory = changes.staged
|
|
13086
|
+
.map((change) => `- ${change.filePath}: ${change.status} - ${change.summary}`)
|
|
13087
|
+
.join('\n');
|
|
13088
|
+
const hunkInventoryText = formatHunkInventory(hunkInventory);
|
|
13089
|
+
const plan = await executeChainWithSchema(CommitSplitPlanSchema, llm, COMMIT_SPLIT_PROMPT, {
|
|
13090
|
+
file_inventory: fileInventory,
|
|
13091
|
+
hunk_inventory: hunkInventoryText,
|
|
13092
|
+
summary,
|
|
13093
|
+
additional_context: argv.additional || '',
|
|
13094
|
+
}, {
|
|
13095
|
+
logger,
|
|
13096
|
+
tokenizer,
|
|
13097
|
+
metadata: {
|
|
13098
|
+
task: 'commit-split-plan',
|
|
13099
|
+
command: 'commit',
|
|
13100
|
+
provider: config.service.provider,
|
|
13101
|
+
model: String(config.service.model),
|
|
13102
|
+
},
|
|
13103
|
+
});
|
|
13104
|
+
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
13105
|
+
if (argv.apply) {
|
|
13106
|
+
return await applyCommitSplitPlan({
|
|
13107
|
+
plan,
|
|
13108
|
+
changes,
|
|
13109
|
+
hunkInventory,
|
|
13110
|
+
git,
|
|
13111
|
+
logger,
|
|
13112
|
+
noVerify: argv.noVerify || config.noVerify || false,
|
|
13113
|
+
});
|
|
13114
|
+
}
|
|
13115
|
+
return formatCommitSplitPlan(plan);
|
|
13116
|
+
}
|
|
13117
|
+
|
|
13118
|
+
const handler$4 = async (argv, logger) => {
|
|
12179
13119
|
const git = getRepo();
|
|
12180
13120
|
const config = loadConfig(argv);
|
|
12181
13121
|
const key = getApiKeyForModel(config);
|
|
12182
|
-
const { provider
|
|
13122
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
13123
|
+
const commitService = resolveDynamicService(config, 'commit');
|
|
13124
|
+
const summaryService = resolveDynamicService(config, 'summarize');
|
|
13125
|
+
const model = commitService.model;
|
|
12183
13126
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
12184
13127
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
12185
13128
|
process.exit(1);
|
|
12186
13129
|
}
|
|
12187
13130
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
12188
|
-
const llm = getLlm(provider, model, config);
|
|
13131
|
+
const llm = getLlm(provider, model, { ...config, service: commitService });
|
|
13132
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
12189
13133
|
const INTERACTIVE = argv.interactive || isInteractive(config);
|
|
12190
13134
|
if (INTERACTIVE) {
|
|
12191
13135
|
if (!config.hideCocoBanner) {
|
|
@@ -12203,6 +13147,22 @@ const handler$3 = async (argv, logger) => {
|
|
|
12203
13147
|
logger.verbose(`→ ${provider} (${model})`, {
|
|
12204
13148
|
color: 'green',
|
|
12205
13149
|
});
|
|
13150
|
+
if (isCommitSplitCommand(argv)) {
|
|
13151
|
+
const splitResult = await handleCommitSplit({
|
|
13152
|
+
argv,
|
|
13153
|
+
config,
|
|
13154
|
+
git,
|
|
13155
|
+
logger,
|
|
13156
|
+
tokenizer,
|
|
13157
|
+
llm,
|
|
13158
|
+
});
|
|
13159
|
+
await handleResult({
|
|
13160
|
+
result: splitResult,
|
|
13161
|
+
mode: config.mode || 'stdout',
|
|
13162
|
+
});
|
|
13163
|
+
logLlmTelemetrySummary(logger, 'commit');
|
|
13164
|
+
return;
|
|
13165
|
+
}
|
|
12206
13166
|
const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
|
|
12207
13167
|
async function factory() {
|
|
12208
13168
|
if (config.noDiff) {
|
|
@@ -12240,12 +13200,17 @@ const handler$3 = async (argv, logger) => {
|
|
|
12240
13200
|
options: {
|
|
12241
13201
|
tokenizer,
|
|
12242
13202
|
git,
|
|
12243
|
-
llm,
|
|
13203
|
+
llm: summaryLlm,
|
|
12244
13204
|
logger,
|
|
12245
13205
|
maxTokens: config.service.tokenLimit,
|
|
12246
13206
|
minTokensForSummary: config.service.minTokensForSummary,
|
|
12247
13207
|
maxFileTokens: config.service.maxFileTokens,
|
|
12248
13208
|
maxConcurrent: config.service.maxConcurrent,
|
|
13209
|
+
metadata: {
|
|
13210
|
+
command: 'commit',
|
|
13211
|
+
provider,
|
|
13212
|
+
model: String(summaryService.model),
|
|
13213
|
+
},
|
|
12249
13214
|
},
|
|
12250
13215
|
});
|
|
12251
13216
|
}
|
|
@@ -12385,7 +13350,24 @@ IMPORTANT RULES:
|
|
|
12385
13350
|
? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
|
|
12386
13351
|
: variables.additional_context,
|
|
12387
13352
|
};
|
|
12388
|
-
const
|
|
13353
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
13354
|
+
prompt,
|
|
13355
|
+
variables: currentVariables,
|
|
13356
|
+
tokenizer,
|
|
13357
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
13358
|
+
});
|
|
13359
|
+
if (budgetedPrompt.truncated) {
|
|
13360
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
13361
|
+
}
|
|
13362
|
+
const commitMsg = await executeChainWithSchema(schema, llm, prompt, budgetedPrompt.variables, {
|
|
13363
|
+
logger,
|
|
13364
|
+
tokenizer,
|
|
13365
|
+
metadata: {
|
|
13366
|
+
task: USE_CONVENTIONAL_COMMITS ? 'commit-message-conventional' : 'commit-message',
|
|
13367
|
+
command: 'commit',
|
|
13368
|
+
provider,
|
|
13369
|
+
model: String(model),
|
|
13370
|
+
},
|
|
12389
13371
|
retryOptions: {
|
|
12390
13372
|
maxAttempts,
|
|
12391
13373
|
onRetry: (attempt, error) => {
|
|
@@ -12530,36 +13512,94 @@ IMPORTANT RULES:
|
|
|
12530
13512
|
handleResult({
|
|
12531
13513
|
result: commitMsg,
|
|
12532
13514
|
interactiveModeCallback: async (result) => {
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
13515
|
+
const noVerify = argv.noVerify || config.noVerify || false;
|
|
13516
|
+
const attemptCommit = async (skipHooks) => {
|
|
13517
|
+
try {
|
|
13518
|
+
await createCommit(result, git, () => {
|
|
13519
|
+
logger.log('⚠️ Pre-commit hook modified files. Staging changes and retrying commit...', { color: 'yellow' });
|
|
13520
|
+
}, { noVerify: skipHooks });
|
|
13521
|
+
logSuccess();
|
|
13522
|
+
}
|
|
13523
|
+
catch (error) {
|
|
13524
|
+
if (error instanceof PreCommitHookError) {
|
|
13525
|
+
// Display friendly hook failure output
|
|
13526
|
+
logger.log('\n✖ Commit blocked by pre-commit hook', { color: 'red' });
|
|
13527
|
+
logger.log('\nHook output:', { color: 'yellow' });
|
|
13528
|
+
logger.log(SEPERATOR);
|
|
13529
|
+
logger.log(error.hookOutput);
|
|
13530
|
+
logger.log(SEPERATOR);
|
|
13531
|
+
if (INTERACTIVE) {
|
|
13532
|
+
const { select } = await import('@inquirer/prompts');
|
|
13533
|
+
const choice = await select({
|
|
13534
|
+
message: 'How would you like to proceed?',
|
|
13535
|
+
choices: [
|
|
13536
|
+
{
|
|
13537
|
+
name: '🔄 Retry',
|
|
13538
|
+
value: 'retry',
|
|
13539
|
+
description: 'Fix the issues above and retry the commit',
|
|
13540
|
+
},
|
|
13541
|
+
{
|
|
13542
|
+
name: '⚠️ Skip hooks',
|
|
13543
|
+
value: 'skip',
|
|
13544
|
+
description: 'Retry with --no-verify to bypass pre-commit hooks (use with care)',
|
|
13545
|
+
},
|
|
13546
|
+
{
|
|
13547
|
+
name: '💣 Abort',
|
|
13548
|
+
value: 'abort',
|
|
13549
|
+
description: 'Abort the commit',
|
|
13550
|
+
},
|
|
13551
|
+
],
|
|
13552
|
+
});
|
|
13553
|
+
if (choice === 'retry') {
|
|
13554
|
+
await attemptCommit(false);
|
|
13555
|
+
}
|
|
13556
|
+
else if (choice === 'skip') {
|
|
13557
|
+
logger.log('⚠️ Skipping hooks with --no-verify...', { color: 'yellow' });
|
|
13558
|
+
await attemptCommit(true);
|
|
13559
|
+
}
|
|
13560
|
+
else {
|
|
13561
|
+
logger.log('\nCommit aborted.', { color: 'red' });
|
|
13562
|
+
process.exit(1);
|
|
13563
|
+
}
|
|
13564
|
+
}
|
|
13565
|
+
else {
|
|
13566
|
+
logger.log('\nFix the issues above and try again, or use --no-verify to skip hooks.', { color: 'yellow' });
|
|
13567
|
+
process.exit(1);
|
|
13568
|
+
}
|
|
13569
|
+
}
|
|
13570
|
+
else {
|
|
13571
|
+
throw error;
|
|
13572
|
+
}
|
|
13573
|
+
}
|
|
13574
|
+
};
|
|
13575
|
+
await attemptCommit(noVerify);
|
|
12537
13576
|
},
|
|
12538
13577
|
mode: MODE,
|
|
12539
13578
|
});
|
|
13579
|
+
logLlmTelemetrySummary(logger, 'commit');
|
|
12540
13580
|
};
|
|
12541
13581
|
|
|
12542
13582
|
var commit = {
|
|
12543
|
-
command: command$
|
|
13583
|
+
command: command$4,
|
|
12544
13584
|
desc: 'Summarize the staged changes in a commit message.',
|
|
12545
|
-
builder: builder$
|
|
12546
|
-
handler: commandExecutor(handler$
|
|
12547
|
-
options: options$
|
|
13585
|
+
builder: builder$4,
|
|
13586
|
+
handler: commandExecutor(handler$4),
|
|
13587
|
+
options: options$4,
|
|
12548
13588
|
};
|
|
12549
13589
|
|
|
12550
|
-
const command$
|
|
13590
|
+
const command$3 = 'init';
|
|
12551
13591
|
/**
|
|
12552
13592
|
* Command line options via yargs
|
|
12553
13593
|
*/
|
|
12554
|
-
const options$
|
|
13594
|
+
const options$3 = {
|
|
12555
13595
|
scope: {
|
|
12556
13596
|
type: 'string',
|
|
12557
13597
|
description: 'configure coco for the current user or project?',
|
|
12558
13598
|
choices: ['global', 'project'],
|
|
12559
13599
|
},
|
|
12560
13600
|
};
|
|
12561
|
-
const builder$
|
|
12562
|
-
return yargs.options(options$
|
|
13601
|
+
const builder$3 = (yargs) => {
|
|
13602
|
+
return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
|
|
12563
13603
|
};
|
|
12564
13604
|
|
|
12565
13605
|
/**
|
|
@@ -12910,7 +13950,7 @@ const questions = {
|
|
|
12910
13950
|
}),
|
|
12911
13951
|
};
|
|
12912
13952
|
|
|
12913
|
-
const handler$
|
|
13953
|
+
const handler$3 = async (argv, logger) => {
|
|
12914
13954
|
const options = loadConfig(argv);
|
|
12915
13955
|
logger.log(LOGO);
|
|
12916
13956
|
let scope = options?.scope;
|
|
@@ -13081,8 +14121,287 @@ async function installCommitlintPackages(scope, logger) {
|
|
|
13081
14121
|
}
|
|
13082
14122
|
|
|
13083
14123
|
var init = {
|
|
13084
|
-
command: command$
|
|
14124
|
+
command: command$3,
|
|
13085
14125
|
desc: 'install & configure coco globally or for the current project',
|
|
14126
|
+
builder: builder$3,
|
|
14127
|
+
handler: commandExecutor(handler$3),
|
|
14128
|
+
options: options$3,
|
|
14129
|
+
};
|
|
14130
|
+
|
|
14131
|
+
const command$2 = 'log';
|
|
14132
|
+
const options$2 = {
|
|
14133
|
+
all: {
|
|
14134
|
+
description: 'Show commits from all local and remote refs',
|
|
14135
|
+
type: 'boolean',
|
|
14136
|
+
default: false,
|
|
14137
|
+
},
|
|
14138
|
+
author: {
|
|
14139
|
+
description: 'Filter commits by author',
|
|
14140
|
+
type: 'string',
|
|
14141
|
+
},
|
|
14142
|
+
branch: {
|
|
14143
|
+
description: 'Show commits reachable from a branch or ref',
|
|
14144
|
+
type: 'string',
|
|
14145
|
+
alias: 'b',
|
|
14146
|
+
},
|
|
14147
|
+
commit: {
|
|
14148
|
+
description: 'Show details and changed files for a single commit',
|
|
14149
|
+
type: 'string',
|
|
14150
|
+
alias: 'c',
|
|
14151
|
+
},
|
|
14152
|
+
format: {
|
|
14153
|
+
description: 'Output format',
|
|
14154
|
+
choices: ['table', 'json'],
|
|
14155
|
+
default: 'table',
|
|
14156
|
+
},
|
|
14157
|
+
limit: {
|
|
14158
|
+
description: 'Maximum number of commits to show',
|
|
14159
|
+
type: 'number',
|
|
14160
|
+
default: 30,
|
|
14161
|
+
alias: 'n',
|
|
14162
|
+
},
|
|
14163
|
+
noMerges: {
|
|
14164
|
+
description: 'Exclude merge commits',
|
|
14165
|
+
type: 'boolean',
|
|
14166
|
+
default: false,
|
|
14167
|
+
},
|
|
14168
|
+
path: {
|
|
14169
|
+
description: 'Filter commits by changed path',
|
|
14170
|
+
type: 'array',
|
|
14171
|
+
},
|
|
14172
|
+
since: {
|
|
14173
|
+
description: 'Show commits more recent than a date',
|
|
14174
|
+
type: 'string',
|
|
14175
|
+
},
|
|
14176
|
+
until: {
|
|
14177
|
+
description: 'Show commits older than a date',
|
|
14178
|
+
type: 'string',
|
|
14179
|
+
},
|
|
14180
|
+
};
|
|
14181
|
+
const builder$2 = (yargs) => {
|
|
14182
|
+
return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
|
|
14183
|
+
};
|
|
14184
|
+
|
|
14185
|
+
const FIELD_SEPARATOR = '\x1f';
|
|
14186
|
+
const LOG_FORMAT = `%x1f%h%x1f%H%x1f%ad%x1f%an%x1f%d%x1f%s`;
|
|
14187
|
+
const DETAIL_FORMAT = `%H%x1f%h%x1f%ad%x1f%an%x1f%d%x1f%s%x1f%b`;
|
|
14188
|
+
function toArray(value) {
|
|
14189
|
+
if (!value) {
|
|
14190
|
+
return [];
|
|
14191
|
+
}
|
|
14192
|
+
return Array.isArray(value) ? value : [value];
|
|
14193
|
+
}
|
|
14194
|
+
function normalizeLimit(limit) {
|
|
14195
|
+
if (!limit || Number.isNaN(limit) || limit < 1) {
|
|
14196
|
+
return 30;
|
|
14197
|
+
}
|
|
14198
|
+
return Math.floor(limit);
|
|
14199
|
+
}
|
|
14200
|
+
function cleanRefs(refs) {
|
|
14201
|
+
const trimmed = refs.trim();
|
|
14202
|
+
if (!trimmed) {
|
|
14203
|
+
return [];
|
|
14204
|
+
}
|
|
14205
|
+
return trimmed
|
|
14206
|
+
.replace(/^\(/, '')
|
|
14207
|
+
.replace(/\)$/, '')
|
|
14208
|
+
.split(',')
|
|
14209
|
+
.map((ref) => ref.trim())
|
|
14210
|
+
.filter(Boolean);
|
|
14211
|
+
}
|
|
14212
|
+
function parseLogOutput(output) {
|
|
14213
|
+
return output
|
|
14214
|
+
.split('\n')
|
|
14215
|
+
.map((line) => line.trimEnd())
|
|
14216
|
+
.filter((line) => line.includes(FIELD_SEPARATOR))
|
|
14217
|
+
.map((line) => {
|
|
14218
|
+
const [graph, shortHash, hash, date, author, refs, message] = line.split(FIELD_SEPARATOR);
|
|
14219
|
+
return {
|
|
14220
|
+
graph: graph.trimEnd(),
|
|
14221
|
+
shortHash,
|
|
14222
|
+
hash,
|
|
14223
|
+
date,
|
|
14224
|
+
author,
|
|
14225
|
+
refs: cleanRefs(refs),
|
|
14226
|
+
message,
|
|
14227
|
+
};
|
|
14228
|
+
});
|
|
14229
|
+
}
|
|
14230
|
+
function parseNameStatus(output) {
|
|
14231
|
+
return output
|
|
14232
|
+
.split('\n')
|
|
14233
|
+
.map((line) => line.trim())
|
|
14234
|
+
.filter(Boolean)
|
|
14235
|
+
.map((line) => {
|
|
14236
|
+
const [status, firstPath, secondPath] = line.split('\t');
|
|
14237
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
14238
|
+
return {
|
|
14239
|
+
status,
|
|
14240
|
+
oldPath: firstPath,
|
|
14241
|
+
path: secondPath,
|
|
14242
|
+
};
|
|
14243
|
+
}
|
|
14244
|
+
return {
|
|
14245
|
+
status,
|
|
14246
|
+
path: firstPath,
|
|
14247
|
+
};
|
|
14248
|
+
});
|
|
14249
|
+
}
|
|
14250
|
+
function parseCommitDetail(metadata, files) {
|
|
14251
|
+
const [hash, shortHash, date, author, refs, message, body = ''] = metadata
|
|
14252
|
+
.trimEnd()
|
|
14253
|
+
.split(FIELD_SEPARATOR);
|
|
14254
|
+
return {
|
|
14255
|
+
shortHash,
|
|
14256
|
+
hash,
|
|
14257
|
+
date,
|
|
14258
|
+
author,
|
|
14259
|
+
refs: cleanRefs(refs),
|
|
14260
|
+
message,
|
|
14261
|
+
body: body.trim(),
|
|
14262
|
+
files: parseNameStatus(files),
|
|
14263
|
+
};
|
|
14264
|
+
}
|
|
14265
|
+
function truncate(value, width) {
|
|
14266
|
+
if (value.length <= width) {
|
|
14267
|
+
return value;
|
|
14268
|
+
}
|
|
14269
|
+
return `${value.slice(0, Math.max(0, width - 1))}.`;
|
|
14270
|
+
}
|
|
14271
|
+
function pad(value, width) {
|
|
14272
|
+
return truncate(value, width).padEnd(width, ' ');
|
|
14273
|
+
}
|
|
14274
|
+
function formatLogTable(entries) {
|
|
14275
|
+
if (entries.length === 0) {
|
|
14276
|
+
return 'No commits found.';
|
|
14277
|
+
}
|
|
14278
|
+
const rows = entries.map((entry) => {
|
|
14279
|
+
const refs = entry.refs.join(', ');
|
|
14280
|
+
return [
|
|
14281
|
+
pad(entry.graph || '*', 8),
|
|
14282
|
+
pad(entry.shortHash, 9),
|
|
14283
|
+
pad(entry.date, 10),
|
|
14284
|
+
pad(entry.author, 18),
|
|
14285
|
+
pad(refs, 26),
|
|
14286
|
+
entry.message,
|
|
14287
|
+
].join(' ');
|
|
14288
|
+
});
|
|
14289
|
+
return [
|
|
14290
|
+
[
|
|
14291
|
+
pad('Graph', 8),
|
|
14292
|
+
pad('Commit', 9),
|
|
14293
|
+
pad('Date', 10),
|
|
14294
|
+
pad('Author', 18),
|
|
14295
|
+
pad('Refs', 26),
|
|
14296
|
+
'Message',
|
|
14297
|
+
].join(' '),
|
|
14298
|
+
...rows,
|
|
14299
|
+
].join('\n');
|
|
14300
|
+
}
|
|
14301
|
+
function formatCommitDetail(detail, format) {
|
|
14302
|
+
if (format === 'json') {
|
|
14303
|
+
return JSON.stringify(detail, null, 2);
|
|
14304
|
+
}
|
|
14305
|
+
const refs = detail.refs.length ? ` (${detail.refs.join(', ')})` : '';
|
|
14306
|
+
const body = detail.body ? `\n\n${detail.body}` : '';
|
|
14307
|
+
const files = detail.files.length
|
|
14308
|
+
? detail.files
|
|
14309
|
+
.map((file) => {
|
|
14310
|
+
if (file.oldPath) {
|
|
14311
|
+
return ` ${file.status} ${file.oldPath} -> ${file.path}`;
|
|
14312
|
+
}
|
|
14313
|
+
return ` ${file.status} ${file.path}`;
|
|
14314
|
+
})
|
|
14315
|
+
.join('\n')
|
|
14316
|
+
: ' No changed files found.';
|
|
14317
|
+
return [
|
|
14318
|
+
`commit ${detail.hash}${refs}`,
|
|
14319
|
+
`Author: ${detail.author}`,
|
|
14320
|
+
`Date: ${detail.date}`,
|
|
14321
|
+
'',
|
|
14322
|
+
` ${detail.message}${body}`,
|
|
14323
|
+
'',
|
|
14324
|
+
'Changed files:',
|
|
14325
|
+
files,
|
|
14326
|
+
].join('\n');
|
|
14327
|
+
}
|
|
14328
|
+
function buildLogArgs(argv) {
|
|
14329
|
+
const args = [
|
|
14330
|
+
'log',
|
|
14331
|
+
'--graph',
|
|
14332
|
+
'--decorate=short',
|
|
14333
|
+
'--date=short',
|
|
14334
|
+
'--color=never',
|
|
14335
|
+
`--max-count=${normalizeLimit(argv.limit)}`,
|
|
14336
|
+
`--pretty=format:${LOG_FORMAT}`,
|
|
14337
|
+
];
|
|
14338
|
+
if (argv.noMerges) {
|
|
14339
|
+
args.push('--no-merges');
|
|
14340
|
+
}
|
|
14341
|
+
if (argv.author) {
|
|
14342
|
+
args.push(`--author=${argv.author}`);
|
|
14343
|
+
}
|
|
14344
|
+
if (argv.since) {
|
|
14345
|
+
args.push(`--since=${argv.since}`);
|
|
14346
|
+
}
|
|
14347
|
+
if (argv.until) {
|
|
14348
|
+
args.push(`--until=${argv.until}`);
|
|
14349
|
+
}
|
|
14350
|
+
if (argv.all) {
|
|
14351
|
+
args.push('--all');
|
|
14352
|
+
}
|
|
14353
|
+
else if (argv.branch) {
|
|
14354
|
+
args.push(argv.branch);
|
|
14355
|
+
}
|
|
14356
|
+
const paths = toArray(argv.path);
|
|
14357
|
+
if (paths.length > 0) {
|
|
14358
|
+
args.push('--', ...paths);
|
|
14359
|
+
}
|
|
14360
|
+
return args;
|
|
14361
|
+
}
|
|
14362
|
+
async function getCommitDetail(git, commit) {
|
|
14363
|
+
const metadata = await git.raw([
|
|
14364
|
+
'show',
|
|
14365
|
+
'--no-patch',
|
|
14366
|
+
'--date=short',
|
|
14367
|
+
'--color=never',
|
|
14368
|
+
`--pretty=format:${DETAIL_FORMAT}`,
|
|
14369
|
+
commit,
|
|
14370
|
+
]);
|
|
14371
|
+
const files = await git.raw([
|
|
14372
|
+
'show',
|
|
14373
|
+
'--name-status',
|
|
14374
|
+
'--format=',
|
|
14375
|
+
'--find-renames',
|
|
14376
|
+
'--color=never',
|
|
14377
|
+
commit,
|
|
14378
|
+
]);
|
|
14379
|
+
return parseCommitDetail(metadata, files);
|
|
14380
|
+
}
|
|
14381
|
+
const handler$2 = async (argv) => {
|
|
14382
|
+
const git = getRepo();
|
|
14383
|
+
const mode = argv.interactive ? 'interactive' : 'stdout';
|
|
14384
|
+
const format = argv.format === 'json' ? 'json' : 'table';
|
|
14385
|
+
if (argv.commit) {
|
|
14386
|
+
const detail = await getCommitDetail(git, argv.commit);
|
|
14387
|
+
await handleResult({
|
|
14388
|
+
result: formatCommitDetail(detail, format),
|
|
14389
|
+
mode,
|
|
14390
|
+
});
|
|
14391
|
+
return;
|
|
14392
|
+
}
|
|
14393
|
+
const output = await git.raw(buildLogArgs(argv));
|
|
14394
|
+
const entries = parseLogOutput(output);
|
|
14395
|
+
const result = format === 'json' ? JSON.stringify(entries, null, 2) : formatLogTable(entries);
|
|
14396
|
+
await handleResult({
|
|
14397
|
+
result,
|
|
14398
|
+
mode,
|
|
14399
|
+
});
|
|
14400
|
+
};
|
|
14401
|
+
|
|
14402
|
+
var log = {
|
|
14403
|
+
command: command$2,
|
|
14404
|
+
desc: 'Explore commit history with a branch graph, filters, and commit details.',
|
|
13086
14405
|
builder: builder$2,
|
|
13087
14406
|
handler: commandExecutor(handler$2),
|
|
13088
14407
|
options: options$2,
|
|
@@ -13160,13 +14479,17 @@ const handler$1 = async (argv, logger) => {
|
|
|
13160
14479
|
const git = getRepo();
|
|
13161
14480
|
const config = loadConfig(argv);
|
|
13162
14481
|
const key = getApiKeyForModel(config);
|
|
13163
|
-
const { provider
|
|
14482
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
14483
|
+
const recapService = resolveDynamicService(config, 'recap');
|
|
14484
|
+
const summaryService = resolveDynamicService(config, 'summarize');
|
|
14485
|
+
const model = recapService.model;
|
|
13164
14486
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
13165
14487
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
13166
14488
|
process.exit(1);
|
|
13167
14489
|
}
|
|
13168
14490
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
13169
|
-
const llm = getLlm(provider, model, config);
|
|
14491
|
+
const llm = getLlm(provider, model, { ...config, service: recapService });
|
|
14492
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
13170
14493
|
const INTERACTIVE = argv.interactive || isInteractive(config);
|
|
13171
14494
|
if (INTERACTIVE) {
|
|
13172
14495
|
if (!config.hideCocoBanner) {
|
|
@@ -13197,19 +14520,49 @@ const handler$1 = async (argv, logger) => {
|
|
|
13197
14520
|
const unstagedChanges = await fileChangeParser({
|
|
13198
14521
|
changes: unstaged || [],
|
|
13199
14522
|
commit: '--unstaged',
|
|
13200
|
-
options: {
|
|
14523
|
+
options: {
|
|
14524
|
+
tokenizer,
|
|
14525
|
+
git,
|
|
14526
|
+
llm: summaryLlm,
|
|
14527
|
+
logger,
|
|
14528
|
+
metadata: {
|
|
14529
|
+
command: 'recap',
|
|
14530
|
+
provider,
|
|
14531
|
+
model: String(summaryService.model),
|
|
14532
|
+
},
|
|
14533
|
+
},
|
|
13201
14534
|
});
|
|
13202
14535
|
const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
|
|
13203
14536
|
const untrackedChanges = await fileChangeParser({
|
|
13204
14537
|
changes: untracked || [],
|
|
13205
14538
|
commit: '--untracked',
|
|
13206
|
-
options: {
|
|
14539
|
+
options: {
|
|
14540
|
+
tokenizer,
|
|
14541
|
+
git,
|
|
14542
|
+
llm: summaryLlm,
|
|
14543
|
+
logger,
|
|
14544
|
+
metadata: {
|
|
14545
|
+
command: 'recap',
|
|
14546
|
+
provider,
|
|
14547
|
+
model: String(summaryService.model),
|
|
14548
|
+
},
|
|
14549
|
+
},
|
|
13207
14550
|
});
|
|
13208
14551
|
const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
|
|
13209
14552
|
const stagedChanges = await fileChangeParser({
|
|
13210
14553
|
changes: staged,
|
|
13211
14554
|
commit: '--staged',
|
|
13212
|
-
options: {
|
|
14555
|
+
options: {
|
|
14556
|
+
tokenizer,
|
|
14557
|
+
git,
|
|
14558
|
+
llm: summaryLlm,
|
|
14559
|
+
logger,
|
|
14560
|
+
metadata: {
|
|
14561
|
+
command: 'recap',
|
|
14562
|
+
provider,
|
|
14563
|
+
model: String(summaryService.model),
|
|
14564
|
+
},
|
|
14565
|
+
},
|
|
13213
14566
|
});
|
|
13214
14567
|
const stagedResponse = `Staged changes:\n${stagedChanges}`;
|
|
13215
14568
|
return [unstagedResponse, untrackedResponse, stagedResponse];
|
|
@@ -13244,7 +14597,17 @@ const handler$1 = async (argv, logger) => {
|
|
|
13244
14597
|
const branchChanges = await fileChangeParser({
|
|
13245
14598
|
changes: changes.staged,
|
|
13246
14599
|
commit: baseBranch,
|
|
13247
|
-
options: {
|
|
14600
|
+
options: {
|
|
14601
|
+
tokenizer,
|
|
14602
|
+
git,
|
|
14603
|
+
llm: summaryLlm,
|
|
14604
|
+
logger,
|
|
14605
|
+
metadata: {
|
|
14606
|
+
command: 'recap',
|
|
14607
|
+
provider,
|
|
14608
|
+
model: String(summaryService.model),
|
|
14609
|
+
},
|
|
14610
|
+
},
|
|
13248
14611
|
});
|
|
13249
14612
|
return [branchChanges];
|
|
13250
14613
|
default:
|
|
@@ -13289,15 +14652,34 @@ const handler$1 = async (argv, logger) => {
|
|
|
13289
14652
|
try {
|
|
13290
14653
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13291
14654
|
const parser = createSchemaParser(RecapLlmResponseSchema, llm);
|
|
14655
|
+
const variables = {
|
|
14656
|
+
changes: context,
|
|
14657
|
+
format_instructions: formatInstructions,
|
|
14658
|
+
timeframe,
|
|
14659
|
+
};
|
|
14660
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
14661
|
+
prompt,
|
|
14662
|
+
variables,
|
|
14663
|
+
tokenizer,
|
|
14664
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
14665
|
+
summaryKey: 'changes',
|
|
14666
|
+
});
|
|
14667
|
+
if (budgetedPrompt.truncated) {
|
|
14668
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
14669
|
+
}
|
|
13292
14670
|
const response = await executeChain({
|
|
13293
14671
|
llm,
|
|
13294
14672
|
prompt,
|
|
13295
|
-
variables:
|
|
13296
|
-
changes: context,
|
|
13297
|
-
format_instructions: formatInstructions,
|
|
13298
|
-
timeframe,
|
|
13299
|
-
},
|
|
14673
|
+
variables: budgetedPrompt.variables,
|
|
13300
14674
|
parser,
|
|
14675
|
+
logger,
|
|
14676
|
+
tokenizer,
|
|
14677
|
+
metadata: {
|
|
14678
|
+
task: 'recap',
|
|
14679
|
+
command: 'recap',
|
|
14680
|
+
provider,
|
|
14681
|
+
model: String(model),
|
|
14682
|
+
},
|
|
13301
14683
|
});
|
|
13302
14684
|
return response ? `${response.title}\n\n${response.summary}` : 'no response';
|
|
13303
14685
|
}
|
|
@@ -13334,6 +14716,7 @@ ${errorMessage}
|
|
|
13334
14716
|
},
|
|
13335
14717
|
mode: MODE,
|
|
13336
14718
|
});
|
|
14719
|
+
logLlmTelemetrySummary(logger, 'recap');
|
|
13337
14720
|
};
|
|
13338
14721
|
|
|
13339
14722
|
var recap = {
|
|
@@ -13689,13 +15072,17 @@ const handler = async (argv, logger) => {
|
|
|
13689
15072
|
const git = getRepo();
|
|
13690
15073
|
const config = loadConfig(argv);
|
|
13691
15074
|
const key = getApiKeyForModel(config);
|
|
13692
|
-
const { provider
|
|
15075
|
+
const { provider } = getModelAndProviderFromConfig(config);
|
|
15076
|
+
const reviewService = resolveDynamicService(config, 'review');
|
|
15077
|
+
const summaryService = resolveDynamicService(config, argv.branch ? 'largeDiff' : 'summarize');
|
|
15078
|
+
const model = reviewService.model;
|
|
13693
15079
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
13694
15080
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
13695
15081
|
process.exit(1);
|
|
13696
15082
|
}
|
|
13697
15083
|
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
|
|
13698
|
-
const llm = getLlm(provider, model, config);
|
|
15084
|
+
const llm = getLlm(provider, model, { ...config, service: reviewService });
|
|
15085
|
+
const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
|
|
13699
15086
|
const INTERACTIVE = isInteractive(config);
|
|
13700
15087
|
if (INTERACTIVE) {
|
|
13701
15088
|
if (!config.hideCocoBanner) {
|
|
@@ -13719,7 +15106,17 @@ const handler = async (argv, logger) => {
|
|
|
13719
15106
|
const branchChanges = await fileChangeParser({
|
|
13720
15107
|
changes: diff.staged,
|
|
13721
15108
|
commit: `--branch-diff-${argv.branch}`,
|
|
13722
|
-
options: {
|
|
15109
|
+
options: {
|
|
15110
|
+
tokenizer,
|
|
15111
|
+
git,
|
|
15112
|
+
llm: summaryLlm,
|
|
15113
|
+
logger,
|
|
15114
|
+
metadata: {
|
|
15115
|
+
command: 'review',
|
|
15116
|
+
provider,
|
|
15117
|
+
model: String(summaryService.model),
|
|
15118
|
+
},
|
|
15119
|
+
},
|
|
13723
15120
|
});
|
|
13724
15121
|
return [branchChanges];
|
|
13725
15122
|
}
|
|
@@ -13741,19 +15138,49 @@ const handler = async (argv, logger) => {
|
|
|
13741
15138
|
const unstagedChanges = await fileChangeParser({
|
|
13742
15139
|
changes: unstaged || [],
|
|
13743
15140
|
commit: '--unstaged',
|
|
13744
|
-
options: {
|
|
15141
|
+
options: {
|
|
15142
|
+
tokenizer,
|
|
15143
|
+
git,
|
|
15144
|
+
llm: summaryLlm,
|
|
15145
|
+
logger,
|
|
15146
|
+
metadata: {
|
|
15147
|
+
command: 'review',
|
|
15148
|
+
provider,
|
|
15149
|
+
model: String(summaryService.model),
|
|
15150
|
+
},
|
|
15151
|
+
},
|
|
13745
15152
|
});
|
|
13746
15153
|
const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
|
|
13747
15154
|
const untrackedChanges = await fileChangeParser({
|
|
13748
15155
|
changes: untracked || [],
|
|
13749
15156
|
commit: '--untracked',
|
|
13750
|
-
options: {
|
|
15157
|
+
options: {
|
|
15158
|
+
tokenizer,
|
|
15159
|
+
git,
|
|
15160
|
+
llm: summaryLlm,
|
|
15161
|
+
logger,
|
|
15162
|
+
metadata: {
|
|
15163
|
+
command: 'review',
|
|
15164
|
+
provider,
|
|
15165
|
+
model: String(summaryService.model),
|
|
15166
|
+
},
|
|
15167
|
+
},
|
|
13751
15168
|
});
|
|
13752
15169
|
const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
|
|
13753
15170
|
const stagedChanges = await fileChangeParser({
|
|
13754
15171
|
changes: staged,
|
|
13755
15172
|
commit: '--staged',
|
|
13756
|
-
options: {
|
|
15173
|
+
options: {
|
|
15174
|
+
tokenizer,
|
|
15175
|
+
git,
|
|
15176
|
+
llm: summaryLlm,
|
|
15177
|
+
logger,
|
|
15178
|
+
metadata: {
|
|
15179
|
+
command: 'review',
|
|
15180
|
+
provider,
|
|
15181
|
+
model: String(summaryService.model),
|
|
15182
|
+
},
|
|
15183
|
+
},
|
|
13757
15184
|
});
|
|
13758
15185
|
const stagedResponse = `Staged changes:\n${stagedChanges}`;
|
|
13759
15186
|
return [unstagedResponse, untrackedResponse, stagedResponse];
|
|
@@ -13793,14 +15220,33 @@ const handler = async (argv, logger) => {
|
|
|
13793
15220
|
variables: REVIEW_PROMPT.inputVariables,
|
|
13794
15221
|
fallback: REVIEW_PROMPT,
|
|
13795
15222
|
});
|
|
15223
|
+
const variables = {
|
|
15224
|
+
changes: context,
|
|
15225
|
+
format_instructions: formatInstructions,
|
|
15226
|
+
};
|
|
15227
|
+
const budgetedPrompt = await enforcePromptBudget({
|
|
15228
|
+
prompt,
|
|
15229
|
+
variables,
|
|
15230
|
+
tokenizer,
|
|
15231
|
+
maxTokens: config.service.tokenLimit || 2048,
|
|
15232
|
+
summaryKey: 'changes',
|
|
15233
|
+
});
|
|
15234
|
+
if (budgetedPrompt.truncated) {
|
|
15235
|
+
logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
|
|
15236
|
+
}
|
|
13796
15237
|
const response = await executeChain({
|
|
13797
15238
|
llm,
|
|
13798
15239
|
prompt,
|
|
13799
|
-
variables:
|
|
13800
|
-
changes: context,
|
|
13801
|
-
format_instructions: formatInstructions,
|
|
13802
|
-
},
|
|
15240
|
+
variables: budgetedPrompt.variables,
|
|
13803
15241
|
parser,
|
|
15242
|
+
logger,
|
|
15243
|
+
tokenizer,
|
|
15244
|
+
metadata: {
|
|
15245
|
+
task: argv.branch ? 'review-branch' : 'review',
|
|
15246
|
+
command: 'review',
|
|
15247
|
+
provider,
|
|
15248
|
+
model: String(model),
|
|
15249
|
+
},
|
|
13804
15250
|
});
|
|
13805
15251
|
// sort by severity
|
|
13806
15252
|
return response.sort((a, b) => b.severity - a.severity);
|
|
@@ -13819,6 +15265,7 @@ const handler = async (argv, logger) => {
|
|
|
13819
15265
|
},
|
|
13820
15266
|
});
|
|
13821
15267
|
const reviewer = new TaskList(recap, { ...config, apiKey: key ?? undefined });
|
|
15268
|
+
logLlmTelemetrySummary(logger, 'review');
|
|
13822
15269
|
await reviewer.start();
|
|
13823
15270
|
};
|
|
13824
15271
|
|
|
@@ -13839,6 +15286,7 @@ y.command(changelog.command, changelog.desc, changelog.builder, changelog.handle
|
|
|
13839
15286
|
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
13840
15287
|
y.command(review.command, review.desc, review.builder, review.handler);
|
|
13841
15288
|
y.command(init.command, init.desc, init.builder, init.handler);
|
|
15289
|
+
y.command(log.command, log.desc, log.builder, log.handler);
|
|
13842
15290
|
y.help().parse(process.argv.slice(2));
|
|
13843
15291
|
|
|
13844
15292
|
/**
|
|
@@ -14293,5 +15741,6 @@ var commitValidationHandler = /*#__PURE__*/Object.freeze({
|
|
|
14293
15741
|
exports.changelog = changelog;
|
|
14294
15742
|
exports.commit = commit;
|
|
14295
15743
|
exports.init = init;
|
|
15744
|
+
exports.log = log;
|
|
14296
15745
|
exports.recap = recap;
|
|
14297
15746
|
exports.types = types;
|