@mcarvin/smart-diff 1.1.0 → 2.1.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.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { generateText } from 'ai';
1
2
  import { resolve, relative } from 'node:path';
2
3
  import { simpleGit } from 'simple-git';
3
4
 
@@ -33,9 +34,55 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
33
34
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
34
35
  };
35
36
 
37
+ const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
38
+ const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
39
+ You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
40
+ Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
41
+ Produce a concise, developer-focused summary in Markdown.
42
+ Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
43
+ Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
44
+ If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
45
+ const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM provider configured. Set LLM_PROVIDER (openai | openai-compatible | anthropic | google | bedrock | mistral | cohere | groq | xai | deepseek), " +
46
+ "or a provider API key (OPENAI_API_KEY, LLM_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, MISTRAL_API_KEY, COHERE_API_KEY, GROQ_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY), " +
47
+ "or LLM_BASE_URL / OPENAI_BASE_URL for an OpenAI-compatible gateway, " +
48
+ "or JSON in OPENAI_DEFAULT_HEADERS / LLM_DEFAULT_HEADERS. " +
49
+ "Alternatively pass llmModelProvider or openAiClientProvider to generateSummary or summarizeGitDiff.";
50
+
51
+ const DEFAULT_MODEL_BY_PROVIDER = {
52
+ openai: "gpt-4o-mini",
53
+ "openai-compatible": "gpt-4o-mini",
54
+ anthropic: "claude-3-5-haiku-latest",
55
+ google: "gemini-2.0-flash",
56
+ bedrock: "anthropic.claude-3-5-haiku-20241022-v1:0",
57
+ mistral: "mistral-small-latest",
58
+ cohere: "command-r-08-2024",
59
+ groq: "llama-3.1-8b-instant",
60
+ xai: "grok-2-latest",
61
+ deepseek: "deepseek-chat",
62
+ };
63
+ const VALID_PROVIDERS = new Set([
64
+ "openai",
65
+ "openai-compatible",
66
+ "anthropic",
67
+ "google",
68
+ "bedrock",
69
+ "mistral",
70
+ "cohere",
71
+ "groq",
72
+ "xai",
73
+ "deepseek",
74
+ ]);
75
+ function readEnv(name) {
76
+ var _a;
77
+ const value = (_a = process.env[name]) === null || _a === void 0 ? void 0 : _a.trim();
78
+ return value && value.length > 0 ? value : undefined;
79
+ }
80
+ function isValidProviderId(value) {
81
+ return VALID_PROVIDERS.has(value);
82
+ }
36
83
  function resolveLlmBaseUrl() {
37
- var _a, _b, _c;
38
- return ((_b = (_a = process.env.LLM_BASE_URL) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_BASE_URL) === null || _c === void 0 ? void 0 : _c.trim());
84
+ var _a;
85
+ return (_a = readEnv("LLM_BASE_URL")) !== null && _a !== void 0 ? _a : readEnv("OPENAI_BASE_URL");
39
86
  }
40
87
  function parseHeaderJsonObject(raw) {
41
88
  const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
@@ -66,87 +113,187 @@ function parseLlmDefaultHeadersFromEnv() {
66
113
  const merged = Object.assign(Object.assign({}, base), override);
67
114
  return Object.keys(merged).length > 0 ? merged : undefined;
68
115
  }
69
- function findAuthorizationHeaderName(headers) {
70
- return Object.keys(headers).find((k) => k.toLowerCase() === "authorization");
71
- }
72
- function stripBearerPrefix(value) {
116
+ function resolveOpenAiApiKey() {
73
117
  var _a;
74
- const trimmed = value.trim();
75
- const match = /^Bearer\s+(\S+)/i.exec(trimmed);
76
- return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : trimmed;
77
- }
78
- function splitPromotableAuthorizationFromHeaders(headers) {
79
- const authName = findAuthorizationHeaderName(headers);
80
- if (!authName) {
81
- return { defaultHeaders: headers };
82
- }
83
- const raw = headers[authName];
84
- if (!raw) {
85
- return { defaultHeaders: headers };
86
- }
87
- const token = stripBearerPrefix(raw);
88
- const looksBearer = /^Bearer\s+\S+/i.test(raw.trim());
89
- const looksOpenAiKey = /^sk-/i.test(token);
90
- if (!looksBearer && !looksOpenAiKey) {
91
- return { defaultHeaders: headers };
92
- }
93
- const next = Object.assign({}, headers);
94
- delete next[authName];
95
- return { defaultHeaders: next, apiKeyFromAuthHeader: token };
96
- }
97
- function shouldUseLlmGateway() {
98
- var _a, _b, _c;
99
- const apiKey = (_b = (_a = process.env.LLM_API_KEY) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_API_KEY) === null || _c === void 0 ? void 0 : _c.trim();
100
- if (apiKey)
101
- return true;
102
- if (resolveLlmBaseUrl())
103
- return true;
104
- const jsonHeaders = parseLlmDefaultHeadersFromEnv();
105
- if (jsonHeaders && Object.keys(jsonHeaders).length > 0)
106
- return true;
107
- return false;
118
+ return (_a = readEnv("LLM_API_KEY")) !== null && _a !== void 0 ? _a : readEnv("OPENAI_API_KEY");
108
119
  }
109
- function resolveOpenAiLikeClientInit() {
110
- var _a, _b, _c, _d, _e;
111
- const baseURL = resolveLlmBaseUrl();
112
- const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
113
- const envApiKey = (_e = (_c = (_b = process.env.LLM_API_KEY) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : (_d = process.env.OPENAI_API_KEY) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : "";
114
- let defaultHeaders;
115
- let apiKey = envApiKey;
116
- if (apiKey.length === 0) {
117
- const split = splitPromotableAuthorizationFromHeaders(mergedHeaders);
118
- if (split.apiKeyFromAuthHeader) {
119
- apiKey = split.apiKeyFromAuthHeader;
120
- }
121
- defaultHeaders =
122
- Object.keys(split.defaultHeaders).length > 0
123
- ? split.defaultHeaders
124
- : undefined;
120
+ function detectLlmProvider() {
121
+ var _a, _b;
122
+ const explicit = (_a = readEnv("LLM_PROVIDER")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
123
+ if (explicit && isValidProviderId(explicit)) {
124
+ return explicit;
125
125
  }
126
- else {
127
- defaultHeaders =
128
- Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
126
+ if (resolveLlmBaseUrl()) {
127
+ return "openai-compatible";
129
128
  }
130
- return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : "unused" }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
129
+ if (resolveOpenAiApiKey()) {
130
+ return "openai";
131
+ }
132
+ if (readEnv("ANTHROPIC_API_KEY"))
133
+ return "anthropic";
134
+ if ((_b = readEnv("GOOGLE_GENERATIVE_AI_API_KEY")) !== null && _b !== void 0 ? _b : readEnv("GOOGLE_API_KEY"))
135
+ return "google";
136
+ if (readEnv("MISTRAL_API_KEY"))
137
+ return "mistral";
138
+ if (readEnv("COHERE_API_KEY"))
139
+ return "cohere";
140
+ if (readEnv("GROQ_API_KEY"))
141
+ return "groq";
142
+ if (readEnv("XAI_API_KEY"))
143
+ return "xai";
144
+ if (readEnv("DEEPSEEK_API_KEY"))
145
+ return "deepseek";
146
+ if (parseLlmDefaultHeadersFromEnv())
147
+ return "openai";
148
+ return undefined;
149
+ }
150
+ function isLlmProviderConfigured() {
151
+ return detectLlmProvider() !== undefined;
131
152
  }
132
- function createOpenAiLikeClient() {
153
+ function defaultModelForProvider(provider) {
154
+ return DEFAULT_MODEL_BY_PROVIDER[provider];
155
+ }
156
+ function createOpenAiModel(modelId) {
133
157
  return __awaiter(this, void 0, void 0, function* () {
134
- const { default: OpenAI } = yield import('openai');
135
- return new OpenAI(resolveOpenAiLikeClientInit());
158
+ const { createOpenAI } = yield import('@ai-sdk/openai');
159
+ const apiKey = resolveOpenAiApiKey();
160
+ const headers = parseLlmDefaultHeadersFromEnv();
161
+ const provider = createOpenAI(Object.assign(Object.assign({}, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
162
+ return provider(modelId);
163
+ });
164
+ }
165
+ function createOpenAiCompatibleModel(modelId) {
166
+ return __awaiter(this, void 0, void 0, function* () {
167
+ var _a;
168
+ const { createOpenAICompatible } = yield import('@ai-sdk/openai-compatible');
169
+ const baseURL = resolveLlmBaseUrl();
170
+ if (!baseURL) {
171
+ throw new Error("openai-compatible provider requires LLM_BASE_URL or OPENAI_BASE_URL to be set.");
172
+ }
173
+ const apiKey = resolveOpenAiApiKey();
174
+ const headers = parseLlmDefaultHeadersFromEnv();
175
+ const provider = createOpenAICompatible(Object.assign(Object.assign({ name: (_a = readEnv("LLM_PROVIDER_NAME")) !== null && _a !== void 0 ? _a : "openai-compatible", baseURL }, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
176
+ return provider(modelId);
177
+ });
178
+ }
179
+ function wrapMissingPeer(failure) {
180
+ const err = new Error(`Failed to load optional provider package "${failure.pkg}" for LLM_PROVIDER="${failure.provider}". ` +
181
+ `Install it with \`npm install ${failure.pkg}\`.`);
182
+ err.cause = failure.cause;
183
+ return err;
184
+ }
185
+ function importOptional(provider, pkg, loader) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ try {
188
+ return yield loader();
189
+ }
190
+ catch (cause) {
191
+ throw wrapMissingPeer({ provider, pkg, cause });
192
+ }
193
+ });
194
+ }
195
+ function createAnthropicModel(modelId) {
196
+ return __awaiter(this, void 0, void 0, function* () {
197
+ const mod = yield importOptional("anthropic", "@ai-sdk/anthropic", () => import('@ai-sdk/anthropic'));
198
+ const apiKey = readEnv("ANTHROPIC_API_KEY");
199
+ const provider = mod.createAnthropic(apiKey ? { apiKey } : undefined);
200
+ return provider(modelId);
201
+ });
202
+ }
203
+ function createGoogleModel(modelId) {
204
+ return __awaiter(this, void 0, void 0, function* () {
205
+ var _a;
206
+ const mod = yield importOptional("google", "@ai-sdk/google", () => import('@ai-sdk/google'));
207
+ const apiKey = (_a = readEnv("GOOGLE_GENERATIVE_AI_API_KEY")) !== null && _a !== void 0 ? _a : readEnv("GOOGLE_API_KEY");
208
+ const provider = mod.createGoogleGenerativeAI(apiKey ? { apiKey } : undefined);
209
+ return provider(modelId);
210
+ });
211
+ }
212
+ function createBedrockModel(modelId) {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const mod = yield importOptional("bedrock", "@ai-sdk/amazon-bedrock", () => import('@ai-sdk/amazon-bedrock'));
215
+ const provider = mod.createAmazonBedrock();
216
+ return provider(modelId);
217
+ });
218
+ }
219
+ function createMistralModel(modelId) {
220
+ return __awaiter(this, void 0, void 0, function* () {
221
+ const mod = yield importOptional("mistral", "@ai-sdk/mistral", () => import('@ai-sdk/mistral'));
222
+ const apiKey = readEnv("MISTRAL_API_KEY");
223
+ const provider = mod.createMistral(apiKey ? { apiKey } : undefined);
224
+ return provider(modelId);
225
+ });
226
+ }
227
+ function createCohereModel(modelId) {
228
+ return __awaiter(this, void 0, void 0, function* () {
229
+ const mod = yield importOptional("cohere", "@ai-sdk/cohere", () => import('@ai-sdk/cohere'));
230
+ const apiKey = readEnv("COHERE_API_KEY");
231
+ const provider = mod.createCohere(apiKey ? { apiKey } : undefined);
232
+ return provider(modelId);
233
+ });
234
+ }
235
+ function createGroqModel(modelId) {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ const mod = yield importOptional("groq", "@ai-sdk/groq", () => import('@ai-sdk/groq'));
238
+ const apiKey = readEnv("GROQ_API_KEY");
239
+ const provider = mod.createGroq(apiKey ? { apiKey } : undefined);
240
+ return provider(modelId);
241
+ });
242
+ }
243
+ function createXaiModel(modelId) {
244
+ return __awaiter(this, void 0, void 0, function* () {
245
+ const mod = yield importOptional("xai", "@ai-sdk/xai", () => import('@ai-sdk/xai'));
246
+ const apiKey = readEnv("XAI_API_KEY");
247
+ const provider = mod.createXai(apiKey ? { apiKey } : undefined);
248
+ return provider(modelId);
249
+ });
250
+ }
251
+ function createDeepseekModel(modelId) {
252
+ return __awaiter(this, void 0, void 0, function* () {
253
+ const mod = yield importOptional("deepseek", "@ai-sdk/deepseek", () => import('@ai-sdk/deepseek'));
254
+ const apiKey = readEnv("DEEPSEEK_API_KEY");
255
+ const provider = mod.createDeepSeek(apiKey ? { apiKey } : undefined);
256
+ return provider(modelId);
257
+ });
258
+ }
259
+ function resolveLanguageModel() {
260
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
261
+ var _a, _b, _c;
262
+ const provider = (_a = options.provider) !== null && _a !== void 0 ? _a : detectLlmProvider();
263
+ if (!provider) {
264
+ throw new Error("No LLM provider could be resolved. Set LLM_PROVIDER or a provider API key " +
265
+ "(OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, MISTRAL_API_KEY, " +
266
+ "COHERE_API_KEY, GROQ_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY), or LLM_BASE_URL for an OpenAI-compatible gateway.");
267
+ }
268
+ const modelId = (_c = (_b = options.model) !== null && _b !== void 0 ? _b : readEnv("LLM_MODEL")) !== null && _c !== void 0 ? _c : defaultModelForProvider(provider);
269
+ switch (provider) {
270
+ case "openai":
271
+ return createOpenAiModel(modelId);
272
+ case "openai-compatible":
273
+ return createOpenAiCompatibleModel(modelId);
274
+ case "anthropic":
275
+ return createAnthropicModel(modelId);
276
+ case "google":
277
+ return createGoogleModel(modelId);
278
+ case "bedrock":
279
+ return createBedrockModel(modelId);
280
+ case "mistral":
281
+ return createMistralModel(modelId);
282
+ case "cohere":
283
+ return createCohereModel(modelId);
284
+ case "groq":
285
+ return createGroqModel(modelId);
286
+ case "xai":
287
+ return createXaiModel(modelId);
288
+ case "deepseek":
289
+ return createDeepseekModel(modelId);
290
+ default: {
291
+ const _exhaustive = provider;
292
+ throw new Error(`Unhandled LLM provider: ${String(_exhaustive)}`);
293
+ }
294
+ }
136
295
  });
137
296
  }
138
-
139
- const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
140
- const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
141
- You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
142
- Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
143
- Produce a concise, developer-focused summary in Markdown.
144
- Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
145
- Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
146
- If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
147
- const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM gateway configured. Set OPENAI_API_KEY or LLM_API_KEY, and/or LLM_BASE_URL or OPENAI_BASE_URL, " +
148
- "and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. " +
149
- "Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.";
150
297
 
151
298
  function resolveLlmMaxDiffChars(cliOverride) {
152
299
  var _a;
@@ -174,18 +321,26 @@ function truncateUnifiedDiffForLlm(diffText, maxChars) {
174
321
  function markdownDiffTruncationNotice(originalChars, maxChars) {
175
322
  return `> **Truncated diff:** The unified diff was ${originalChars} characters; only the first ${maxChars} were sent to the model. The summary may not reflect the full change set. Narrow the ref range, adjust path filters, or raise \`maxDiffChars\` / \`LLM_MAX_DIFF_CHARS\`—often together with switching to a model whose context window can fit a larger prompt.\n\n`;
176
323
  }
324
+ function resolveMaxOutputTokens() {
325
+ var _a;
326
+ const raw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
327
+ const parsed = raw !== undefined ? Number.parseInt(raw, 10) : 4000;
328
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
329
+ }
177
330
  function generateSummary(input) {
178
331
  return __awaiter(this, void 0, void 0, function* () {
179
- var _a, _b;
180
- const { diffText, fileNames, commits, flags, openAiClientProvider, diffSummary, } = input;
181
- if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
332
+ var _a;
333
+ const { diffText, fileNames, commits, flags, llmModelProvider, diffSummary, } = input;
334
+ if (!llmModelProvider && !isLlmProviderConfigured()) {
182
335
  throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
183
336
  }
184
337
  const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
185
338
  const diffTruncated = diffText.length > maxDiffChars;
186
339
  const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
187
- const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
188
- const summary = yield callOpenAi(userContent, (_a = flags.model) !== null && _a !== void 0 ? _a : "gpt-4o-mini", (_b = flags.systemPrompt) !== null && _b !== void 0 ? _b : DEFAULT_GIT_DIFF_SYSTEM_PROMPT, openAiClientProvider !== null && openAiClientProvider !== void 0 ? openAiClientProvider : (() => __awaiter(this, void 0, void 0, function* () { return createOpenAiLikeClient(); })));
340
+ const userContent = buildUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
341
+ const systemPrompt = (_a = flags.systemPrompt) !== null && _a !== void 0 ? _a : DEFAULT_GIT_DIFF_SYSTEM_PROMPT;
342
+ const maxOutputTokens = resolveMaxOutputTokens();
343
+ const summary = yield callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, flags);
189
344
  if (!diffTruncated) {
190
345
  return summary;
191
346
  }
@@ -212,7 +367,7 @@ function formatRegexFilterLines(flags) {
212
367
  return (`${incLine}${excLine}` +
213
368
  "Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n");
214
369
  }
215
- function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
370
+ function buildUserContent(flags, commits, fileNames, diffText, diffSummary) {
216
371
  var _a, _b;
217
372
  const from = flags.from;
218
373
  const to = (_a = flags.to) !== null && _a !== void 0 ? _a : "HEAD";
@@ -242,31 +397,20 @@ function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary
242
397
  "=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n" +
243
398
  diffText);
244
399
  }
245
- function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
400
+ function callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, flags) {
246
401
  return __awaiter(this, void 0, void 0, function* () {
247
- var _a, _b, _c, _d, _e, _f;
248
- const client = yield openAiClientProvider();
249
- const maxTokensRaw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
250
- const parsed = maxTokensRaw !== undefined ? Number.parseInt(maxTokensRaw, 10) : 4000;
251
- const maxTokens = Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
252
- const response = yield client.chat.completions.create({
402
+ const model = llmModelProvider
403
+ ? yield llmModelProvider()
404
+ : yield resolveLanguageModel(Object.assign(Object.assign({}, (flags.provider ? { provider: flags.provider } : {})), (flags.model ? { model: flags.model } : {})));
405
+ const result = yield generateText({
253
406
  model,
254
- messages: [
255
- {
256
- role: "system",
257
- content: systemPrompt,
258
- },
259
- {
260
- role: "user",
261
- content: userContent,
262
- },
263
- ],
407
+ system: systemPrompt,
408
+ prompt: userContent,
264
409
  temperature: 0.2,
265
- max_tokens: maxTokens,
410
+ maxOutputTokens,
266
411
  });
267
- const typedResponse = response;
268
- const text = (_f = (_e = (_d = (_c = (_b = typedResponse.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : "";
269
- return text.length > 0 ? text : "No summary generated by OpenAI.";
412
+ const text = result.text.trim();
413
+ return text.length > 0 ? text : "No summary generated by the model.";
270
414
  });
271
415
  }
272
416
 
@@ -348,6 +492,130 @@ function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns
348
492
  return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
349
493
  }
350
494
 
495
+ const DEFAULT_NOISE_EXCLUDES = [
496
+ "package-lock.json",
497
+ "yarn.lock",
498
+ "pnpm-lock.yaml",
499
+ "npm-shrinkwrap.json",
500
+ "bun.lockb",
501
+ "go.sum",
502
+ "Cargo.lock",
503
+ "Gemfile.lock",
504
+ "composer.lock",
505
+ "Pipfile.lock",
506
+ "poetry.lock",
507
+ "uv.lock",
508
+ "Podfile.lock",
509
+ "node_modules",
510
+ "dist",
511
+ "build",
512
+ "out",
513
+ "coverage",
514
+ "__snapshots__",
515
+ ];
516
+ function normalizeContextLines(raw) {
517
+ if (!Number.isFinite(raw) || raw < 0)
518
+ return 0;
519
+ return Math.trunc(raw);
520
+ }
521
+ function buildDiffShapingGitArgs(shaping) {
522
+ const args = [];
523
+ if ((shaping === null || shaping === void 0 ? void 0 : shaping.contextLines) !== undefined) {
524
+ args.push(`-U${normalizeContextLines(shaping.contextLines)}`);
525
+ }
526
+ if (shaping === null || shaping === void 0 ? void 0 : shaping.ignoreWhitespace) {
527
+ args.push("-w");
528
+ }
529
+ return args;
530
+ }
531
+ const PREAMBLE_NOISE_PREFIXES = [
532
+ "diff --git ",
533
+ "index ",
534
+ "new file mode ",
535
+ "deleted file mode ",
536
+ "old mode ",
537
+ "new mode ",
538
+ "similarity index ",
539
+ "dissimilarity index ",
540
+ "rename from ",
541
+ "rename to ",
542
+ "copy from ",
543
+ "copy to ",
544
+ ];
545
+ function isPreambleNoiseLine(line) {
546
+ for (const prefix of PREAMBLE_NOISE_PREFIXES) {
547
+ if (line.startsWith(prefix))
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ function stripPreambleLines(text) {
553
+ return text
554
+ .split(/\r?\n/)
555
+ .filter((line) => !isPreambleNoiseLine(line))
556
+ .join("\n");
557
+ }
558
+ function isFileHeaderLine(line) {
559
+ return (/^--- (a\/|b\/|"a\/|"b\/|\/dev\/null)/.test(line) ||
560
+ /^\+\+\+ (a\/|b\/|"a\/|"b\/|\/dev\/null)/.test(line));
561
+ }
562
+ function elideLargeHunks(text, maxHunkLines) {
563
+ const limit = normalizeContextLines(maxHunkLines);
564
+ const lines = text.split(/\r?\n/);
565
+ const out = [];
566
+ let inHunk = false;
567
+ let hunkBuf = [];
568
+ const flushHunk = () => {
569
+ if (hunkBuf.length > limit) {
570
+ const elided = hunkBuf.length - limit;
571
+ out.push(...hunkBuf.slice(0, limit));
572
+ out.push(`[... ${elided} diff line${elided === 1 ? "" : "s"} elided ...]`);
573
+ }
574
+ else {
575
+ out.push(...hunkBuf);
576
+ }
577
+ hunkBuf = [];
578
+ inHunk = false;
579
+ };
580
+ for (const line of lines) {
581
+ if (line.startsWith("@@")) {
582
+ if (inHunk)
583
+ flushHunk();
584
+ out.push(line);
585
+ inHunk = true;
586
+ continue;
587
+ }
588
+ if (line.startsWith("diff --git ") || isFileHeaderLine(line)) {
589
+ if (inHunk)
590
+ flushHunk();
591
+ out.push(line);
592
+ continue;
593
+ }
594
+ if (inHunk) {
595
+ hunkBuf.push(line);
596
+ }
597
+ else {
598
+ out.push(line);
599
+ }
600
+ }
601
+ if (inHunk)
602
+ flushHunk();
603
+ return out.join("\n");
604
+ }
605
+ function shapeUnifiedDiff(text, shaping) {
606
+ if (!(shaping === null || shaping === void 0 ? void 0 : shaping.stripDiffPreamble) && (shaping === null || shaping === void 0 ? void 0 : shaping.maxHunkLines) === undefined) {
607
+ return text;
608
+ }
609
+ let out = text;
610
+ if (shaping.stripDiffPreamble) {
611
+ out = stripPreambleLines(out);
612
+ }
613
+ if (shaping.maxHunkLines !== undefined) {
614
+ out = elideLargeHunks(out, shaping.maxHunkLines);
615
+ }
616
+ return out;
617
+ }
618
+
351
619
  const GIT_STATUS_BY_FIRST_CHAR = {
352
620
  A: "added",
353
621
  D: "deleted",
@@ -378,25 +646,17 @@ function mergeStatus(existing, next) {
378
646
  }
379
647
 
380
648
  function parseNameStatusLine(line) {
381
- var _a;
382
649
  const parts = line.split("\t");
383
650
  let entry = null;
384
651
  if (parts.length >= 2) {
385
- const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
652
+ const statusToken = parts[0];
386
653
  const status = mapGitStatus(statusToken);
387
654
  const isRenameOrCopy = statusToken.startsWith("R") || statusToken.startsWith("C");
388
655
  if (isRenameOrCopy && parts.length >= 3) {
389
- const oldPath = parts[1];
390
- const newPath = parts[2];
391
- if (oldPath !== undefined && newPath !== undefined) {
392
- entry = { path: newPath, status, oldPath };
393
- }
656
+ entry = { path: parts[2], status, oldPath: parts[1] };
394
657
  }
395
658
  else if (!isRenameOrCopy) {
396
- const pathOnly = parts[1];
397
- if (pathOnly !== undefined) {
398
- entry = { path: pathOnly, status };
399
- }
659
+ entry = { path: parts[1], status };
400
660
  }
401
661
  }
402
662
  return entry;
@@ -441,12 +701,11 @@ function numStatPathToLookupKey(pathField) {
441
701
  return `${dirRaw}${toSeg}`;
442
702
  }
443
703
  function parseNumStatLine(line) {
444
- var _a, _b;
445
704
  const parts = line.split("\t");
446
705
  if (parts.length < 3)
447
706
  return null;
448
- const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
449
- const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : "";
707
+ const addStr = parts[0];
708
+ const delStr = parts[1];
450
709
  const pathField = parts.slice(2).join("\t");
451
710
  const additions = addStr !== "-" ? Number.parseInt(addStr, 10) || 0 : 0;
452
711
  const deletions = delStr !== "-" ? Number.parseInt(delStr, 10) || 0 : 0;
@@ -471,11 +730,10 @@ function accumulateNumStat(numStatOutput, into) {
471
730
  }
472
731
 
473
732
  function parseTabDiffSummaryLine(line) {
474
- var _a;
475
733
  const parts = line.split("\t");
476
734
  if (parts.length < 3)
477
735
  return null;
478
- const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : "";
736
+ const statusToken = parts.shift();
479
737
  const status = mapGitStatus(statusToken);
480
738
  const add0 = parts[0];
481
739
  const del0 = parts[1];
@@ -593,31 +851,48 @@ function getDiffPathContext(git, pathFilter, repoRootOverride) {
593
851
  }
594
852
  function getDiff(git, query) {
595
853
  return __awaiter(this, void 0, void 0, function* () {
596
- const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
854
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride, shaping, } = query;
597
855
  const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
856
+ const shapingArgs = buildDiffShapingGitArgs(shaping);
598
857
  if (!filterByCommits) {
599
- return git.diff([`${from}..${to}`, "--", ...specs]);
858
+ const raw = yield git.diff([
859
+ ...shapingArgs,
860
+ `${from}..${to}`,
861
+ "--",
862
+ ...specs,
863
+ ]);
864
+ return shapeUnifiedDiff(raw, shaping);
600
865
  }
601
- const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, "--", ...specs])));
602
- return patches.filter(Boolean).join("\n");
866
+ const patches = yield Promise.all(commits.map((c) => git.diff([...shapingArgs, `${c.hash}^!`, "--", ...specs])));
867
+ return patches
868
+ .map((p) => shapeUnifiedDiff(p, shaping))
869
+ .filter(Boolean)
870
+ .join("\n");
603
871
  });
604
872
  }
605
873
  function getDiffSummary(git, query) {
606
874
  return __awaiter(this, void 0, void 0, function* () {
607
- const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
875
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride, shaping, } = query;
608
876
  const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
877
+ const whitespaceArgs = (shaping === null || shaping === void 0 ? void 0 : shaping.ignoreWhitespace) ? ["-w"] : [];
609
878
  if (!filterByCommits) {
610
879
  const [numOutput, nameOutput] = yield Promise.all([
611
- git.diff(["--numstat", `${from}..${to}`, "--", ...specs]),
612
- git.diff(["--name-status", `${from}..${to}`, "--", ...specs]),
880
+ git.diff([...whitespaceArgs, "--numstat", `${from}..${to}`, "--", ...specs]),
881
+ git.diff([
882
+ ...whitespaceArgs,
883
+ "--name-status",
884
+ `${from}..${to}`,
885
+ "--",
886
+ ...specs,
887
+ ]),
613
888
  ]);
614
889
  return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
615
890
  }
616
891
  const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
617
892
  const range = `${c.hash}^!`;
618
893
  const [numOutput, nameOutput] = yield Promise.all([
619
- git.diff(["--numstat", range, "--", ...specs]),
620
- git.diff(["--name-status", range, "--", ...specs]),
894
+ git.diff([...whitespaceArgs, "--numstat", range, "--", ...specs]),
895
+ git.diff([...whitespaceArgs, "--name-status", range, "--", ...specs]),
621
896
  ]);
622
897
  return { numOutput, nameOutput };
623
898
  })));
@@ -667,6 +942,37 @@ function getChangedFiles(git, query) {
667
942
  });
668
943
  }
669
944
 
945
+ function buildShapingFromOptions(options) {
946
+ const shaping = {};
947
+ if (options.contextLines !== undefined) {
948
+ shaping.contextLines = options.contextLines;
949
+ }
950
+ if (options.ignoreWhitespace)
951
+ shaping.ignoreWhitespace = true;
952
+ if (options.stripDiffPreamble)
953
+ shaping.stripDiffPreamble = true;
954
+ if (options.maxHunkLines !== undefined) {
955
+ shaping.maxHunkLines = options.maxHunkLines;
956
+ }
957
+ return Object.keys(shaping).length > 0 ? shaping : undefined;
958
+ }
959
+ function buildEffectiveExcludeFolders(options) {
960
+ var _a;
961
+ const userExcludes = (_a = options.excludeFolders) !== null && _a !== void 0 ? _a : [];
962
+ if (!options.excludeDefaultNoise) {
963
+ return userExcludes.length > 0 ? userExcludes : undefined;
964
+ }
965
+ const seen = new Set();
966
+ const merged = [];
967
+ for (const p of [...DEFAULT_NOISE_EXCLUDES, ...userExcludes]) {
968
+ const key = p.trim();
969
+ if (!key || seen.has(key))
970
+ continue;
971
+ seen.add(key);
972
+ merged.push(p);
973
+ }
974
+ return merged;
975
+ }
670
976
  function hasNonEmptyTrimmed(arr) {
671
977
  return (arr !== null && arr !== void 0 ? arr : []).some((s) => s.trim().length > 0);
672
978
  }
@@ -683,22 +989,25 @@ function summarizeGitDiff(options) {
683
989
  const git = (_a = options.git) !== null && _a !== void 0 ? _a : createGitClient(options.cwd);
684
990
  const from = options.from;
685
991
  const to = (_b = options.to) !== null && _b !== void 0 ? _b : "HEAD";
992
+ const effectiveExcludeFolders = buildEffectiveExcludeFolders(options);
686
993
  const pathFilter = hasNonEmptyTrimmed(options.includeFolders) ||
687
- hasNonEmptyTrimmed(options.excludeFolders)
994
+ hasNonEmptyTrimmed(effectiveExcludeFolders)
688
995
  ? {
689
996
  includeFolders: options.includeFolders,
690
- excludeFolders: options.excludeFolders,
997
+ excludeFolders: effectiveExcludeFolders,
691
998
  }
692
999
  : undefined;
693
1000
  const allCommits = yield getCommits(git, from, to);
694
1001
  const filteredCommits = filterCommitsByMessageRegexes(allCommits, options.commitMessageIncludeRegexes, options.commitMessageExcludeRegexes);
695
1002
  const filterByCommits = shouldFilterByCommits(allCommits, filteredCommits, options);
1003
+ const shaping = buildShapingFromOptions(options);
696
1004
  const rangeQuery = {
697
1005
  from,
698
1006
  to,
699
1007
  commits: filteredCommits,
700
1008
  filterByCommits,
701
1009
  pathFilter,
1010
+ shaping,
702
1011
  };
703
1012
  const [diffText, fileNames, diffSummary] = yield Promise.all([
704
1013
  getDiff(git, rangeQuery),
@@ -710,6 +1019,7 @@ function summarizeGitDiff(options) {
710
1019
  to,
711
1020
  team: options.teamName,
712
1021
  model: options.model,
1022
+ provider: options.provider,
713
1023
  maxDiffChars: options.maxDiffChars,
714
1024
  systemPrompt: options.systemPrompt,
715
1025
  commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
@@ -720,11 +1030,11 @@ function summarizeGitDiff(options) {
720
1030
  fileNames,
721
1031
  commits: filteredCommits,
722
1032
  flags: summarizeFlags,
723
- openAiClientProvider: options.openAiClientProvider,
1033
+ llmModelProvider: options.llmModelProvider,
724
1034
  diffSummary,
725
1035
  });
726
1036
  });
727
1037
  }
728
1038
 
729
- export { DEFAULT_GIT_DIFF_SYSTEM_PROMPT, LLM_GATEWAY_REQUIRED_MESSAGE, buildDiffPathspecs, createGitClient, createOpenAiLikeClient, filterCommitsByMessageRegexes, generateSummary, getChangedFiles, getCommits, getDiff, getDiffSummary, getRepoRoot, parseLlmDefaultHeadersFromEnv, resolveLlmBaseUrl, resolveLlmMaxDiffChars, resolveOpenAiLikeClientInit, shouldUseLlmGateway, splitPromotableAuthorizationFromHeaders, summarizeGitDiff, truncateUnifiedDiffForLlm };
1039
+ export { DEFAULT_GIT_DIFF_SYSTEM_PROMPT, DEFAULT_NOISE_EXCLUDES, LLM_GATEWAY_REQUIRED_MESSAGE, buildDiffPathspecs, buildDiffShapingGitArgs, createGitClient, defaultModelForProvider, detectLlmProvider, filterCommitsByMessageRegexes, generateSummary, getChangedFiles, getCommits, getDiff, getDiffSummary, getRepoRoot, isLlmProviderConfigured, parseLlmDefaultHeadersFromEnv, resolveLanguageModel, resolveLlmBaseUrl, resolveLlmMaxDiffChars, shapeUnifiedDiff, summarizeGitDiff, truncateUnifiedDiffForLlm };
730
1040
  //# sourceMappingURL=index.mjs.map