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.
@@ -22,7 +22,7 @@ import { StructuredOutputParser, BaseOutputParser, StringOutputParser } from '@l
22
22
  import { minimatch } from 'minimatch';
23
23
  import { simpleGit, GitError } from 'simple-git';
24
24
  import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
25
- import { createTwoFilesPatch } from 'diff';
25
+ import { createTwoFilesPatch, parsePatch, formatPatch } from 'diff';
26
26
  import { ensureConfig, Runnable } from '@langchain/core/runnables';
27
27
  import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
28
28
  import { RUN_KEY } from '@langchain/core/outputs';
@@ -37,7 +37,7 @@ import '@langchain/core/utils/json_patch';
37
37
  import '@langchain/core/utils/env';
38
38
  import '@langchain/core/utils/async_caller';
39
39
  import { encoding_for_model } from 'tiktoken';
40
- import { exec, spawn } from 'child_process';
40
+ import { spawn, exec } from 'child_process';
41
41
  import * as readline from 'readline';
42
42
  import { pathToFileURL } from 'url';
43
43
 
@@ -46,7 +46,7 @@ import { pathToFileURL } from 'url';
46
46
  /**
47
47
  * Current build version from package.json
48
48
  */
49
- const BUILD_VERSION = "0.32.0";
49
+ const BUILD_VERSION = "0.33.0";
50
50
 
51
51
  const isInteractive = (config) => {
52
52
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -501,6 +501,8 @@ function loadEnvConfig(config) {
501
501
  'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT',
502
502
  'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES',
503
503
  'COCO_SERVICE_FIELDS',
504
+ 'COCO_SERVICE_DYNAMIC_MODELS',
505
+ 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE',
504
506
  ];
505
507
  envKeys.forEach((key) => {
506
508
  const envVarName = toEnvVarName(key);
@@ -515,7 +517,9 @@ function loadEnvConfig(config) {
515
517
  key === 'COCO_SERVICE_ENDPOINT' ||
516
518
  key === 'COCO_SERVICE_REQUEST_OPTIONS_TIMEOUT' ||
517
519
  key === 'COCO_SERVICE_REQUEST_OPTIONS_MAX_RETRIES' ||
518
- key === 'COCO_SERVICE_FIELDS') {
520
+ key === 'COCO_SERVICE_FIELDS' ||
521
+ key === 'COCO_SERVICE_DYNAMIC_MODELS' ||
522
+ key === 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE') {
519
523
  // NOTE: We want to ensure that the service object is always defined
520
524
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
521
525
  // @ts-ignore
@@ -570,6 +574,12 @@ function handleServiceEnvVar(service, key, value) {
570
574
  case 'COCO_SERVICE_FIELDS':
571
575
  service.fields = value;
572
576
  break;
577
+ case 'COCO_SERVICE_DYNAMIC_MODELS':
578
+ service.dynamicModels = value;
579
+ break;
580
+ case 'COCO_SERVICE_DYNAMIC_MODEL_PREFERENCE':
581
+ service.dynamicModelPreference = value;
582
+ break;
573
583
  }
574
584
  }
575
585
  function parseEnvValue(key, value) {
@@ -978,7 +988,7 @@ const schema$1 = {
978
988
  "$ref": "#/definitions/LLMProvider"
979
989
  },
980
990
  "model": {
981
- "$ref": "#/definitions/LLMModel"
991
+ "$ref": "#/definitions/ConfiguredLLMModel"
982
992
  },
983
993
  "baseURL": {
984
994
  "type": "string",
@@ -1670,6 +1680,15 @@ const schema$1 = {
1670
1680
  "type": "number",
1671
1681
  "description": "The maximum number of attempts for schema parsing with retry logic.",
1672
1682
  "default": 3
1683
+ },
1684
+ "dynamicModels": {
1685
+ "$ref": "#/definitions/DynamicModelProfile",
1686
+ "description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
1687
+ },
1688
+ "dynamicModelPreference": {
1689
+ "$ref": "#/definitions/DynamicModelPreference",
1690
+ "description": "Default dynamic routing preference when model is set to \"dynamic\".",
1691
+ "default": "balanced"
1673
1692
  }
1674
1693
  },
1675
1694
  "required": [
@@ -1686,6 +1705,17 @@ const schema$1 = {
1686
1705
  "anthropic"
1687
1706
  ]
1688
1707
  },
1708
+ "ConfiguredLLMModel": {
1709
+ "anyOf": [
1710
+ {
1711
+ "$ref": "#/definitions/LLMModel"
1712
+ },
1713
+ {
1714
+ "type": "string",
1715
+ "const": "dynamic"
1716
+ }
1717
+ ]
1718
+ },
1689
1719
  "LLMModel": {
1690
1720
  "anyOf": [
1691
1721
  {
@@ -1983,6 +2013,41 @@ const schema$1 = {
1983
2013
  null
1984
2014
  ]
1985
2015
  },
2016
+ "DynamicModelProfile": {
2017
+ "type": "object",
2018
+ "properties": {
2019
+ "summarize": {
2020
+ "$ref": "#/definitions/LLMModel"
2021
+ },
2022
+ "commit": {
2023
+ "$ref": "#/definitions/LLMModel"
2024
+ },
2025
+ "changelog": {
2026
+ "$ref": "#/definitions/LLMModel"
2027
+ },
2028
+ "review": {
2029
+ "$ref": "#/definitions/LLMModel"
2030
+ },
2031
+ "recap": {
2032
+ "$ref": "#/definitions/LLMModel"
2033
+ },
2034
+ "repair": {
2035
+ "$ref": "#/definitions/LLMModel"
2036
+ },
2037
+ "largeDiff": {
2038
+ "$ref": "#/definitions/LLMModel"
2039
+ }
2040
+ },
2041
+ "additionalProperties": false
2042
+ },
2043
+ "DynamicModelPreference": {
2044
+ "type": "string",
2045
+ "enum": [
2046
+ "cost",
2047
+ "balanced",
2048
+ "quality"
2049
+ ]
2050
+ },
1986
2051
  "OllamaLLMService": {
1987
2052
  "type": "object",
1988
2053
  "additionalProperties": false,
@@ -1991,7 +2056,7 @@ const schema$1 = {
1991
2056
  "$ref": "#/definitions/LLMProvider"
1992
2057
  },
1993
2058
  "model": {
1994
- "$ref": "#/definitions/LLMModel"
2059
+ "$ref": "#/definitions/ConfiguredLLMModel"
1995
2060
  },
1996
2061
  "endpoint": {
1997
2062
  "type": "string"
@@ -2711,6 +2776,15 @@ const schema$1 = {
2711
2776
  "type": "number",
2712
2777
  "description": "The maximum number of attempts for schema parsing with retry logic.",
2713
2778
  "default": 3
2779
+ },
2780
+ "dynamicModels": {
2781
+ "$ref": "#/definitions/DynamicModelProfile",
2782
+ "description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
2783
+ },
2784
+ "dynamicModelPreference": {
2785
+ "$ref": "#/definitions/DynamicModelPreference",
2786
+ "description": "Default dynamic routing preference when model is set to \"dynamic\".",
2787
+ "default": "balanced"
2714
2788
  }
2715
2789
  },
2716
2790
  "required": [
@@ -2733,7 +2807,7 @@ const schema$1 = {
2733
2807
  "$ref": "#/definitions/LLMProvider"
2734
2808
  },
2735
2809
  "model": {
2736
- "$ref": "#/definitions/LLMModel"
2810
+ "$ref": "#/definitions/ConfiguredLLMModel"
2737
2811
  },
2738
2812
  "fields": {
2739
2813
  "type": "object",
@@ -2863,6 +2937,15 @@ const schema$1 = {
2863
2937
  "type": "number",
2864
2938
  "description": "The maximum number of attempts for schema parsing with retry logic.",
2865
2939
  "default": 3
2940
+ },
2941
+ "dynamicModels": {
2942
+ "$ref": "#/definitions/DynamicModelProfile",
2943
+ "description": "Optional task-to-model overrides used when model is set to \"dynamic\"."
2944
+ },
2945
+ "dynamicModelPreference": {
2946
+ "$ref": "#/definitions/DynamicModelPreference",
2947
+ "description": "Default dynamic routing preference when model is set to \"dynamic\".",
2948
+ "default": "balanced"
2866
2949
  }
2867
2950
  },
2868
2951
  "required": [
@@ -6929,11 +7012,11 @@ const ChangelogResponseSchema = objectType({
6929
7012
  title: stringType(),
6930
7013
  content: stringType(),
6931
7014
  });
6932
- const command$4 = 'changelog';
7015
+ const command$5 = 'changelog';
6933
7016
  /**
6934
7017
  * Command line options via yargs
6935
7018
  */
6936
- const options$4 = {
7019
+ const options$5 = {
6937
7020
  range: {
6938
7021
  type: 'string',
6939
7022
  alias: 'r',
@@ -6980,8 +7063,8 @@ const options$4 = {
6980
7063
  description: 'Toggle interactive mode',
6981
7064
  },
6982
7065
  };
6983
- const builder$4 = (yargs) => {
6984
- return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
7066
+ const builder$5 = (yargs) => {
7067
+ return yargs.options(options$5).usage(getCommandUsageHeader(command$5));
6985
7068
  };
6986
7069
 
6987
7070
  /**
@@ -7181,6 +7264,212 @@ function getLlm(provider, model, config) {
7181
7264
  }
7182
7265
  }
7183
7266
 
7267
+ const OPENAI_DYNAMIC_DEFAULTS = {
7268
+ cost: {
7269
+ summarize: 'gpt-4.1-nano',
7270
+ commit: 'gpt-4.1-mini',
7271
+ changelog: 'gpt-4.1-mini',
7272
+ review: 'gpt-4.1-mini',
7273
+ recap: 'gpt-4.1-nano',
7274
+ repair: 'gpt-4.1-mini',
7275
+ largeDiff: 'gpt-4.1',
7276
+ },
7277
+ balanced: {
7278
+ summarize: 'gpt-4.1-mini',
7279
+ commit: 'gpt-4.1-mini',
7280
+ changelog: 'gpt-4.1',
7281
+ review: 'gpt-4.1',
7282
+ recap: 'gpt-4.1-mini',
7283
+ repair: 'gpt-4.1',
7284
+ largeDiff: 'gpt-4.1',
7285
+ },
7286
+ quality: {
7287
+ summarize: 'gpt-4.1-mini',
7288
+ commit: 'gpt-4.1',
7289
+ changelog: 'gpt-4.1',
7290
+ review: 'gpt-4.1',
7291
+ recap: 'gpt-4.1',
7292
+ repair: 'gpt-4.1',
7293
+ largeDiff: 'gpt-4.1',
7294
+ },
7295
+ };
7296
+ const ANTHROPIC_DYNAMIC_DEFAULTS = {
7297
+ cost: {
7298
+ summarize: 'claude-3-5-haiku-latest',
7299
+ commit: 'claude-3-5-haiku-latest',
7300
+ changelog: 'claude-3-5-sonnet-latest',
7301
+ review: 'claude-3-5-sonnet-latest',
7302
+ recap: 'claude-3-5-haiku-latest',
7303
+ repair: 'claude-3-5-sonnet-latest',
7304
+ largeDiff: 'claude-3-5-sonnet-latest',
7305
+ },
7306
+ balanced: {
7307
+ summarize: 'claude-3-5-haiku-latest',
7308
+ commit: 'claude-3-5-sonnet-latest',
7309
+ changelog: 'claude-3-5-sonnet-latest',
7310
+ review: 'claude-3-7-sonnet-latest',
7311
+ recap: 'claude-3-5-sonnet-latest',
7312
+ repair: 'claude-3-7-sonnet-latest',
7313
+ largeDiff: 'claude-3-7-sonnet-latest',
7314
+ },
7315
+ quality: {
7316
+ summarize: 'claude-3-5-sonnet-latest',
7317
+ commit: 'claude-3-7-sonnet-latest',
7318
+ changelog: 'claude-3-7-sonnet-latest',
7319
+ review: 'claude-sonnet-4-0',
7320
+ recap: 'claude-3-7-sonnet-latest',
7321
+ repair: 'claude-sonnet-4-0',
7322
+ largeDiff: 'claude-sonnet-4-0',
7323
+ },
7324
+ };
7325
+ const OLLAMA_DYNAMIC_DEFAULTS = {
7326
+ cost: {
7327
+ summarize: 'llama3.2:3b',
7328
+ commit: 'llama3.1:8b',
7329
+ changelog: 'llama3.1:8b',
7330
+ review: 'qwen2.5-coder:7b',
7331
+ recap: 'llama3.2:3b',
7332
+ repair: 'qwen2.5-coder:7b',
7333
+ largeDiff: 'qwen2.5-coder:14b',
7334
+ },
7335
+ balanced: {
7336
+ summarize: 'llama3.1:8b',
7337
+ commit: 'qwen2.5-coder:14b',
7338
+ changelog: 'qwen2.5-coder:14b',
7339
+ review: 'qwen2.5-coder:32b',
7340
+ recap: 'llama3.1:8b',
7341
+ repair: 'qwen2.5-coder:32b',
7342
+ largeDiff: 'qwen2.5-coder:32b',
7343
+ },
7344
+ quality: {
7345
+ summarize: 'qwen2.5-coder:14b',
7346
+ commit: 'qwen2.5-coder:32b',
7347
+ changelog: 'qwen2.5-coder:32b',
7348
+ review: 'qwen2.5-coder:32b',
7349
+ recap: 'qwen2.5-coder:14b',
7350
+ repair: 'qwen2.5-coder:32b',
7351
+ largeDiff: 'qwen2.5-coder:32b',
7352
+ },
7353
+ };
7354
+ const DYNAMIC_DEFAULTS = {
7355
+ openai: OPENAI_DYNAMIC_DEFAULTS,
7356
+ anthropic: ANTHROPIC_DYNAMIC_DEFAULTS,
7357
+ ollama: OLLAMA_DYNAMIC_DEFAULTS,
7358
+ };
7359
+ const DYNAMIC_MODEL_TASKS = [
7360
+ 'summarize',
7361
+ 'commit',
7362
+ 'changelog',
7363
+ 'review',
7364
+ 'recap',
7365
+ 'repair',
7366
+ 'largeDiff',
7367
+ ];
7368
+ function validateDynamicModelProfile(service) {
7369
+ const dynamicModels = service.dynamicModels;
7370
+ if (!dynamicModels)
7371
+ return;
7372
+ const unknownTasks = Object.keys(dynamicModels).filter((task) => !DYNAMIC_MODEL_TASKS.includes(task));
7373
+ if (unknownTasks.length > 0) {
7374
+ throw new LangChainConfigurationError(`Unknown dynamic model task(s): ${unknownTasks.join(', ')}. Supported tasks: ${DYNAMIC_MODEL_TASKS.join(', ')}`, { unknownTasks, supportedTasks: DYNAMIC_MODEL_TASKS });
7375
+ }
7376
+ Object.entries(dynamicModels).forEach(([task, model]) => {
7377
+ if (typeof model !== 'string' || model.trim() === '' || model === 'dynamic') {
7378
+ throw new LangChainConfigurationError(`Dynamic model override for '${task}' must be a concrete model name`, { task, model });
7379
+ }
7380
+ });
7381
+ }
7382
+ function resolveDynamicModel(config, task) {
7383
+ const service = config.service;
7384
+ validateDynamicModelProfile(service);
7385
+ if (service.model !== 'dynamic') {
7386
+ return service.model;
7387
+ }
7388
+ const preference = service.dynamicModelPreference || 'balanced';
7389
+ const providerDefaults = DYNAMIC_DEFAULTS[service.provider];
7390
+ const defaultModel = providerDefaults[preference]?.[task];
7391
+ return service.dynamicModels?.[task] || defaultModel;
7392
+ }
7393
+ function resolveDynamicService(config, task) {
7394
+ const model = resolveDynamicModel(config, task);
7395
+ return {
7396
+ ...config.service,
7397
+ model,
7398
+ };
7399
+ }
7400
+
7401
+ const telemetryByCommand = new Map();
7402
+ function estimatePromptTokens(tokenizer, renderedPrompt) {
7403
+ if (!tokenizer)
7404
+ return undefined;
7405
+ try {
7406
+ return tokenizer(renderedPrompt);
7407
+ }
7408
+ catch {
7409
+ return undefined;
7410
+ }
7411
+ }
7412
+ function logLlmCall(logger, metadata) {
7413
+ if (!logger)
7414
+ return;
7415
+ recordLlmTelemetry(metadata);
7416
+ const fields = [
7417
+ `task=${metadata.task}`,
7418
+ metadata.command ? `command=${metadata.command}` : undefined,
7419
+ metadata.provider ? `provider=${metadata.provider}` : undefined,
7420
+ metadata.model ? `model=${metadata.model}` : undefined,
7421
+ metadata.retryAttempt ? `retryAttempt=${metadata.retryAttempt}` : undefined,
7422
+ metadata.promptTokens !== undefined ? `promptTokens=${metadata.promptTokens}` : undefined,
7423
+ metadata.elapsedMs !== undefined ? `elapsedMs=${metadata.elapsedMs}` : undefined,
7424
+ metadata.inputDocuments !== undefined ? `inputDocuments=${metadata.inputDocuments}` : undefined,
7425
+ metadata.inputChunks !== undefined ? `inputChunks=${metadata.inputChunks}` : undefined,
7426
+ metadata.parserType ? `parser=${metadata.parserType}` : undefined,
7427
+ metadata.variableKeys?.length ? `variableKeys=${metadata.variableKeys.join(',')}` : undefined,
7428
+ ].filter(Boolean);
7429
+ logger.verbose(`[llm] ${fields.join(' ')}`, { color: 'cyan' });
7430
+ }
7431
+ function recordLlmTelemetry(metadata) {
7432
+ const command = metadata.command || 'unknown';
7433
+ const current = telemetryByCommand.get(command) || {
7434
+ calls: 0,
7435
+ promptTokens: 0,
7436
+ elapsedMs: 0,
7437
+ inputDocuments: 0,
7438
+ inputChunks: 0,
7439
+ tasks: new Set(),
7440
+ models: new Set(),
7441
+ };
7442
+ current.calls += 1;
7443
+ current.promptTokens += metadata.promptTokens || 0;
7444
+ current.elapsedMs += metadata.elapsedMs || 0;
7445
+ current.inputDocuments += metadata.inputDocuments || 0;
7446
+ current.inputChunks += metadata.inputChunks || 0;
7447
+ current.tasks.add(metadata.task);
7448
+ if (metadata.model) {
7449
+ current.models.add(metadata.model);
7450
+ }
7451
+ telemetryByCommand.set(command, current);
7452
+ }
7453
+ function logLlmTelemetrySummary(logger, command) {
7454
+ if (!logger)
7455
+ return;
7456
+ const summary = telemetryByCommand.get(command);
7457
+ if (!summary || summary.calls === 0)
7458
+ return;
7459
+ const fields = [
7460
+ `command=${command}`,
7461
+ `calls=${summary.calls}`,
7462
+ summary.promptTokens > 0 ? `promptTokens=${summary.promptTokens}` : undefined,
7463
+ summary.elapsedMs > 0 ? `elapsedMs=${summary.elapsedMs}` : undefined,
7464
+ summary.inputDocuments > 0 ? `inputDocuments=${summary.inputDocuments}` : undefined,
7465
+ summary.inputChunks > 0 ? `inputChunks=${summary.inputChunks}` : undefined,
7466
+ summary.tasks.size > 0 ? `tasks=${[...summary.tasks].join(',')}` : undefined,
7467
+ summary.models.size > 0 ? `models=${[...summary.models].join(',')}` : undefined,
7468
+ ].filter(Boolean);
7469
+ logger.verbose(`[llm:summary] ${fields.join(' ')}`, { color: 'cyan' });
7470
+ telemetryByCommand.delete(command);
7471
+ }
7472
+
7184
7473
  /**
7185
7474
  * Creates a PromptTemplate from a template string or returns a fallback template.
7186
7475
  *
@@ -7370,7 +7659,7 @@ function extractLlmInfo(llm) {
7370
7659
  * @throws LangChainExecutionError if the chain execution fails or returns empty results
7371
7660
  * @throws LangChainNetworkError if a network/connection error occurs
7372
7661
  */
7373
- const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, }) => {
7662
+ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint, logger, tokenizer, metadata, }) => {
7374
7663
  validateRequired(llm, 'llm', 'executeChain');
7375
7664
  validateRequired(prompt, 'prompt', 'executeChain');
7376
7665
  validateRequired(variables, 'variables', 'executeChain');
@@ -7388,8 +7677,21 @@ const executeChain = async ({ llm, prompt, variables, parser, provider, endpoint
7388
7677
  const effectiveProvider = provider || llmInfo.provider;
7389
7678
  const effectiveEndpoint = endpoint || llmInfo.endpoint;
7390
7679
  try {
7680
+ const renderedPrompt = await prompt.format(variables);
7681
+ const promptTokens = estimatePromptTokens(tokenizer, renderedPrompt);
7391
7682
  const chain = prompt.pipe(llm).pipe(parser);
7683
+ const startedAt = Date.now();
7392
7684
  const result = (await chain.invoke(variables));
7685
+ const elapsedMs = Date.now() - startedAt;
7686
+ logLlmCall(logger, {
7687
+ task: metadata?.task || 'chain',
7688
+ provider: effectiveProvider,
7689
+ parserType: parser.constructor.name,
7690
+ variableKeys: Object.keys(variables),
7691
+ promptTokens,
7692
+ elapsedMs,
7693
+ ...metadata,
7694
+ });
7393
7695
  if (result === null || result === undefined) {
7394
7696
  throw new LangChainExecutionError('executeChain: Chain execution returned null or undefined result', { variables, promptInputVariables: prompt.inputVariables });
7395
7697
  }
@@ -8218,13 +8520,26 @@ function getPathFromFilePath(filePath) {
8218
8520
  return filePath.split('/').slice(0, -1).join('/');
8219
8521
  }
8220
8522
 
8221
- async function summarize(documents, { chain, textSplitter, options }) {
8523
+ async function summarize(documents, { chain, textSplitter, options, logger, tokenizer, metadata }) {
8222
8524
  const { returnIntermediateSteps = false } = options || {};
8223
8525
  const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
8526
+ const promptTokens = tokenizer
8527
+ ? docs.reduce((sum, doc) => sum + tokenizer(doc.pageContent), 0)
8528
+ : undefined;
8529
+ const startedAt = Date.now();
8224
8530
  const res = await chain.invoke({
8225
8531
  input_documents: docs,
8226
8532
  returnIntermediateSteps,
8227
8533
  });
8534
+ const elapsedMs = Date.now() - startedAt;
8535
+ logLlmCall(logger, {
8536
+ task: 'summarize',
8537
+ promptTokens,
8538
+ elapsedMs,
8539
+ inputDocuments: documents.length,
8540
+ inputChunks: docs.length,
8541
+ ...metadata,
8542
+ });
8228
8543
  if (res.error)
8229
8544
  throw new Error(res.error);
8230
8545
  return res.text && res.text.trim();
@@ -8233,7 +8548,7 @@ async function summarize(documents, { chain, textSplitter, options }) {
8233
8548
  /**
8234
8549
  * Summarize a single file diff that exceeds the token threshold.
8235
8550
  */
8236
- async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
8551
+ async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer, logger, metadata, }) {
8237
8552
  try {
8238
8553
  const fileSummary = await summarize([
8239
8554
  {
@@ -8246,6 +8561,12 @@ async function summarizeFileDiff(fileDiff, { chain, textSplitter, tokenizer }) {
8246
8561
  ], {
8247
8562
  chain,
8248
8563
  textSplitter,
8564
+ tokenizer,
8565
+ logger,
8566
+ metadata: {
8567
+ ...metadata,
8568
+ task: 'summarize-large-file',
8569
+ },
8249
8570
  options: {
8250
8571
  returnIntermediateSteps: false,
8251
8572
  },
@@ -8285,7 +8606,7 @@ async function processInWaves$1(items, processor, maxConcurrent) {
8285
8606
  * @returns Array of file diffs with large files summarized
8286
8607
  */
8287
8608
  async function summarizeLargeFiles(diffs, options) {
8288
- const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter } = options;
8609
+ const { maxFileTokens, minTokensForSummary, maxConcurrent, tokenizer, logger, chain, textSplitter, metadata } = options;
8289
8610
  // Identify files that need summarization
8290
8611
  const filesToSummarize = [];
8291
8612
  const results = [...diffs];
@@ -8299,7 +8620,7 @@ async function summarizeLargeFiles(diffs, options) {
8299
8620
  }
8300
8621
  logger.verbose(`Pre-summarizing ${filesToSummarize.length} large file(s)...`, { color: 'blue' });
8301
8622
  // Process large files in waves
8302
- const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer }), maxConcurrent);
8623
+ const summarizedFiles = await processInWaves$1(filesToSummarize, async ({ diff }) => summarizeFileDiff(diff, { chain, textSplitter, tokenizer, logger, metadata }), maxConcurrent);
8303
8624
  // Update results with summarized files
8304
8625
  summarizedFiles.forEach((summarizedDiff, i) => {
8305
8626
  const originalIndex = filesToSummarize[i].index;
@@ -8362,7 +8683,7 @@ function createDirectoryDiffs(node) {
8362
8683
  /**
8363
8684
  * Summarize a directory diff asynchronously.
8364
8685
  */
8365
- async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
8686
+ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer, logger, metadata }) {
8366
8687
  try {
8367
8688
  const directorySummary = await summarize(directory.diffs.map((diff) => ({
8368
8689
  pageContent: diff.diff,
@@ -8373,6 +8694,12 @@ async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenize
8373
8694
  })), {
8374
8695
  chain,
8375
8696
  textSplitter,
8697
+ tokenizer,
8698
+ logger,
8699
+ metadata: {
8700
+ ...metadata,
8701
+ task: 'summarize-directory-diff',
8702
+ },
8376
8703
  options: {
8377
8704
  returnIntermediateSteps: true,
8378
8705
  },
@@ -8416,7 +8743,7 @@ const defaultOutputCallback = (group) => {
8416
8743
  * while maintaining predictable behavior.
8417
8744
  */
8418
8745
  async function summarizeInWaves(directories, options) {
8419
- const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, } = options;
8746
+ const { totalTokenCount: initialTotal, maxTokens, minTokensForSummary, maxConcurrent, logger, chain, textSplitter, tokenizer, metadata, } = options;
8420
8747
  let totalTokenCount = initialTotal;
8421
8748
  const results = [...directories];
8422
8749
  // Create sorted indices by token count (descending) for prioritized processing
@@ -8448,7 +8775,7 @@ async function summarizeInWaves(directories, options) {
8448
8775
  }
8449
8776
  logger.verbose(`\nProcessing wave of ${wave.length} directories...`, { color: 'blue' });
8450
8777
  // Process wave in parallel
8451
- const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer })));
8778
+ const waveResults = await Promise.all(wave.map((idx) => summarizeDirectoryDiff(results[idx], { chain, textSplitter, tokenizer, logger, metadata })));
8452
8779
  // Update results and recalculate total
8453
8780
  waveResults.forEach((result, i) => {
8454
8781
  const idx = wave[i];
@@ -8485,7 +8812,7 @@ async function summarizeInWaves(directories, options) {
8485
8812
  * - Efficient parallel processing with predictable behavior
8486
8813
  * - Early exit when under token budget
8487
8814
  */
8488
- async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
8815
+ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, minTokensForSummary = 400, maxFileTokens, maxConcurrent = 6, textSplitter, chain, metadata, handleOutput = defaultOutputCallback, }) {
8489
8816
  // Calculate maxFileTokens as 25% of maxTokens if not specified
8490
8817
  const effectiveMaxFileTokens = maxFileTokens ?? Math.floor(maxTokens * 0.25);
8491
8818
  // PHASE 1: Directory grouping & assessment
@@ -8513,6 +8840,7 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
8513
8840
  logger,
8514
8841
  chain,
8515
8842
  textSplitter,
8843
+ metadata,
8516
8844
  });
8517
8845
  logger.stopSpinner('Files pre-processed').stopTimer();
8518
8846
  directoryDiffs = createDirectoryDiffs(preprocessedNode);
@@ -8536,6 +8864,7 @@ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 204
8536
8864
  chain,
8537
8865
  textSplitter,
8538
8866
  tokenizer,
8867
+ metadata,
8539
8868
  });
8540
8869
  logger.stopSpinner(`Diffs Consolidated`).stopTimer();
8541
8870
  return summarizedDiffs.map(handleOutput).join('');
@@ -10477,7 +10806,7 @@ function isObject(subject) {
10477
10806
  }
10478
10807
 
10479
10808
 
10480
- function toArray(sequence) {
10809
+ function toArray$1(sequence) {
10481
10810
  if (Array.isArray(sequence)) return sequence;
10482
10811
  else if (isNothing(sequence)) return [];
10483
10812
 
@@ -10519,7 +10848,7 @@ function isNegativeZero(number) {
10519
10848
 
10520
10849
  var isNothing_1 = isNothing;
10521
10850
  var isObject_1 = isObject;
10522
- var toArray_1 = toArray;
10851
+ var toArray_1 = toArray$1;
10523
10852
  var repeat_1 = repeat;
10524
10853
  var isNegativeZero_1 = isNegativeZero;
10525
10854
  var extend_1 = extend;
@@ -11490,7 +11819,7 @@ for (var i = 0; i < 256; i++) {
11490
11819
  simpleEscapeMap[i] = simpleEscapeSequence(i);
11491
11820
  }
11492
11821
 
11493
- async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, }, }) {
11822
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger, maxTokens, minTokensForSummary, maxFileTokens, maxConcurrent, metadata, }, }) {
11494
11823
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
11495
11824
  const summarizationChain = loadSummarizationChain(model, {
11496
11825
  type: 'map_reduce',
@@ -11518,6 +11847,7 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
11518
11847
  textSplitter,
11519
11848
  chain: summarizationChain,
11520
11849
  logger,
11850
+ metadata,
11521
11851
  });
11522
11852
  logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
11523
11853
  return summary;
@@ -11590,11 +11920,14 @@ async function processInWaves(items, processor, maxConcurrent = 6) {
11590
11920
  }
11591
11921
  return results;
11592
11922
  }
11593
- const handler$4 = async (argv, logger) => {
11923
+ const handler$5 = async (argv, logger) => {
11594
11924
  const config = loadConfig(argv);
11595
11925
  const git = getRepo();
11596
11926
  const key = getApiKeyForModel(config);
11597
- const { provider, model } = getModelAndProviderFromConfig(config);
11927
+ const { provider } = getModelAndProviderFromConfig(config);
11928
+ const changelogService = resolveDynamicService(config, 'changelog');
11929
+ const summaryService = resolveDynamicService(config, argv.withDiff || argv.onlyDiff ? 'largeDiff' : 'summarize');
11930
+ const model = changelogService.model;
11598
11931
  const exclusiveOptions = [
11599
11932
  argv.branch ? '--branch' : null,
11600
11933
  argv.tag ? '--tag' : null,
@@ -11608,7 +11941,8 @@ const handler$4 = async (argv, logger) => {
11608
11941
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
11609
11942
  process.exit(1);
11610
11943
  }
11611
- const llm = getLlm(provider, model, config);
11944
+ const llm = getLlm(provider, model, { ...config, service: changelogService });
11945
+ const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
11612
11946
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
11613
11947
  const INTERACTIVE = isInteractive(config);
11614
11948
  if (INTERACTIVE) {
@@ -11678,12 +12012,17 @@ const handler$4 = async (argv, logger) => {
11678
12012
  options: {
11679
12013
  tokenizer,
11680
12014
  git,
11681
- llm,
12015
+ llm: summaryLlm,
11682
12016
  logger,
11683
12017
  maxTokens: config.service.tokenLimit,
11684
12018
  minTokensForSummary: config.service.minTokensForSummary,
11685
12019
  maxFileTokens: config.service.maxFileTokens,
11686
12020
  maxConcurrent: config.service.maxConcurrent,
12021
+ metadata: {
12022
+ command: 'changelog',
12023
+ provider,
12024
+ model: String(summaryService.model),
12025
+ },
11687
12026
  },
11688
12027
  })
11689
12028
  : undefined,
@@ -11704,12 +12043,17 @@ const handler$4 = async (argv, logger) => {
11704
12043
  options: {
11705
12044
  tokenizer,
11706
12045
  git,
11707
- llm,
12046
+ llm: summaryLlm,
11708
12047
  logger,
11709
12048
  maxTokens: config.service.tokenLimit,
11710
12049
  minTokensForSummary: config.service.minTokensForSummary,
11711
12050
  maxFileTokens: config.service.maxFileTokens,
11712
12051
  maxConcurrent: config.service.maxConcurrent,
12052
+ metadata: {
12053
+ command: 'changelog',
12054
+ provider,
12055
+ model: String(summaryService.model),
12056
+ },
11713
12057
  },
11714
12058
  });
11715
12059
  return `## Diff for ${data.branch}\n\n${diffSummary}`;
@@ -11776,6 +12120,14 @@ const handler$4 = async (argv, logger) => {
11776
12120
  prompt,
11777
12121
  variables: budgetedPrompt.variables,
11778
12122
  parser,
12123
+ logger,
12124
+ tokenizer,
12125
+ metadata: {
12126
+ task: argv.withDiff ? 'changelog-with-diff' : argv.onlyDiff ? 'changelog-only-diff' : 'changelog',
12127
+ command: 'changelog',
12128
+ provider,
12129
+ model: String(model),
12130
+ },
11779
12131
  });
11780
12132
  const branchName = await getCurrentBranchName({ git });
11781
12133
  const ticketId = extractTicketIdFromBranchName(branchName);
@@ -11799,14 +12151,15 @@ const handler$4 = async (argv, logger) => {
11799
12151
  },
11800
12152
  mode: MODE,
11801
12153
  });
12154
+ logLlmTelemetrySummary(logger, 'changelog');
11802
12155
  };
11803
12156
 
11804
12157
  var changelog = {
11805
- command: command$4,
12158
+ command: command$5,
11806
12159
  desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
11807
- builder: builder$4,
11808
- handler: commandExecutor(handler$4),
11809
- options: options$4,
12160
+ builder: builder$5,
12161
+ handler: commandExecutor(handler$5),
12162
+ options: options$5,
11810
12163
  };
11811
12164
 
11812
12165
  const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:/;
@@ -11823,11 +12176,11 @@ const ConventionalCommitMessageResponseSchema = objectType({
11823
12176
  body: stringType().describe("Body of the commit message")
11824
12177
  // .max(280, "Body must be 280 characters or less"),
11825
12178
  }).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
11826
- const command$3 = 'commit';
12179
+ const command$4 = 'commit';
11827
12180
  /**
11828
12181
  * Command line options via yargs
11829
12182
  */
11830
- const options$3 = {
12183
+ const options$4 = {
11831
12184
  i: {
11832
12185
  alias: 'interactive',
11833
12186
  description: 'Toggle interactive mode',
@@ -11883,9 +12236,24 @@ const options$3 = {
11883
12236
  default: false,
11884
12237
  alias: 'n',
11885
12238
  },
12239
+ split: {
12240
+ description: 'Group staged changes into multiple related commit proposals',
12241
+ type: 'boolean',
12242
+ default: false,
12243
+ },
12244
+ plan: {
12245
+ description: 'Only print a commit split plan without changing git state',
12246
+ type: 'boolean',
12247
+ default: false,
12248
+ },
12249
+ apply: {
12250
+ description: 'Apply a generated file-level or hunk-level commit split plan and create commits',
12251
+ type: 'boolean',
12252
+ default: false,
12253
+ },
11886
12254
  };
11887
- const builder$3 = (yargs) => {
11888
- return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
12255
+ const builder$4 = (yargs) => {
12256
+ return yargs.options(options$4).usage(getCommandUsageHeader(command$4));
11889
12257
  };
11890
12258
 
11891
12259
  /**
@@ -11899,15 +12267,24 @@ const builder$3 = (yargs) => {
11899
12267
  * @returns Parsed result matching the schema type
11900
12268
  */
11901
12269
  async function executeChainWithSchema(schema, llm, prompt, variables, options = {}) {
11902
- const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, ...parserOptions } = options;
12270
+ const { retryOptions = { maxAttempts: 3 }, fallbackParser, onFallback, logger, tokenizer, metadata, ...parserOptions } = options;
11903
12271
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11904
12272
  const parser = createSchemaParser(schema, llm, parserOptions);
12273
+ let attempt = 0;
11905
12274
  const operation = async () => {
12275
+ attempt++;
11906
12276
  const result = await executeChain({
11907
12277
  llm,
11908
12278
  prompt,
11909
12279
  variables,
11910
12280
  parser,
12281
+ logger,
12282
+ tokenizer,
12283
+ metadata: {
12284
+ task: 'schema-chain',
12285
+ ...metadata,
12286
+ retryAttempt: attempt,
12287
+ },
11911
12288
  });
11912
12289
  return result;
11913
12290
  };
@@ -11924,6 +12301,12 @@ async function executeChainWithSchema(schema, llm, prompt, variables, options =
11924
12301
  prompt,
11925
12302
  variables,
11926
12303
  parser: new StringOutputParser(),
12304
+ logger,
12305
+ tokenizer,
12306
+ metadata: {
12307
+ task: 'schema-chain-fallback',
12308
+ ...metadata,
12309
+ },
11927
12310
  });
11928
12311
  const fallbackText = typeof fallbackResult === 'string' ? fallbackResult : String(fallbackResult);
11929
12312
  return fallbackParser(fallbackText);
@@ -12386,17 +12769,345 @@ async function noResult$2({ git, logger }) {
12386
12769
  }
12387
12770
  }
12388
12771
 
12389
- const handler$3 = async (argv, logger) => {
12772
+ const CommitSplitPlanSchema = objectType({
12773
+ groups: arrayType(objectType({
12774
+ title: stringType().min(1),
12775
+ body: stringType().optional(),
12776
+ rationale: stringType().optional(),
12777
+ files: arrayType(stringType()),
12778
+ hunks: arrayType(stringType()),
12779
+ })
12780
+ .refine((group) => group.files.length > 0 || group.hunks.length > 0, {
12781
+ message: 'Each group must include at least one file or hunk',
12782
+ }))
12783
+ .min(1),
12784
+ });
12785
+ const COMMIT_SPLIT_PROMPT = PromptTemplate.fromTemplate(`You are helping split staged git changes into a small sequence of coherent commits.
12786
+
12787
+ Return ONLY valid JSON matching this schema:
12788
+ {{
12789
+ "groups": [
12790
+ {{
12791
+ "title": "conventional commit style title",
12792
+ "body": "commit body",
12793
+ "rationale": "why these files belong together",
12794
+ "files": ["relative/path.ts"],
12795
+ "hunks": ["relative/path.ts::hunk-1"]
12796
+ }}
12797
+ ]
12798
+ }}
12799
+
12800
+ Rules:
12801
+ - Use each staged file exactly once.
12802
+ - If a file has hunk IDs and contains unrelated changes, assign every hunk ID exactly once instead of assigning the whole file.
12803
+ - Do not list the same file in "files" when assigning that file through "hunks".
12804
+ - Only use file paths listed in the staged file inventory.
12805
+ - Only use hunk IDs listed in the staged hunk inventory.
12806
+ - Prefer 2-5 commits unless the changes are truly all one topic.
12807
+ - Keep commit titles concise and understandable.
12808
+ - Do not invent files.
12809
+
12810
+ Staged file inventory:
12811
+ {file_inventory}
12812
+
12813
+ Staged hunk inventory:
12814
+ {hunk_inventory}
12815
+
12816
+ Condensed staged diff:
12817
+ {summary}
12818
+
12819
+ Additional context:
12820
+ {additional_context}`);
12821
+ function isCommitSplitCommand(argv) {
12822
+ return Boolean(argv.split || argv.plan || argv.apply || argv._.includes('split'));
12823
+ }
12824
+ function formatCommitSplitPlan(plan) {
12825
+ return plan.groups
12826
+ .map((group, index) => {
12827
+ const body = group.body ? `\n\n${group.body}` : '';
12828
+ const rationale = group.rationale ? `\n\nRationale: ${group.rationale}` : '';
12829
+ const files = (group.files || []).map((file) => `- ${file}`).join('\n');
12830
+ const hunks = (group.hunks || []).map((hunk) => `- ${hunk}`).join('\n');
12831
+ const sections = [
12832
+ files ? `Files:\n${files}` : undefined,
12833
+ hunks ? `Hunks:\n${hunks}` : undefined,
12834
+ ].filter(Boolean);
12835
+ return `## ${index + 1}. ${group.title}${body}${rationale}\n\n${sections.join('\n\n')}`;
12836
+ })
12837
+ .join('\n\n---\n\n');
12838
+ }
12839
+ function getStagedFileSet(changes) {
12840
+ return new Set(changes.map((change) => change.filePath));
12841
+ }
12842
+ function getGroupFiles(group) {
12843
+ return group.files || [];
12844
+ }
12845
+ function getGroupHunks(group) {
12846
+ return group.hunks || [];
12847
+ }
12848
+ function hunkHeader(hunk) {
12849
+ return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
12850
+ }
12851
+ function hunkPreview(hunk) {
12852
+ return hunk.lines
12853
+ .filter((line) => line.startsWith('+') || line.startsWith('-'))
12854
+ .slice(0, 6)
12855
+ .join('\n');
12856
+ }
12857
+ async function collectHunkInventory(staged, git) {
12858
+ const hunks = [];
12859
+ const byId = new Map();
12860
+ const byFile = new Map();
12861
+ for (const change of staged) {
12862
+ if (change.status !== 'modified') {
12863
+ continue;
12864
+ }
12865
+ const diff = await git.diff(['--staged', '--', change.filePath]);
12866
+ const [patch] = parsePatch(diff);
12867
+ if (!patch || patch.hunks.length === 0) {
12868
+ continue;
12869
+ }
12870
+ patch.hunks.forEach((hunk, index) => {
12871
+ const stagedHunk = {
12872
+ id: `${change.filePath}::hunk-${index + 1}`,
12873
+ filePath: change.filePath,
12874
+ patch,
12875
+ hunk,
12876
+ header: hunkHeader(hunk),
12877
+ preview: hunkPreview(hunk),
12878
+ };
12879
+ hunks.push(stagedHunk);
12880
+ byId.set(stagedHunk.id, stagedHunk);
12881
+ byFile.set(change.filePath, [...(byFile.get(change.filePath) || []), stagedHunk]);
12882
+ });
12883
+ }
12884
+ return { hunks, byId, byFile };
12885
+ }
12886
+ function formatHunkInventory(inventory) {
12887
+ if (inventory.hunks.length === 0) {
12888
+ return 'No hunk-level inventory available. Use file-level groups.';
12889
+ }
12890
+ return inventory.hunks
12891
+ .map((hunk) => {
12892
+ const preview = hunk.preview ? `\n${hunk.preview}` : '';
12893
+ return `- ${hunk.id}: ${hunk.header}${preview}`;
12894
+ })
12895
+ .join('\n');
12896
+ }
12897
+ function validatePlanForStagedFiles(plan, staged, hunkInventory) {
12898
+ const stagedFiles = getStagedFileSet(staged);
12899
+ const seen = new Set();
12900
+ const seenHunks = new Set();
12901
+ const unknown = [];
12902
+ const duplicate = [];
12903
+ const unknownHunks = [];
12904
+ const duplicateHunks = [];
12905
+ plan.groups.forEach((group) => {
12906
+ getGroupFiles(group).forEach((file) => {
12907
+ if (!stagedFiles.has(file)) {
12908
+ unknown.push(file);
12909
+ return;
12910
+ }
12911
+ if (seen.has(file)) {
12912
+ duplicate.push(file);
12913
+ return;
12914
+ }
12915
+ seen.add(file);
12916
+ });
12917
+ getGroupHunks(group).forEach((hunkId) => {
12918
+ const hunk = hunkInventory?.byId.get(hunkId);
12919
+ if (!hunk) {
12920
+ unknownHunks.push(hunkId);
12921
+ return;
12922
+ }
12923
+ if (seenHunks.has(hunkId)) {
12924
+ duplicateHunks.push(hunkId);
12925
+ return;
12926
+ }
12927
+ seenHunks.add(hunkId);
12928
+ });
12929
+ });
12930
+ const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
12931
+ const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
12932
+ const partiallyCoveredFiles = [...hunkCoveredFiles]
12933
+ .filter((file) => Boolean(file))
12934
+ .filter((file) => {
12935
+ const fileHunks = hunkInventory?.byFile.get(file) || [];
12936
+ return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
12937
+ });
12938
+ const missing = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
12939
+ if (unknown.length ||
12940
+ duplicate.length ||
12941
+ unknownHunks.length ||
12942
+ duplicateHunks.length ||
12943
+ mixedFiles.length ||
12944
+ partiallyCoveredFiles.length ||
12945
+ missing.length) {
12946
+ throw new Error([
12947
+ unknown.length ? `unknown files: ${unknown.join(', ')}` : undefined,
12948
+ duplicate.length ? `duplicate files: ${duplicate.join(', ')}` : undefined,
12949
+ unknownHunks.length ? `unknown hunks: ${unknownHunks.join(', ')}` : undefined,
12950
+ duplicateHunks.length ? `duplicate hunks: ${duplicateHunks.join(', ')}` : undefined,
12951
+ mixedFiles.length ? `files assigned both as whole files and hunks: ${mixedFiles.join(', ')}` : undefined,
12952
+ partiallyCoveredFiles.length
12953
+ ? `files with only some hunks assigned: ${partiallyCoveredFiles.join(', ')}`
12954
+ : undefined,
12955
+ missing.length ? `missing files: ${missing.join(', ')}` : undefined,
12956
+ ]
12957
+ .filter(Boolean)
12958
+ .join('; '));
12959
+ }
12960
+ }
12961
+ function assertNoUnstagedOverlap(plan, changes, hunkInventory) {
12962
+ const hunkFiles = new Set(plan.groups.flatMap((group) => getGroupHunks(group)
12963
+ .map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath)
12964
+ .filter((file) => Boolean(file))));
12965
+ const plannedFiles = new Set(plan.groups
12966
+ .flatMap((group) => getGroupFiles(group))
12967
+ .filter((file) => !hunkFiles.has(file)));
12968
+ const overlapping = [...(changes.unstaged || []), ...(changes.untracked || [])]
12969
+ .map((change) => change.filePath)
12970
+ .filter((file) => plannedFiles.has(file));
12971
+ if (overlapping.length > 0) {
12972
+ throw new Error(`Cannot apply split plan because these files also have unstaged or untracked changes: ${overlapping.join(', ')}`);
12973
+ }
12974
+ }
12975
+ function buildPatchForHunks(hunks) {
12976
+ const byFile = new Map();
12977
+ hunks.forEach((hunk) => {
12978
+ byFile.set(hunk.filePath, [...(byFile.get(hunk.filePath) || []), hunk]);
12979
+ });
12980
+ return [...byFile.values()]
12981
+ .map((fileHunks) => {
12982
+ const [firstHunk] = fileHunks;
12983
+ return formatPatch({
12984
+ ...firstHunk.patch,
12985
+ hunks: fileHunks.map((hunk) => hunk.hunk),
12986
+ });
12987
+ })
12988
+ .join('\n');
12989
+ }
12990
+ async function applyPatchToIndex(patch, git) {
12991
+ const cwd = await git.revparse(['--show-toplevel']);
12992
+ await new Promise((resolve, reject) => {
12993
+ const child = spawn('git', ['apply', '--cached', '-'], {
12994
+ cwd,
12995
+ stdio: ['pipe', 'ignore', 'pipe'],
12996
+ });
12997
+ let stderr = '';
12998
+ child.stderr.on('data', (chunk) => {
12999
+ stderr += String(chunk);
13000
+ });
13001
+ child.on('error', reject);
13002
+ child.on('close', (code) => {
13003
+ if (code === 0) {
13004
+ resolve();
13005
+ return;
13006
+ }
13007
+ reject(new Error(`Failed to apply hunk patch to index: ${stderr.trim()}`));
13008
+ });
13009
+ child.stdin.write(patch);
13010
+ child.stdin.end();
13011
+ });
13012
+ }
13013
+ async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger, noVerify, }) {
13014
+ validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
13015
+ assertNoUnstagedOverlap(plan, changes, hunkInventory);
13016
+ await git.raw(['reset']);
13017
+ for (const group of plan.groups) {
13018
+ const groupFiles = getGroupFiles(group);
13019
+ const groupHunks = getGroupHunks(group).map((hunkId) => hunkInventory.byId.get(hunkId));
13020
+ if (groupFiles.length > 0) {
13021
+ await git.add(groupFiles);
13022
+ }
13023
+ if (groupHunks.length > 0) {
13024
+ const patch = buildPatchForHunks(groupHunks.filter((hunk) => Boolean(hunk)));
13025
+ await applyPatchToIndex(patch, git);
13026
+ }
13027
+ await createCommit(`${group.title}\n\n${group.body}`.trim(), git, undefined, { noVerify });
13028
+ logger.verbose(`Created split commit: ${group.title}`, { color: 'green' });
13029
+ }
13030
+ return `Created ${plan.groups.length} split commit(s).`;
13031
+ }
13032
+ async function handleCommitSplit({ argv, config, git, logger, tokenizer, llm, }) {
13033
+ const changes = await getChanges({
13034
+ git,
13035
+ options: {
13036
+ ignoredFiles: config.ignoredFiles || undefined,
13037
+ ignoredExtensions: config.ignoredExtensions || undefined,
13038
+ },
13039
+ });
13040
+ if (changes.staged.length === 0) {
13041
+ return 'No staged changes found.';
13042
+ }
13043
+ const hunkInventory = await collectHunkInventory(changes.staged, git);
13044
+ const summary = await fileChangeParser({
13045
+ changes: changes.staged,
13046
+ commit: '--staged',
13047
+ options: {
13048
+ tokenizer,
13049
+ git,
13050
+ llm,
13051
+ logger,
13052
+ maxTokens: config.service.tokenLimit,
13053
+ minTokensForSummary: config.service.minTokensForSummary,
13054
+ maxFileTokens: config.service.maxFileTokens,
13055
+ maxConcurrent: config.service.maxConcurrent,
13056
+ metadata: {
13057
+ command: 'commit',
13058
+ provider: config.service.provider,
13059
+ model: String(config.service.model),
13060
+ },
13061
+ },
13062
+ });
13063
+ const fileInventory = changes.staged
13064
+ .map((change) => `- ${change.filePath}: ${change.status} - ${change.summary}`)
13065
+ .join('\n');
13066
+ const hunkInventoryText = formatHunkInventory(hunkInventory);
13067
+ const plan = await executeChainWithSchema(CommitSplitPlanSchema, llm, COMMIT_SPLIT_PROMPT, {
13068
+ file_inventory: fileInventory,
13069
+ hunk_inventory: hunkInventoryText,
13070
+ summary,
13071
+ additional_context: argv.additional || '',
13072
+ }, {
13073
+ logger,
13074
+ tokenizer,
13075
+ metadata: {
13076
+ task: 'commit-split-plan',
13077
+ command: 'commit',
13078
+ provider: config.service.provider,
13079
+ model: String(config.service.model),
13080
+ },
13081
+ });
13082
+ validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
13083
+ if (argv.apply) {
13084
+ return await applyCommitSplitPlan({
13085
+ plan,
13086
+ changes,
13087
+ hunkInventory,
13088
+ git,
13089
+ logger,
13090
+ noVerify: argv.noVerify || config.noVerify || false,
13091
+ });
13092
+ }
13093
+ return formatCommitSplitPlan(plan);
13094
+ }
13095
+
13096
+ const handler$4 = async (argv, logger) => {
12390
13097
  const git = getRepo();
12391
13098
  const config = loadConfig(argv);
12392
13099
  const key = getApiKeyForModel(config);
12393
- const { provider, model } = getModelAndProviderFromConfig(config);
13100
+ const { provider } = getModelAndProviderFromConfig(config);
13101
+ const commitService = resolveDynamicService(config, 'commit');
13102
+ const summaryService = resolveDynamicService(config, 'summarize');
13103
+ const model = commitService.model;
12394
13104
  if (config.service.authentication.type !== 'None' && !key) {
12395
13105
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
12396
13106
  process.exit(1);
12397
13107
  }
12398
13108
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
12399
- const llm = getLlm(provider, model, config);
13109
+ const llm = getLlm(provider, model, { ...config, service: commitService });
13110
+ const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
12400
13111
  const INTERACTIVE = argv.interactive || isInteractive(config);
12401
13112
  if (INTERACTIVE) {
12402
13113
  if (!config.hideCocoBanner) {
@@ -12414,6 +13125,22 @@ const handler$3 = async (argv, logger) => {
12414
13125
  logger.verbose(`→ ${provider} (${model})`, {
12415
13126
  color: 'green',
12416
13127
  });
13128
+ if (isCommitSplitCommand(argv)) {
13129
+ const splitResult = await handleCommitSplit({
13130
+ argv,
13131
+ config,
13132
+ git,
13133
+ logger,
13134
+ tokenizer,
13135
+ llm,
13136
+ });
13137
+ await handleResult({
13138
+ result: splitResult,
13139
+ mode: config.mode || 'stdout',
13140
+ });
13141
+ logLlmTelemetrySummary(logger, 'commit');
13142
+ return;
13143
+ }
12417
13144
  const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
12418
13145
  async function factory() {
12419
13146
  if (config.noDiff) {
@@ -12451,12 +13178,17 @@ const handler$3 = async (argv, logger) => {
12451
13178
  options: {
12452
13179
  tokenizer,
12453
13180
  git,
12454
- llm,
13181
+ llm: summaryLlm,
12455
13182
  logger,
12456
13183
  maxTokens: config.service.tokenLimit,
12457
13184
  minTokensForSummary: config.service.minTokensForSummary,
12458
13185
  maxFileTokens: config.service.maxFileTokens,
12459
13186
  maxConcurrent: config.service.maxConcurrent,
13187
+ metadata: {
13188
+ command: 'commit',
13189
+ provider,
13190
+ model: String(summaryService.model),
13191
+ },
12460
13192
  },
12461
13193
  });
12462
13194
  }
@@ -12606,6 +13338,14 @@ IMPORTANT RULES:
12606
13338
  logger.verbose(`Rendered prompt exceeded token budget; trimmed summary to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
12607
13339
  }
12608
13340
  const commitMsg = await executeChainWithSchema(schema, llm, prompt, budgetedPrompt.variables, {
13341
+ logger,
13342
+ tokenizer,
13343
+ metadata: {
13344
+ task: USE_CONVENTIONAL_COMMITS ? 'commit-message-conventional' : 'commit-message',
13345
+ command: 'commit',
13346
+ provider,
13347
+ model: String(model),
13348
+ },
12609
13349
  retryOptions: {
12610
13350
  maxAttempts,
12611
13351
  onRetry: (attempt, error) => {
@@ -12814,29 +13554,30 @@ IMPORTANT RULES:
12814
13554
  },
12815
13555
  mode: MODE,
12816
13556
  });
13557
+ logLlmTelemetrySummary(logger, 'commit');
12817
13558
  };
12818
13559
 
12819
13560
  var commit = {
12820
- command: command$3,
13561
+ command: command$4,
12821
13562
  desc: 'Summarize the staged changes in a commit message.',
12822
- builder: builder$3,
12823
- handler: commandExecutor(handler$3),
12824
- options: options$3,
13563
+ builder: builder$4,
13564
+ handler: commandExecutor(handler$4),
13565
+ options: options$4,
12825
13566
  };
12826
13567
 
12827
- const command$2 = 'init';
13568
+ const command$3 = 'init';
12828
13569
  /**
12829
13570
  * Command line options via yargs
12830
13571
  */
12831
- const options$2 = {
13572
+ const options$3 = {
12832
13573
  scope: {
12833
13574
  type: 'string',
12834
13575
  description: 'configure coco for the current user or project?',
12835
13576
  choices: ['global', 'project'],
12836
13577
  },
12837
13578
  };
12838
- const builder$2 = (yargs) => {
12839
- return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
13579
+ const builder$3 = (yargs) => {
13580
+ return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
12840
13581
  };
12841
13582
 
12842
13583
  /**
@@ -13187,7 +13928,7 @@ const questions = {
13187
13928
  }),
13188
13929
  };
13189
13930
 
13190
- const handler$2 = async (argv, logger) => {
13931
+ const handler$3 = async (argv, logger) => {
13191
13932
  const options = loadConfig(argv);
13192
13933
  logger.log(LOGO);
13193
13934
  let scope = options?.scope;
@@ -13358,8 +14099,287 @@ async function installCommitlintPackages(scope, logger) {
13358
14099
  }
13359
14100
 
13360
14101
  var init = {
13361
- command: command$2,
14102
+ command: command$3,
13362
14103
  desc: 'install & configure coco globally or for the current project',
14104
+ builder: builder$3,
14105
+ handler: commandExecutor(handler$3),
14106
+ options: options$3,
14107
+ };
14108
+
14109
+ const command$2 = 'log';
14110
+ const options$2 = {
14111
+ all: {
14112
+ description: 'Show commits from all local and remote refs',
14113
+ type: 'boolean',
14114
+ default: false,
14115
+ },
14116
+ author: {
14117
+ description: 'Filter commits by author',
14118
+ type: 'string',
14119
+ },
14120
+ branch: {
14121
+ description: 'Show commits reachable from a branch or ref',
14122
+ type: 'string',
14123
+ alias: 'b',
14124
+ },
14125
+ commit: {
14126
+ description: 'Show details and changed files for a single commit',
14127
+ type: 'string',
14128
+ alias: 'c',
14129
+ },
14130
+ format: {
14131
+ description: 'Output format',
14132
+ choices: ['table', 'json'],
14133
+ default: 'table',
14134
+ },
14135
+ limit: {
14136
+ description: 'Maximum number of commits to show',
14137
+ type: 'number',
14138
+ default: 30,
14139
+ alias: 'n',
14140
+ },
14141
+ noMerges: {
14142
+ description: 'Exclude merge commits',
14143
+ type: 'boolean',
14144
+ default: false,
14145
+ },
14146
+ path: {
14147
+ description: 'Filter commits by changed path',
14148
+ type: 'array',
14149
+ },
14150
+ since: {
14151
+ description: 'Show commits more recent than a date',
14152
+ type: 'string',
14153
+ },
14154
+ until: {
14155
+ description: 'Show commits older than a date',
14156
+ type: 'string',
14157
+ },
14158
+ };
14159
+ const builder$2 = (yargs) => {
14160
+ return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
14161
+ };
14162
+
14163
+ const FIELD_SEPARATOR = '\x1f';
14164
+ const LOG_FORMAT = `%x1f%h%x1f%H%x1f%ad%x1f%an%x1f%d%x1f%s`;
14165
+ const DETAIL_FORMAT = `%H%x1f%h%x1f%ad%x1f%an%x1f%d%x1f%s%x1f%b`;
14166
+ function toArray(value) {
14167
+ if (!value) {
14168
+ return [];
14169
+ }
14170
+ return Array.isArray(value) ? value : [value];
14171
+ }
14172
+ function normalizeLimit(limit) {
14173
+ if (!limit || Number.isNaN(limit) || limit < 1) {
14174
+ return 30;
14175
+ }
14176
+ return Math.floor(limit);
14177
+ }
14178
+ function cleanRefs(refs) {
14179
+ const trimmed = refs.trim();
14180
+ if (!trimmed) {
14181
+ return [];
14182
+ }
14183
+ return trimmed
14184
+ .replace(/^\(/, '')
14185
+ .replace(/\)$/, '')
14186
+ .split(',')
14187
+ .map((ref) => ref.trim())
14188
+ .filter(Boolean);
14189
+ }
14190
+ function parseLogOutput(output) {
14191
+ return output
14192
+ .split('\n')
14193
+ .map((line) => line.trimEnd())
14194
+ .filter((line) => line.includes(FIELD_SEPARATOR))
14195
+ .map((line) => {
14196
+ const [graph, shortHash, hash, date, author, refs, message] = line.split(FIELD_SEPARATOR);
14197
+ return {
14198
+ graph: graph.trimEnd(),
14199
+ shortHash,
14200
+ hash,
14201
+ date,
14202
+ author,
14203
+ refs: cleanRefs(refs),
14204
+ message,
14205
+ };
14206
+ });
14207
+ }
14208
+ function parseNameStatus(output) {
14209
+ return output
14210
+ .split('\n')
14211
+ .map((line) => line.trim())
14212
+ .filter(Boolean)
14213
+ .map((line) => {
14214
+ const [status, firstPath, secondPath] = line.split('\t');
14215
+ if (status.startsWith('R') || status.startsWith('C')) {
14216
+ return {
14217
+ status,
14218
+ oldPath: firstPath,
14219
+ path: secondPath,
14220
+ };
14221
+ }
14222
+ return {
14223
+ status,
14224
+ path: firstPath,
14225
+ };
14226
+ });
14227
+ }
14228
+ function parseCommitDetail(metadata, files) {
14229
+ const [hash, shortHash, date, author, refs, message, body = ''] = metadata
14230
+ .trimEnd()
14231
+ .split(FIELD_SEPARATOR);
14232
+ return {
14233
+ shortHash,
14234
+ hash,
14235
+ date,
14236
+ author,
14237
+ refs: cleanRefs(refs),
14238
+ message,
14239
+ body: body.trim(),
14240
+ files: parseNameStatus(files),
14241
+ };
14242
+ }
14243
+ function truncate(value, width) {
14244
+ if (value.length <= width) {
14245
+ return value;
14246
+ }
14247
+ return `${value.slice(0, Math.max(0, width - 1))}.`;
14248
+ }
14249
+ function pad(value, width) {
14250
+ return truncate(value, width).padEnd(width, ' ');
14251
+ }
14252
+ function formatLogTable(entries) {
14253
+ if (entries.length === 0) {
14254
+ return 'No commits found.';
14255
+ }
14256
+ const rows = entries.map((entry) => {
14257
+ const refs = entry.refs.join(', ');
14258
+ return [
14259
+ pad(entry.graph || '*', 8),
14260
+ pad(entry.shortHash, 9),
14261
+ pad(entry.date, 10),
14262
+ pad(entry.author, 18),
14263
+ pad(refs, 26),
14264
+ entry.message,
14265
+ ].join(' ');
14266
+ });
14267
+ return [
14268
+ [
14269
+ pad('Graph', 8),
14270
+ pad('Commit', 9),
14271
+ pad('Date', 10),
14272
+ pad('Author', 18),
14273
+ pad('Refs', 26),
14274
+ 'Message',
14275
+ ].join(' '),
14276
+ ...rows,
14277
+ ].join('\n');
14278
+ }
14279
+ function formatCommitDetail(detail, format) {
14280
+ if (format === 'json') {
14281
+ return JSON.stringify(detail, null, 2);
14282
+ }
14283
+ const refs = detail.refs.length ? ` (${detail.refs.join(', ')})` : '';
14284
+ const body = detail.body ? `\n\n${detail.body}` : '';
14285
+ const files = detail.files.length
14286
+ ? detail.files
14287
+ .map((file) => {
14288
+ if (file.oldPath) {
14289
+ return ` ${file.status} ${file.oldPath} -> ${file.path}`;
14290
+ }
14291
+ return ` ${file.status} ${file.path}`;
14292
+ })
14293
+ .join('\n')
14294
+ : ' No changed files found.';
14295
+ return [
14296
+ `commit ${detail.hash}${refs}`,
14297
+ `Author: ${detail.author}`,
14298
+ `Date: ${detail.date}`,
14299
+ '',
14300
+ ` ${detail.message}${body}`,
14301
+ '',
14302
+ 'Changed files:',
14303
+ files,
14304
+ ].join('\n');
14305
+ }
14306
+ function buildLogArgs(argv) {
14307
+ const args = [
14308
+ 'log',
14309
+ '--graph',
14310
+ '--decorate=short',
14311
+ '--date=short',
14312
+ '--color=never',
14313
+ `--max-count=${normalizeLimit(argv.limit)}`,
14314
+ `--pretty=format:${LOG_FORMAT}`,
14315
+ ];
14316
+ if (argv.noMerges) {
14317
+ args.push('--no-merges');
14318
+ }
14319
+ if (argv.author) {
14320
+ args.push(`--author=${argv.author}`);
14321
+ }
14322
+ if (argv.since) {
14323
+ args.push(`--since=${argv.since}`);
14324
+ }
14325
+ if (argv.until) {
14326
+ args.push(`--until=${argv.until}`);
14327
+ }
14328
+ if (argv.all) {
14329
+ args.push('--all');
14330
+ }
14331
+ else if (argv.branch) {
14332
+ args.push(argv.branch);
14333
+ }
14334
+ const paths = toArray(argv.path);
14335
+ if (paths.length > 0) {
14336
+ args.push('--', ...paths);
14337
+ }
14338
+ return args;
14339
+ }
14340
+ async function getCommitDetail(git, commit) {
14341
+ const metadata = await git.raw([
14342
+ 'show',
14343
+ '--no-patch',
14344
+ '--date=short',
14345
+ '--color=never',
14346
+ `--pretty=format:${DETAIL_FORMAT}`,
14347
+ commit,
14348
+ ]);
14349
+ const files = await git.raw([
14350
+ 'show',
14351
+ '--name-status',
14352
+ '--format=',
14353
+ '--find-renames',
14354
+ '--color=never',
14355
+ commit,
14356
+ ]);
14357
+ return parseCommitDetail(metadata, files);
14358
+ }
14359
+ const handler$2 = async (argv) => {
14360
+ const git = getRepo();
14361
+ const mode = argv.interactive ? 'interactive' : 'stdout';
14362
+ const format = argv.format === 'json' ? 'json' : 'table';
14363
+ if (argv.commit) {
14364
+ const detail = await getCommitDetail(git, argv.commit);
14365
+ await handleResult({
14366
+ result: formatCommitDetail(detail, format),
14367
+ mode,
14368
+ });
14369
+ return;
14370
+ }
14371
+ const output = await git.raw(buildLogArgs(argv));
14372
+ const entries = parseLogOutput(output);
14373
+ const result = format === 'json' ? JSON.stringify(entries, null, 2) : formatLogTable(entries);
14374
+ await handleResult({
14375
+ result,
14376
+ mode,
14377
+ });
14378
+ };
14379
+
14380
+ var log = {
14381
+ command: command$2,
14382
+ desc: 'Explore commit history with a branch graph, filters, and commit details.',
13363
14383
  builder: builder$2,
13364
14384
  handler: commandExecutor(handler$2),
13365
14385
  options: options$2,
@@ -13437,13 +14457,17 @@ const handler$1 = async (argv, logger) => {
13437
14457
  const git = getRepo();
13438
14458
  const config = loadConfig(argv);
13439
14459
  const key = getApiKeyForModel(config);
13440
- const { provider, model } = getModelAndProviderFromConfig(config);
14460
+ const { provider } = getModelAndProviderFromConfig(config);
14461
+ const recapService = resolveDynamicService(config, 'recap');
14462
+ const summaryService = resolveDynamicService(config, 'summarize');
14463
+ const model = recapService.model;
13441
14464
  if (config.service.authentication.type !== 'None' && !key) {
13442
14465
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
13443
14466
  process.exit(1);
13444
14467
  }
13445
14468
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
13446
- const llm = getLlm(provider, model, config);
14469
+ const llm = getLlm(provider, model, { ...config, service: recapService });
14470
+ const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
13447
14471
  const INTERACTIVE = argv.interactive || isInteractive(config);
13448
14472
  if (INTERACTIVE) {
13449
14473
  if (!config.hideCocoBanner) {
@@ -13474,19 +14498,49 @@ const handler$1 = async (argv, logger) => {
13474
14498
  const unstagedChanges = await fileChangeParser({
13475
14499
  changes: unstaged || [],
13476
14500
  commit: '--unstaged',
13477
- options: { tokenizer, git, llm, logger },
14501
+ options: {
14502
+ tokenizer,
14503
+ git,
14504
+ llm: summaryLlm,
14505
+ logger,
14506
+ metadata: {
14507
+ command: 'recap',
14508
+ provider,
14509
+ model: String(summaryService.model),
14510
+ },
14511
+ },
13478
14512
  });
13479
14513
  const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
13480
14514
  const untrackedChanges = await fileChangeParser({
13481
14515
  changes: untracked || [],
13482
14516
  commit: '--untracked',
13483
- options: { tokenizer, git, llm, logger },
14517
+ options: {
14518
+ tokenizer,
14519
+ git,
14520
+ llm: summaryLlm,
14521
+ logger,
14522
+ metadata: {
14523
+ command: 'recap',
14524
+ provider,
14525
+ model: String(summaryService.model),
14526
+ },
14527
+ },
13484
14528
  });
13485
14529
  const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
13486
14530
  const stagedChanges = await fileChangeParser({
13487
14531
  changes: staged,
13488
14532
  commit: '--staged',
13489
- options: { tokenizer, git, llm, logger },
14533
+ options: {
14534
+ tokenizer,
14535
+ git,
14536
+ llm: summaryLlm,
14537
+ logger,
14538
+ metadata: {
14539
+ command: 'recap',
14540
+ provider,
14541
+ model: String(summaryService.model),
14542
+ },
14543
+ },
13490
14544
  });
13491
14545
  const stagedResponse = `Staged changes:\n${stagedChanges}`;
13492
14546
  return [unstagedResponse, untrackedResponse, stagedResponse];
@@ -13521,7 +14575,17 @@ const handler$1 = async (argv, logger) => {
13521
14575
  const branchChanges = await fileChangeParser({
13522
14576
  changes: changes.staged,
13523
14577
  commit: baseBranch,
13524
- options: { tokenizer, git, llm, logger },
14578
+ options: {
14579
+ tokenizer,
14580
+ git,
14581
+ llm: summaryLlm,
14582
+ logger,
14583
+ metadata: {
14584
+ command: 'recap',
14585
+ provider,
14586
+ model: String(summaryService.model),
14587
+ },
14588
+ },
13525
14589
  });
13526
14590
  return [branchChanges];
13527
14591
  default:
@@ -13566,15 +14630,34 @@ const handler$1 = async (argv, logger) => {
13566
14630
  try {
13567
14631
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13568
14632
  const parser = createSchemaParser(RecapLlmResponseSchema, llm);
14633
+ const variables = {
14634
+ changes: context,
14635
+ format_instructions: formatInstructions,
14636
+ timeframe,
14637
+ };
14638
+ const budgetedPrompt = await enforcePromptBudget({
14639
+ prompt,
14640
+ variables,
14641
+ tokenizer,
14642
+ maxTokens: config.service.tokenLimit || 2048,
14643
+ summaryKey: 'changes',
14644
+ });
14645
+ if (budgetedPrompt.truncated) {
14646
+ logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
14647
+ }
13569
14648
  const response = await executeChain({
13570
14649
  llm,
13571
14650
  prompt,
13572
- variables: {
13573
- changes: context,
13574
- format_instructions: formatInstructions,
13575
- timeframe,
13576
- },
14651
+ variables: budgetedPrompt.variables,
13577
14652
  parser,
14653
+ logger,
14654
+ tokenizer,
14655
+ metadata: {
14656
+ task: 'recap',
14657
+ command: 'recap',
14658
+ provider,
14659
+ model: String(model),
14660
+ },
13578
14661
  });
13579
14662
  return response ? `${response.title}\n\n${response.summary}` : 'no response';
13580
14663
  }
@@ -13611,6 +14694,7 @@ ${errorMessage}
13611
14694
  },
13612
14695
  mode: MODE,
13613
14696
  });
14697
+ logLlmTelemetrySummary(logger, 'recap');
13614
14698
  };
13615
14699
 
13616
14700
  var recap = {
@@ -13966,13 +15050,17 @@ const handler = async (argv, logger) => {
13966
15050
  const git = getRepo();
13967
15051
  const config = loadConfig(argv);
13968
15052
  const key = getApiKeyForModel(config);
13969
- const { provider, model } = getModelAndProviderFromConfig(config);
15053
+ const { provider } = getModelAndProviderFromConfig(config);
15054
+ const reviewService = resolveDynamicService(config, 'review');
15055
+ const summaryService = resolveDynamicService(config, argv.branch ? 'largeDiff' : 'summarize');
15056
+ const model = reviewService.model;
13970
15057
  if (config.service.authentication.type !== 'None' && !key) {
13971
15058
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
13972
15059
  process.exit(1);
13973
15060
  }
13974
15061
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
13975
- const llm = getLlm(provider, model, config);
15062
+ const llm = getLlm(provider, model, { ...config, service: reviewService });
15063
+ const summaryLlm = getLlm(provider, summaryService.model, { ...config, service: summaryService });
13976
15064
  const INTERACTIVE = isInteractive(config);
13977
15065
  if (INTERACTIVE) {
13978
15066
  if (!config.hideCocoBanner) {
@@ -13996,7 +15084,17 @@ const handler = async (argv, logger) => {
13996
15084
  const branchChanges = await fileChangeParser({
13997
15085
  changes: diff.staged,
13998
15086
  commit: `--branch-diff-${argv.branch}`,
13999
- options: { tokenizer, git, llm, logger },
15087
+ options: {
15088
+ tokenizer,
15089
+ git,
15090
+ llm: summaryLlm,
15091
+ logger,
15092
+ metadata: {
15093
+ command: 'review',
15094
+ provider,
15095
+ model: String(summaryService.model),
15096
+ },
15097
+ },
14000
15098
  });
14001
15099
  return [branchChanges];
14002
15100
  }
@@ -14018,19 +15116,49 @@ const handler = async (argv, logger) => {
14018
15116
  const unstagedChanges = await fileChangeParser({
14019
15117
  changes: unstaged || [],
14020
15118
  commit: '--unstaged',
14021
- options: { tokenizer, git, llm, logger },
15119
+ options: {
15120
+ tokenizer,
15121
+ git,
15122
+ llm: summaryLlm,
15123
+ logger,
15124
+ metadata: {
15125
+ command: 'review',
15126
+ provider,
15127
+ model: String(summaryService.model),
15128
+ },
15129
+ },
14022
15130
  });
14023
15131
  const unstagedResponse = `Unstaged changes:\n${unstagedChanges}`;
14024
15132
  const untrackedChanges = await fileChangeParser({
14025
15133
  changes: untracked || [],
14026
15134
  commit: '--untracked',
14027
- options: { tokenizer, git, llm, logger },
15135
+ options: {
15136
+ tokenizer,
15137
+ git,
15138
+ llm: summaryLlm,
15139
+ logger,
15140
+ metadata: {
15141
+ command: 'review',
15142
+ provider,
15143
+ model: String(summaryService.model),
15144
+ },
15145
+ },
14028
15146
  });
14029
15147
  const untrackedResponse = `Untracked changes:\n${untrackedChanges}`;
14030
15148
  const stagedChanges = await fileChangeParser({
14031
15149
  changes: staged,
14032
15150
  commit: '--staged',
14033
- options: { tokenizer, git, llm, logger },
15151
+ options: {
15152
+ tokenizer,
15153
+ git,
15154
+ llm: summaryLlm,
15155
+ logger,
15156
+ metadata: {
15157
+ command: 'review',
15158
+ provider,
15159
+ model: String(summaryService.model),
15160
+ },
15161
+ },
14034
15162
  });
14035
15163
  const stagedResponse = `Staged changes:\n${stagedChanges}`;
14036
15164
  return [unstagedResponse, untrackedResponse, stagedResponse];
@@ -14070,14 +15198,33 @@ const handler = async (argv, logger) => {
14070
15198
  variables: REVIEW_PROMPT.inputVariables,
14071
15199
  fallback: REVIEW_PROMPT,
14072
15200
  });
15201
+ const variables = {
15202
+ changes: context,
15203
+ format_instructions: formatInstructions,
15204
+ };
15205
+ const budgetedPrompt = await enforcePromptBudget({
15206
+ prompt,
15207
+ variables,
15208
+ tokenizer,
15209
+ maxTokens: config.service.tokenLimit || 2048,
15210
+ summaryKey: 'changes',
15211
+ });
15212
+ if (budgetedPrompt.truncated) {
15213
+ logger.verbose(`Rendered prompt exceeded token budget; trimmed changes to ${budgetedPrompt.promptTokenCount} tokens.`, { color: 'yellow' });
15214
+ }
14073
15215
  const response = await executeChain({
14074
15216
  llm,
14075
15217
  prompt,
14076
- variables: {
14077
- changes: context,
14078
- format_instructions: formatInstructions,
14079
- },
15218
+ variables: budgetedPrompt.variables,
14080
15219
  parser,
15220
+ logger,
15221
+ tokenizer,
15222
+ metadata: {
15223
+ task: argv.branch ? 'review-branch' : 'review',
15224
+ command: 'review',
15225
+ provider,
15226
+ model: String(model),
15227
+ },
14081
15228
  });
14082
15229
  // sort by severity
14083
15230
  return response.sort((a, b) => b.severity - a.severity);
@@ -14096,6 +15243,7 @@ const handler = async (argv, logger) => {
14096
15243
  },
14097
15244
  });
14098
15245
  const reviewer = new TaskList(recap, { ...config, apiKey: key ?? undefined });
15246
+ logLlmTelemetrySummary(logger, 'review');
14099
15247
  await reviewer.start();
14100
15248
  };
14101
15249
 
@@ -14116,6 +15264,7 @@ y.command(changelog.command, changelog.desc, changelog.builder, changelog.handle
14116
15264
  y.command(recap.command, recap.desc, recap.builder, recap.handler);
14117
15265
  y.command(review.command, review.desc, review.builder, review.handler);
14118
15266
  y.command(init.command, init.desc, init.builder, init.handler);
15267
+ y.command(log.command, log.desc, log.builder, log.handler);
14119
15268
  y.help().parse(process.argv.slice(2));
14120
15269
 
14121
15270
  /**
@@ -14567,4 +15716,4 @@ var commitValidationHandler = /*#__PURE__*/Object.freeze({
14567
15716
  handleValidationErrors: handleValidationErrors
14568
15717
  });
14569
15718
 
14570
- export { changelog, commit, init, recap, types };
15719
+ export { changelog, commit, init, log, recap, types };