@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/README.md +123 -21
- package/dist/index.cjs +447 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.cjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.min.umd.js +1 -1
- package/dist/index.min.umd.js.map +1 -1
- package/dist/index.mjs +441 -131
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +450 -138
- package/dist/index.umd.js.map +1 -1
- package/dist/typings/ai/aiTypes.d.ts +5 -3
- package/dist/typings/ai/llmProviders.d.ts +12 -0
- package/dist/typings/git/diffShaping.d.ts +9 -0
- package/dist/typings/git/diffTypes.d.ts +2 -0
- package/dist/typings/git/gitDiff.d.ts +2 -0
- package/dist/typings/index.d.ts +14 -7
- package/package.json +34 -7
- package/dist/typings/ai/openAIConfig.d.ts +0 -21
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
|
|
38
|
-
return (
|
|
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
|
|
70
|
-
return Object.keys(headers).find((k) => k.toLowerCase() === "authorization");
|
|
71
|
-
}
|
|
72
|
-
function stripBearerPrefix(value) {
|
|
116
|
+
function resolveOpenAiApiKey() {
|
|
73
117
|
var _a;
|
|
74
|
-
|
|
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
|
|
110
|
-
var _a, _b
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
|
|
126
|
+
if (resolveLlmBaseUrl()) {
|
|
127
|
+
return "openai-compatible";
|
|
129
128
|
}
|
|
130
|
-
|
|
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
|
|
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 {
|
|
135
|
-
|
|
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
|
|
180
|
-
const { diffText, fileNames, commits, flags,
|
|
181
|
-
if (!
|
|
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 =
|
|
188
|
-
const
|
|
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
|
|
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
|
|
400
|
+
function callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, flags) {
|
|
246
401
|
return __awaiter(this, void 0, void 0, function* () {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
410
|
+
maxOutputTokens,
|
|
266
411
|
});
|
|
267
|
-
const
|
|
268
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
449
|
-
const delStr =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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([
|
|
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(
|
|
994
|
+
hasNonEmptyTrimmed(effectiveExcludeFolders)
|
|
688
995
|
? {
|
|
689
996
|
includeFolders: options.includeFolders,
|
|
690
|
-
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
|
-
|
|
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,
|
|
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
|