git-coco 0.32.0 → 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/dist/index.js CHANGED
@@ -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.32.0";
71
+ const BUILD_VERSION = "0.33.0";
72
72
 
73
73
  const isInteractive = (config) => {
74
74
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -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/LLMModel"
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/LLMModel"
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/LLMModel"
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$4 = 'changelog';
7037
+ const command$5 = 'changelog';
6955
7038
  /**
6956
7039
  * Command line options via yargs
6957
7040
  */
6958
- const options$4 = {
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$4 = (yargs) => {
7006
- return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
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
  *
@@ -7392,7 +7681,7 @@ function extractLlmInfo(llm) {
7392
7681
  * @throws LangChainExecutionError if the chain execution fails or returns empty results
7393
7682
  * @throws LangChainNetworkError if a network/connection error occurs
7394
7683
  */
7395
- const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, }) => {
7684
+ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, logger, tokenizer, metadata, }) => {
7396
7685
  validateRequired(llm, 'llm', 'executeChain');
7397
7686
  validateRequired(prompt, 'prompt', 'executeChain');
7398
7687
  validateRequired(variables, 'variables', 'executeChain');
@@ -7410,8 +7699,21 @@ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint
7410
7699
  const effectiveProvider = provider || llmInfo.provider;
7411
7700
  const effectiveEndpoint = endpoint || llmInfo.endpoint;
7412
7701
  try {
7702
+ const renderedPrompt = await prompt.format(variables);
7703
+ const promptTokens = estimatePromptTokens(tokenizer, renderedPrompt);
7413
7704
  const chain = prompt.pipe(llm).pipe(parser);
7705
+ const startedAt = Date.now();
7414
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
+ });
7415
7717
  if (result === null || result === undefined) {
7416
7718
  throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
7417
7719
  }
@@ -8240,13 +8542,26 @@ function getPathFromFilePath(filePath) {
8240
8542
  return filePath.split('/').slice(0, -1).join('/');
8241
8543
  }
8242
8544
 
8243
- async function summarize(documents$1, { chain, textSplitter, options }) {
8545
+ async function summarize(documents$1, { chain, textSplitter, options, logger, tokenizer, metadata }) {
8244
8546
  const { returnIntermediateSteps = false } = options || {};
8245
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();
8246
8552
  const res = await chain.invoke({
8247
8553
  input_documents: docs,
8248
8554
  returnIntermediateSteps,
8249
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
+ });
8250
8565
  if (res.error)
8251
8566
  throw new Error(res.error);
8252
8567
  return res.text && res.text.trim();
@@ -8255,7 +8570,7 @@ async function summarize(documents$1, { chain, textSplitter, options }) {
8255
8570
  /**
8256
8571
  * Summarize a single file diff that exceeds the token threshold.
8257
8572
  */
8258
- async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
8573
+ async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer, logger, metadata, }) {
8259
8574
  try {
8260
8575
  const fileSummary = await summarize([
8261
8576
  {
@@ -8268,6 +8583,12 @@ async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
8268
8583
  ], {
8269
8584
  chain,
8270
8585
  textSplitter,
8586
+ tokenizer,
8587
+ logger,
8588
+ metadata: {
8589
+ ...metadata,
8590
+ task: 'summarize-large-file',
8591
+ },
8271
8592
  options: {
8272
8593
  returnIntermediateSteps: false,
8273
8594
  },
@@ -8307,7 +8628,7 @@ async function processInWaves$1(items, processor, maxConcurrent) {
8307
8628
  * @returns Array of file diffs with large files summarized
8308
8629
  */
8309
8630
  async function summarizeLargeFiles(diffs, options) {
8310
- const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter } = options;
8631
+ const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter, metadata } = options;
8311
8632
  // Identify files that need summarization
8312
8633
  const filesToSummarize = [];
8313
8634
  const results = [...diffs];
@@ -8321,7 +8642,7 @@ async function summarizeLargeFiles(diffs, options) {
8321
8642
  }
8322
8643
  logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
8323
8644
  // Process large files in waves
8324
- const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer }), maxConcurrent);
8645
+ const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer, logger, metadata }), maxConcurrent);
8325
8646
  // Update results with summarized files
8326
8647
  summarizedFiles.forEach((summarizedDiff, i) => {
8327
8648
  const originalIndex = filesToSummarize[i].index;
@@ -8384,7 +8705,7 @@ function createDirectoryDiffs(node) {
8384
8705
  /**
8385
8706
  * Summarize a directory diff asynchronously.
8386
8707
  */
8387
- async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
8708
+ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer, logger, metadata }) {
8388
8709
  try {
8389
8710
  const directorySummary = await summarize(directory.diffs.map((diff) => ({
8390
8711
  pageContent: diff.diff,
@@ -8395,6 +8716,12 @@ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenize
8395
8716
  })), {
8396
8717
  chain,
8397
8718
  textSplitter,
8719
+ tokenizer,
8720
+ logger,
8721
+ metadata: {
8722
+ ...metadata,
8723
+ task: 'summarize-directory-diff',
8724
+ },
8398
8725
  options: {
8399
8726
  returnIntermediateSteps: true,
8400
8727
  },
@@ -8438,7 +8765,7 @@ const defaultOutputCallback = (group) => {
8438
8765
  * while maintaining predictable behavior.
8439
8766
  */
8440
8767
  async function summarizeInWaves(directories, options) {
8441
- const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, } = options;
8768
+ const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, metadata, } = options;
8442
8769
  let totalTokenCount = initialTotal;
8443
8770
  const results = [...directories];
8444
8771
  // Create sorted indices by token count (descending) for prioritized processing
@@ -8470,7 +8797,7 @@ async function summarizeInWaves(directories, options) {
8470
8797
  }
8471
8798
  logger.verbose(`\nProcessing wave of ${wave.length} directories...`, { color: 'blue' });
8472
8799
  // Process wave in parallel
8473
- 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 })));
8474
8801
  // Update results and recalculate total
8475
8802
  waveResults.forEach((result, i) => {
8476
8803
  const idx = wave[i];
@@ -8507,7 +8834,7 @@ async function summarizeInWaves(directories, options) {
8507
8834
  * - Efficient parallel processing with predictable behavior
8508
8835
  * - Early exit when under token budget
8509
8836
  */
8510
- 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, }) {
8511
8838
  // Calculate maxFileTokens as 25% of maxTokens if not specified
8512
8839
  const effectiveMaxFileTokens = maxFileTokens ?? Math.floor(maxTokens * 0.25);
8513
8840
  // PHASE 1: Directory grouping & assessment
@@ -8535,6 +8862,7 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
8535
8862
  logger,
8536
8863
  chain,
8537
8864
  textSplitter,
8865
+ metadata,
8538
8866
  });
8539
8867
  logger.stopSpinner('Files pre-processed').stopTimer();
8540
8868
  directoryDiffs = createDirectoryDiffs(preprocessedNode);
@@ -8558,6 +8886,7 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
8558
8886
  chain,
8559
8887
  textSplitter,
8560
8888
  tokenizer,
8889
+ metadata,
8561
8890
  });
8562
8891
  logger.stopSpinner(`Diffs Consolidated`).stopTimer();
8563
8892
  return summarizedDiffs.map(handleOutput).join('');
@@ -10499,7 +10828,7 @@ function isObject(subject) {
10499
10828
  }
10500
10829
 
10501
10830
 
10502
- function toArray(sequence) {
10831
+ function toArray$1(sequence) {
10503
10832
  if (Array.isArray(sequence)) return sequence;
10504
10833
  else if (isNothing(sequence)) return [];
10505
10834
 
@@ -10541,7 +10870,7 @@ function isNegativeZero(number) {
10541
10870
 
10542
10871
  var isNothing_1 = isNothing;
10543
10872
  var isObject_1 = isObject;
10544
- var toArray_1 = toArray;
10873
+ var toArray_1 = toArray$1;
10545
10874
  var repeat_1 = repeat;
10546
10875
  var isNegativeZero_1 = isNegativeZero;
10547
10876
  var extend_1 = extend;
@@ -11512,7 +11841,7 @@ for (var i = 0; i < 256; i++) {
11512
11841
  simpleEscapeMap[i] = simpleEscapeSequence(i);
11513
11842
  }
11514
11843
 
11515
- async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, }, }) {
11844
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, metadata, }, }) {
11516
11845
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
11517
11846
  const summarizationChain = loadSummarizationChain(model, {
11518
11847
  type: 'map_reduce',
@@ -11540,6 +11869,7 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
11540
11869
  textSplitter,
11541
11870
  chain: summarizationChain,
11542
11871
  logger,
11872
+ metadata,
11543
11873
  });
11544
11874
  logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
11545
11875
  return summary;
@@ -11612,11 +11942,14 @@ async function processInWaves(items, processor, maxConcurrent = 6) {
11612
11942
  }
11613
11943
  return results;
11614
11944
  }
11615
- const handler$4 = async (argv, logger) => {
11945
+ const handler$5 = async (argv, logger) => {
11616
11946
  const config = loadConfig(argv);
11617
11947
  const git = getRepo();
11618
11948
  const key = getApiKeyForModel(config);
11619
- const { provider, model } = getModelAndProviderFromConfig(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;
11620
11953
  const exclusiveOptions = [
11621
11954
  argv.branch ? '--branch' : null,
11622
11955
  argv.tag ? '--tag' : null,
@@ -11630,7 +11963,8 @@ const handler$4 = async (argv, logger) => {
11630
11963
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
11631
11964
  process.exit(1);
11632
11965
  }
11633
- const llm = getLlm(provider, model, config);
11966
+ const llm = getLlm(provider, model, { ...config, service: changelogService });
11967
+ const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
11634
11968
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
11635
11969
  const INTERACTIVE = isInteractive(config);
11636
11970
  if (INTERACTIVE) {
@@ -11700,12 +12034,17 @@ const handler$4 = async (argv, logger) => {
11700
12034
  options: {
11701
12035
  tokenizer,
11702
12036
  git,
11703
- llm,
12037
+ llm: summaryLlm,
11704
12038
  logger,
11705
12039
  maxTokens: config.service.tokenLimit,
11706
12040
  minTokensForSummary: config.service.minTokensForSummary,
11707
12041
  maxFileTokens: config.service.maxFileTokens,
11708
12042
  maxConcurrent: config.service.maxConcurrent,
12043
+ metadata: {
12044
+ command: 'changelog',
12045
+ provider,
12046
+ model: String(summaryService.model),
12047
+ },
11709
12048
  },
11710
12049
  })
11711
12050
  : undefined,
@@ -11726,12 +12065,17 @@ const handler$4 = async (argv, logger) => {
11726
12065
  options: {
11727
12066
  tokenizer,
11728
12067
  git,
11729
- llm,
12068
+ llm: summaryLlm,
11730
12069
  logger,
11731
12070
  maxTokens: config.service.tokenLimit,
11732
12071
  minTokensForSummary: config.service.minTokensForSummary,
11733
12072
  maxFileTokens: config.service.maxFileTokens,
11734
12073
  maxConcurrent: config.service.maxConcurrent,
12074
+ metadata: {
12075
+ command: 'changelog',
12076
+ provider,
12077
+ model: String(summaryService.model),
12078
+ },
11735
12079
  },
11736
12080
  });
11737
12081
  return `## Diff for ${data.branch}\n\n${diffSummary}`;
@@ -11798,6 +12142,14 @@ const handler$4 = async (argv, logger) => {
11798
12142
  prompt,
11799
12143
  variables: budgetedPrompt.variables,
11800
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
+ },
11801
12153
  });
11802
12154
  const branchName = await getCurrentBranchName({ git });
11803
12155
  const ticketId = extractTicketIdFromBranchName(branchName);
@@ -11821,14 +12173,15 @@ const handler$4 = async (argv, logger) => {
11821
12173
  },
11822
12174
  mode: MODE,
11823
12175
  });
12176
+ logLlmTelemetrySummary(logger, 'changelog');
11824
12177
  };
11825
12178
 
11826
12179
  var changelog = {
11827
- command: command$4,
12180
+ command: command$5,
11828
12181
  desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
11829
- builder: builder$4,
11830
- handler: commandExecutor(handler$4),
11831
- options: options$4,
12182
+ builder: builder$5,
12183
+ handler: commandExecutor(handler$5),
12184
+ options: options$5,
11832
12185
  };
11833
12186
 
11834
12187
  const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
@@ -11845,11 +12198,11 @@ const ConventionalCommitMessageResponseSchema = objectType({
11845
12198
  body: stringType().describe("Body of the commit message")
11846
12199
  // .max(280, "Body must be 280 characters or less"),
11847
12200
  }).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
11848
- const command$3 = 'commit';
12201
+ const command$4 = 'commit';
11849
12202
  /**
11850
12203
  * Command line options via yargs
11851
12204
  */
11852
- const options$3 = {
12205
+ const options$4 = {
11853
12206
  i: {
11854
12207
  alias: 'interactive',
11855
12208
  description: 'Toggle interactive mode',
@@ -11905,9 +12258,24 @@ const options$3 = {
11905
12258
  default: false,
11906
12259
  alias: 'n',
11907
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
+ },
11908
12276
  };
11909
- const builder$3 = (yargs) => {
11910
- return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
12277
+ const builder$4 = (yargs) => {
12278
+ return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
11911
12279
  };
11912
12280
 
11913
12281
  /**
@@ -11921,15 +12289,24 @@ const builder$3 = (yargs) => {
11921
12289
  * @returns Parsed result matching the schema type
11922
12290
  */
11923
12291
  async function executeChainWithSchema(schema, llm, prompt, variables, options = {}) {
11924
- const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, ...parserOptions } = options;
12292
+ const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, logger, tokenizer, metadata, ...parserOptions } = options;
11925
12293
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11926
12294
  const parser = createSchemaParser(schema, llm, parserOptions);
12295
+ let attempt = 0;
11927
12296
  const operation = async () => {
12297
+ attempt++;
11928
12298
  const result = await executeChain({
11929
12299
  llm,
11930
12300
  prompt,
11931
12301
  variables,
11932
12302
  parser,
12303
+ logger,
12304
+ tokenizer,
12305
+ metadata: {
12306
+ task: 'schema-chain',
12307
+ ...metadata,
12308
+ retryAttempt: attempt,
12309
+ },
11933
12310
  });
11934
12311
  return result;
11935
12312
  };
@@ -11946,6 +12323,12 @@ async function executeChainWithSchema(schema, llm, prompt, variables, options =
11946
12323
  prompt,
11947
12324
  variables,
11948
12325
  parser: new output_parsers.StringOutputParser(),
12326
+ logger,
12327
+ tokenizer,
12328
+ metadata: {
12329
+ task: 'schema-chain-fallback',
12330
+ ...metadata,
12331
+ },
11949
12332
  });
11950
12333
  const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
11951
12334
  return fallbackParser(fallbackText);
@@ -12408,17 +12791,345 @@ async function noResult$2({ git, logger }) {
12408
12791
  }
12409
12792
  }
12410
12793
 
12411
- const handler$3 = async (argv, logger) => {
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) => {
12412
13119
  const git = getRepo();
12413
13120
  const config = loadConfig(argv);
12414
13121
  const key = getApiKeyForModel(config);
12415
- const { provider, model } = getModelAndProviderFromConfig(config);
13122
+ const { provider } = getModelAndProviderFromConfig(config);
13123
+ const commitService = resolveDynamicService(config, 'commit');
13124
+ const summaryService = resolveDynamicService(config, 'summarize');
13125
+ const model = commitService.model;
12416
13126
  if (config.service.authentication.type !== 'None' && !key) {
12417
13127
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
12418
13128
  process.exit(1);
12419
13129
  }
12420
13130
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
12421
- 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 });
12422
13133
  const INTERACTIVE = argv.interactive || isInteractive(config);
12423
13134
  if (INTERACTIVE) {
12424
13135
  if (!config.hideCocoBanner) {
@@ -12436,6 +13147,22 @@ const handler$3 = async (argv, logger) => {
12436
13147
  logger.verbose(`→ ${provider} (${model})`, {
12437
13148
  color: 'green',
12438
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
+ }
12439
13166
  const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
12440
13167
  async function factory() {
12441
13168
  if (config.noDiff) {
@@ -12473,12 +13200,17 @@ const handler$3 = async (argv, logger) => {
12473
13200
  options: {
12474
13201
  tokenizer,
12475
13202
  git,
12476
- llm,
13203
+ llm: summaryLlm,
12477
13204
  logger,
12478
13205
  maxTokens: config.service.tokenLimit,
12479
13206
  minTokensForSummary: config.service.minTokensForSummary,
12480
13207
  maxFileTokens: config.service.maxFileTokens,
12481
13208
  maxConcurrent: config.service.maxConcurrent,
13209
+ metadata: {
13210
+ command: 'commit',
13211
+ provider,
13212
+ model: String(summaryService.model),
13213
+ },
12482
13214
  },
12483
13215
  });
12484
13216
  }
@@ -12628,6 +13360,14 @@ IMPORTANT RULES:
12628
13360
  logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
12629
13361
  }
12630
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
+ },
12631
13371
  retryOptions: {
12632
13372
  maxAttempts,
12633
13373
  onRetry: (attempt, error) => {
@@ -12836,29 +13576,30 @@ IMPORTANT RULES:
12836
13576
  },
12837
13577
  mode: MODE,
12838
13578
  });
13579
+ logLlmTelemetrySummary(logger, 'commit');
12839
13580
  };
12840
13581
 
12841
13582
  var commit = {
12842
- command: command$3,
13583
+ command: command$4,
12843
13584
  desc: 'Summarize the staged changes in a commit message.',
12844
- builder: builder$3,
12845
- handler: commandExecutor(handler$3),
12846
- options: options$3,
13585
+ builder: builder$4,
13586
+ handler: commandExecutor(handler$4),
13587
+ options: options$4,
12847
13588
  };
12848
13589
 
12849
- const command$2 = 'init';
13590
+ const command$3 = 'init';
12850
13591
  /**
12851
13592
  * Command line options via yargs
12852
13593
  */
12853
- const options$2 = {
13594
+ const options$3 = {
12854
13595
  scope: {
12855
13596
  type: 'string',
12856
13597
  description: 'configure coco for the current user or project?',
12857
13598
  choices: ['global', 'project'],
12858
13599
  },
12859
13600
  };
12860
- const builder$2 = (yargs) => {
12861
- return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
13601
+ const builder$3 = (yargs) => {
13602
+ return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
12862
13603
  };
12863
13604
 
12864
13605
  /**
@@ -13209,7 +13950,7 @@ const questions = {
13209
13950
  }),
13210
13951
  };
13211
13952
 
13212
- const handler$2 = async (argv, logger) => {
13953
+ const handler$3 = async (argv, logger) => {
13213
13954
  const options = loadConfig(argv);
13214
13955
  logger.log(LOGO);
13215
13956
  let scope = options?.scope;
@@ -13380,8 +14121,287 @@ async function installCommitlintPackages(scope, logger) {
13380
14121
  }
13381
14122
 
13382
14123
  var init = {
13383
- command: command$2,
14124
+ command: command$3,
13384
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.',
13385
14405
  builder: builder$2,
13386
14406
  handler: commandExecutor(handler$2),
13387
14407
  options: options$2,
@@ -13459,13 +14479,17 @@ const handler$1 = async (argv, logger) => {
13459
14479
  const git = getRepo();
13460
14480
  const config = loadConfig(argv);
13461
14481
  const key = getApiKeyForModel(config);
13462
- const { provider, model } = getModelAndProviderFromConfig(config);
14482
+ const { provider } = getModelAndProviderFromConfig(config);
14483
+ const recapService = resolveDynamicService(config, 'recap');
14484
+ const summaryService = resolveDynamicService(config, 'summarize');
14485
+ const model = recapService.model;
13463
14486
  if (config.service.authentication.type !== 'None' && !key) {
13464
14487
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
13465
14488
  process.exit(1);
13466
14489
  }
13467
14490
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
13468
- 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 });
13469
14493
  const INTERACTIVE = argv.interactive || isInteractive(config);
13470
14494
  if (INTERACTIVE) {
13471
14495
  if (!config.hideCocoBanner) {
@@ -13496,19 +14520,49 @@ const handler$1 = async (argv, logger) => {
13496
14520
  const unstagedChanges = await fileChangeParser({
13497
14521
  changes: unstaged || [],
13498
14522
  commit: '--unstaged',
13499
- options: { tokenizer, git, llm, logger },
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
+ },
13500
14534
  });
13501
14535
  const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
13502
14536
  const untrackedChanges = await fileChangeParser({
13503
14537
  changes: untracked || [],
13504
14538
  commit: '--untracked',
13505
- options: { tokenizer, git, llm, logger },
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
+ },
13506
14550
  });
13507
14551
  const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
13508
14552
  const stagedChanges = await fileChangeParser({
13509
14553
  changes: staged,
13510
14554
  commit: '--staged',
13511
- options: { tokenizer, git, llm, logger },
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
+ },
13512
14566
  });
13513
14567
  const stagedResponse = `Staged changes:\n${stagedChanges}`;
13514
14568
  return [unstagedResponse, untrackedResponse, stagedResponse];
@@ -13543,7 +14597,17 @@ const handler$1 = async (argv, logger) => {
13543
14597
  const branchChanges = await fileChangeParser({
13544
14598
  changes: changes.staged,
13545
14599
  commit: baseBranch,
13546
- options: { tokenizer, git, llm, logger },
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
+ },
13547
14611
  });
13548
14612
  return [branchChanges];
13549
14613
  default:
@@ -13588,15 +14652,34 @@ const handler$1 = async (argv, logger) => {
13588
14652
  try {
13589
14653
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13590
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
+ }
13591
14670
  const response = await executeChain({
13592
14671
  llm,
13593
14672
  prompt,
13594
- variables: {
13595
- changes: context,
13596
- format_instructions: formatInstructions,
13597
- timeframe,
13598
- },
14673
+ variables: budgetedPrompt.variables,
13599
14674
  parser,
14675
+ logger,
14676
+ tokenizer,
14677
+ metadata: {
14678
+ task: 'recap',
14679
+ command: 'recap',
14680
+ provider,
14681
+ model: String(model),
14682
+ },
13600
14683
  });
13601
14684
  return response ? `${response.title}\n\n${response.summary}` : 'no response';
13602
14685
  }
@@ -13633,6 +14716,7 @@ ${errorMessage}
13633
14716
  },
13634
14717
  mode: MODE,
13635
14718
  });
14719
+ logLlmTelemetrySummary(logger, 'recap');
13636
14720
  };
13637
14721
 
13638
14722
  var recap = {
@@ -13988,13 +15072,17 @@ const handler = async (argv, logger) => {
13988
15072
  const git = getRepo();
13989
15073
  const config = loadConfig(argv);
13990
15074
  const key = getApiKeyForModel(config);
13991
- const { provider, model } = getModelAndProviderFromConfig(config);
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;
13992
15079
  if (config.service.authentication.type !== 'None' && !key) {
13993
15080
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
13994
15081
  process.exit(1);
13995
15082
  }
13996
15083
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
13997
- 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 });
13998
15086
  const INTERACTIVE = isInteractive(config);
13999
15087
  if (INTERACTIVE) {
14000
15088
  if (!config.hideCocoBanner) {
@@ -14018,7 +15106,17 @@ const handler = async (argv, logger) => {
14018
15106
  const branchChanges = await fileChangeParser({
14019
15107
  changes: diff.staged,
14020
15108
  commit: `--branch-diff-${argv.branch}`,
14021
- options: { tokenizer, git, llm, logger },
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
+ },
14022
15120
  });
14023
15121
  return [branchChanges];
14024
15122
  }
@@ -14040,19 +15138,49 @@ const handler = async (argv, logger) => {
14040
15138
  const unstagedChanges = await fileChangeParser({
14041
15139
  changes: unstaged || [],
14042
15140
  commit: '--unstaged',
14043
- options: { tokenizer, git, llm, logger },
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
+ },
14044
15152
  });
14045
15153
  const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
14046
15154
  const untrackedChanges = await fileChangeParser({
14047
15155
  changes: untracked || [],
14048
15156
  commit: '--untracked',
14049
- options: { tokenizer, git, llm, logger },
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
+ },
14050
15168
  });
14051
15169
  const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
14052
15170
  const stagedChanges = await fileChangeParser({
14053
15171
  changes: staged,
14054
15172
  commit: '--staged',
14055
- options: { tokenizer, git, llm, logger },
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
+ },
14056
15184
  });
14057
15185
  const stagedResponse = `Staged changes:\n${stagedChanges}`;
14058
15186
  return [unstagedResponse, untrackedResponse, stagedResponse];
@@ -14092,14 +15220,33 @@ const handler = async (argv, logger) => {
14092
15220
  variables: REVIEW_PROMPT.inputVariables,
14093
15221
  fallback: REVIEW_PROMPT,
14094
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
+ }
14095
15237
  const response = await executeChain({
14096
15238
  llm,
14097
15239
  prompt,
14098
- variables: {
14099
- changes: context,
14100
- format_instructions: formatInstructions,
14101
- },
15240
+ variables: budgetedPrompt.variables,
14102
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
+ },
14103
15250
  });
14104
15251
  // sort by severity
14105
15252
  return response.sort((a, b) => b.severity - a.severity);
@@ -14118,6 +15265,7 @@ const handler = async (argv, logger) => {
14118
15265
  },
14119
15266
  });
14120
15267
  const reviewer = new TaskList(recap, { ...config, apiKey: key ?? undefined });
15268
+ logLlmTelemetrySummary(logger, 'review');
14121
15269
  await reviewer.start();
14122
15270
  };
14123
15271
 
@@ -14138,6 +15286,7 @@ y.command(changelog.command, changelog.desc, changelog.builder, changelog.handle
14138
15286
  y.command(recap.command, recap.desc, recap.builder, recap.handler);
14139
15287
  y.command(review.command, review.desc, review.builder, review.handler);
14140
15288
  y.command(init.command, init.desc, init.builder, init.handler);
15289
+ y.command(log.command, log.desc, log.builder, log.handler);
14141
15290
  y.help().parse(process.argv.slice(2));
14142
15291
 
14143
15292
  /**
@@ -14592,5 +15741,6 @@ var commitValidationHandler = /*#__PURE__*/Object.freeze({
14592
15741
  exports.changelog = changelog;
14593
15742
  exports.commit = commit;
14594
15743
  exports.init = init;
15744
+ exports.log = log;
14595
15745
  exports.recap = recap;
14596
15746
  exports.types = types;