@eldrforge/ai-service 0.1.9 → 0.1.11
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 +312 -7
- package/dist/index.js +1642 -3
- package/dist/index.js.map +1 -1
- package/dist/src/agentic/commit.d.ts +34 -0
- package/dist/src/agentic/commit.d.ts.map +1 -0
- package/dist/src/agentic/executor.d.ts +54 -0
- package/dist/src/agentic/executor.d.ts.map +1 -0
- package/dist/src/agentic/release.d.ts +35 -0
- package/dist/src/agentic/release.d.ts.map +1 -0
- package/dist/src/ai.d.ts +7 -0
- package/dist/src/ai.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/tools/commit-tools.d.ts +6 -0
- package/dist/src/tools/commit-tools.d.ts.map +1 -0
- package/dist/src/tools/registry.d.ts +58 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/release-tools.d.ts +6 -0
- package/dist/src/tools/release-tools.d.ts.map +1 -0
- package/dist/src/tools/types.d.ts +60 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenAI } from "openai";
|
|
2
|
-
import { safeJsonParse } from "@eldrforge/git-tools";
|
|
2
|
+
import { safeJsonParse, run } from "@eldrforge/git-tools";
|
|
3
3
|
import fs$1 from "fs";
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
5
|
import * as path from "path";
|
|
@@ -146,6 +146,10 @@ async function createCompletion(messages, options = { model: "gpt-4o-mini" }) {
|
|
|
146
146
|
max_completion_tokens: maxCompletionTokens,
|
|
147
147
|
response_format: options.responseFormat
|
|
148
148
|
};
|
|
149
|
+
if (options.tools && options.tools.length > 0) {
|
|
150
|
+
apiOptions.tools = options.tools;
|
|
151
|
+
apiOptions.tool_choice = options.toolChoice || "auto";
|
|
152
|
+
}
|
|
149
153
|
if (options.openaiReasoning && (modelToUse.includes("gpt-5") || modelToUse.includes("o3"))) {
|
|
150
154
|
apiOptions.reasoning_effort = options.openaiReasoning;
|
|
151
155
|
}
|
|
@@ -178,9 +182,26 @@ async function createCompletion(messages, options = { model: "gpt-4o-mini" }) {
|
|
|
178
182
|
await options.storage.writeTemp(debugFile, JSON.stringify(completion, null, 2));
|
|
179
183
|
logger2.debug("Wrote response debug file to %s", debugFile);
|
|
180
184
|
}
|
|
181
|
-
const
|
|
185
|
+
const message = completion.choices[0]?.message;
|
|
186
|
+
if (!message) {
|
|
187
|
+
throw new OpenAIError("No response message received from OpenAI");
|
|
188
|
+
}
|
|
189
|
+
if (options.tools && options.tools.length > 0) {
|
|
190
|
+
const elapsedTimeFormatted2 = elapsedTime >= 1e3 ? `${(elapsedTime / 1e3).toFixed(1)}s` : `${elapsedTime}ms`;
|
|
191
|
+
logger2.info(" Time: %s", elapsedTimeFormatted2);
|
|
192
|
+
if (completion.usage) {
|
|
193
|
+
logger2.info(
|
|
194
|
+
" Token usage: %s prompt + %s completion = %s total",
|
|
195
|
+
completion.usage.prompt_tokens?.toLocaleString() || "?",
|
|
196
|
+
completion.usage.completion_tokens?.toLocaleString() || "?",
|
|
197
|
+
completion.usage.total_tokens?.toLocaleString() || "?"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return message;
|
|
201
|
+
}
|
|
202
|
+
const response = message.content?.trim();
|
|
182
203
|
if (!response) {
|
|
183
|
-
throw new OpenAIError("No response received from OpenAI");
|
|
204
|
+
throw new OpenAIError("No response content received from OpenAI");
|
|
184
205
|
}
|
|
185
206
|
const responseSize = response.length;
|
|
186
207
|
const responseSizeKB = (responseSize / 1024).toFixed(2);
|
|
@@ -795,18 +816,1633 @@ const createReviewPrompt = async ({ overridePaths: _overridePaths, overrides: _o
|
|
|
795
816
|
}
|
|
796
817
|
return recipe(basePath).persona({ path: "personas/you.md" }).instructions({ path: "instructions/review.md" }).overridePaths(_overridePaths ?? []).overrides(_overrides ?? true).content(...contentItems).context(...contextItems).cook();
|
|
797
818
|
};
|
|
819
|
+
class AgenticExecutor {
|
|
820
|
+
logger;
|
|
821
|
+
toolMetrics = [];
|
|
822
|
+
constructor(logger2) {
|
|
823
|
+
this.logger = logger2;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Run the agentic loop
|
|
827
|
+
*/
|
|
828
|
+
async run(config) {
|
|
829
|
+
const {
|
|
830
|
+
messages: initialMessages,
|
|
831
|
+
tools,
|
|
832
|
+
model = "gpt-4o",
|
|
833
|
+
maxIterations = 10,
|
|
834
|
+
debug = false,
|
|
835
|
+
debugRequestFile,
|
|
836
|
+
debugResponseFile,
|
|
837
|
+
storage,
|
|
838
|
+
logger: logger2,
|
|
839
|
+
openaiReasoning = false
|
|
840
|
+
} = config;
|
|
841
|
+
const messages = [...initialMessages];
|
|
842
|
+
let iterations = 0;
|
|
843
|
+
let toolCallsExecuted = 0;
|
|
844
|
+
this.log("Starting agentic loop", { maxIterations, toolCount: tools.count() });
|
|
845
|
+
while (iterations < maxIterations) {
|
|
846
|
+
iterations++;
|
|
847
|
+
this.log(`Iteration ${iterations}/${maxIterations}`);
|
|
848
|
+
const response = await createCompletionWithRetry(
|
|
849
|
+
messages,
|
|
850
|
+
{
|
|
851
|
+
model,
|
|
852
|
+
openaiReasoning: openaiReasoning || void 0,
|
|
853
|
+
debug,
|
|
854
|
+
debugRequestFile: debugRequestFile ? `${debugRequestFile}-iter${iterations}` : void 0,
|
|
855
|
+
debugResponseFile: debugResponseFile ? `${debugResponseFile}-iter${iterations}` : void 0,
|
|
856
|
+
storage,
|
|
857
|
+
logger: logger2,
|
|
858
|
+
tools: tools.toOpenAIFormat()
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
const message = typeof response === "string" ? { content: response } : response;
|
|
862
|
+
const toolCalls = message.tool_calls || [];
|
|
863
|
+
if (toolCalls.length === 0) {
|
|
864
|
+
const finalContent = message.content || "";
|
|
865
|
+
this.log("Agent completed without tool calls", { iterations, toolCallsExecuted });
|
|
866
|
+
messages.push({
|
|
867
|
+
role: "assistant",
|
|
868
|
+
content: finalContent
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
finalMessage: finalContent,
|
|
872
|
+
iterations,
|
|
873
|
+
toolCallsExecuted,
|
|
874
|
+
conversationHistory: messages,
|
|
875
|
+
toolMetrics: this.toolMetrics
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
messages.push({
|
|
879
|
+
role: "assistant",
|
|
880
|
+
content: message.content || null,
|
|
881
|
+
tool_calls: toolCalls
|
|
882
|
+
});
|
|
883
|
+
this.log(`Executing ${toolCalls.length} tool call(s)`);
|
|
884
|
+
for (const toolCall of toolCalls) {
|
|
885
|
+
const startTime = Date.now();
|
|
886
|
+
const toolName = toolCall.function.name;
|
|
887
|
+
try {
|
|
888
|
+
this.log(`Executing tool: ${toolName}`, toolCall.function.arguments);
|
|
889
|
+
if (this.logger?.info) {
|
|
890
|
+
this.logger.info(`🔧 Running tool: ${toolName}`);
|
|
891
|
+
}
|
|
892
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
893
|
+
const result = await tools.execute(toolName, args);
|
|
894
|
+
const duration = Date.now() - startTime;
|
|
895
|
+
messages.push({
|
|
896
|
+
role: "tool",
|
|
897
|
+
tool_call_id: toolCall.id,
|
|
898
|
+
content: this.formatToolResult({ id: toolCall.id, name: toolName, result })
|
|
899
|
+
});
|
|
900
|
+
toolCallsExecuted++;
|
|
901
|
+
this.toolMetrics.push({
|
|
902
|
+
name: toolName,
|
|
903
|
+
success: true,
|
|
904
|
+
duration,
|
|
905
|
+
iteration: iterations,
|
|
906
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
907
|
+
});
|
|
908
|
+
this.log(`Tool ${toolName} succeeded in ${duration}ms`);
|
|
909
|
+
if (this.logger?.info) {
|
|
910
|
+
this.logger.info(`✅ Tool ${toolName} completed (${duration}ms)`);
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
const duration = Date.now() - startTime;
|
|
914
|
+
const errorMessage = error.message || String(error);
|
|
915
|
+
this.log(`Tool ${toolName} failed: ${errorMessage}`);
|
|
916
|
+
this.toolMetrics.push({
|
|
917
|
+
name: toolName,
|
|
918
|
+
success: false,
|
|
919
|
+
duration,
|
|
920
|
+
error: errorMessage,
|
|
921
|
+
iteration: iterations,
|
|
922
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
923
|
+
});
|
|
924
|
+
if (this.logger?.warn) {
|
|
925
|
+
this.logger.warn(`❌ Tool ${toolName} failed: ${errorMessage}`);
|
|
926
|
+
}
|
|
927
|
+
messages.push({
|
|
928
|
+
role: "tool",
|
|
929
|
+
tool_call_id: toolCall.id,
|
|
930
|
+
content: `Tool execution failed: ${errorMessage}`
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
this.log(`Completed tool execution`, { toolCallsExecuted });
|
|
935
|
+
}
|
|
936
|
+
this.log("Max iterations reached, forcing completion", { iterations, toolCallsExecuted });
|
|
937
|
+
messages.push({
|
|
938
|
+
role: "user",
|
|
939
|
+
content: "Please provide your final analysis and commit message based on your investigation. Do not request any more tools."
|
|
940
|
+
});
|
|
941
|
+
const finalResponse = await createCompletionWithRetry(
|
|
942
|
+
messages,
|
|
943
|
+
{
|
|
944
|
+
model,
|
|
945
|
+
openaiReasoning: openaiReasoning || void 0,
|
|
946
|
+
debug,
|
|
947
|
+
storage,
|
|
948
|
+
logger: logger2
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
messages.push({
|
|
952
|
+
role: "assistant",
|
|
953
|
+
content: finalResponse
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
finalMessage: finalResponse,
|
|
957
|
+
iterations,
|
|
958
|
+
toolCallsExecuted,
|
|
959
|
+
conversationHistory: messages,
|
|
960
|
+
toolMetrics: this.toolMetrics
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Format tool result for inclusion in conversation
|
|
965
|
+
*/
|
|
966
|
+
formatToolResult(result) {
|
|
967
|
+
if (typeof result.result === "string") {
|
|
968
|
+
return result.result;
|
|
969
|
+
}
|
|
970
|
+
return JSON.stringify(result.result, null, 2);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Log a message if logger is available
|
|
974
|
+
*/
|
|
975
|
+
log(message, data) {
|
|
976
|
+
if (this.logger?.debug) {
|
|
977
|
+
if (data) {
|
|
978
|
+
this.logger.debug(`[AgenticExecutor] ${message}`, data);
|
|
979
|
+
} else {
|
|
980
|
+
this.logger.debug(`[AgenticExecutor] ${message}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async function runAgentic(config) {
|
|
986
|
+
const executor = new AgenticExecutor(config.logger);
|
|
987
|
+
return await executor.run(config);
|
|
988
|
+
}
|
|
989
|
+
class ToolRegistry {
|
|
990
|
+
tools = /* @__PURE__ */ new Map();
|
|
991
|
+
context;
|
|
992
|
+
constructor(context) {
|
|
993
|
+
this.context = context;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Register a tool in the registry
|
|
997
|
+
*/
|
|
998
|
+
register(tool) {
|
|
999
|
+
if (this.tools.has(tool.name)) {
|
|
1000
|
+
throw new Error(`Tool with name "${tool.name}" is already registered`);
|
|
1001
|
+
}
|
|
1002
|
+
this.tools.set(tool.name, tool);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Register multiple tools at once
|
|
1006
|
+
*/
|
|
1007
|
+
registerAll(tools) {
|
|
1008
|
+
for (const tool of tools) {
|
|
1009
|
+
this.register(tool);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Get a tool by name
|
|
1014
|
+
*/
|
|
1015
|
+
get(name) {
|
|
1016
|
+
return this.tools.get(name);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Get all registered tools
|
|
1020
|
+
*/
|
|
1021
|
+
getAll() {
|
|
1022
|
+
return Array.from(this.tools.values());
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Check if a tool exists
|
|
1026
|
+
*/
|
|
1027
|
+
has(name) {
|
|
1028
|
+
return this.tools.has(name);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Execute a tool by name with given parameters
|
|
1032
|
+
*/
|
|
1033
|
+
async execute(name, params) {
|
|
1034
|
+
const tool = this.tools.get(name);
|
|
1035
|
+
if (!tool) {
|
|
1036
|
+
throw new Error(`Tool "${name}" not found in registry`);
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
return await tool.execute(params, this.context);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
throw new Error(`Tool "${name}" execution failed: ${error.message}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Convert tools to OpenAI function calling format
|
|
1046
|
+
*/
|
|
1047
|
+
toOpenAIFormat() {
|
|
1048
|
+
return this.getAll().map((tool) => ({
|
|
1049
|
+
type: "function",
|
|
1050
|
+
function: {
|
|
1051
|
+
name: tool.name,
|
|
1052
|
+
description: tool.description,
|
|
1053
|
+
parameters: tool.parameters
|
|
1054
|
+
}
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get tool definitions (without execute functions) for serialization
|
|
1059
|
+
*/
|
|
1060
|
+
getDefinitions() {
|
|
1061
|
+
return this.getAll().map((tool) => ({
|
|
1062
|
+
name: tool.name,
|
|
1063
|
+
description: tool.description,
|
|
1064
|
+
parameters: tool.parameters
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Clear all registered tools
|
|
1069
|
+
*/
|
|
1070
|
+
clear() {
|
|
1071
|
+
this.tools.clear();
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get count of registered tools
|
|
1075
|
+
*/
|
|
1076
|
+
count() {
|
|
1077
|
+
return this.tools.size;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function createToolRegistry(context) {
|
|
1081
|
+
return new ToolRegistry(context);
|
|
1082
|
+
}
|
|
1083
|
+
function createCommitTools() {
|
|
1084
|
+
return [
|
|
1085
|
+
createGetFileHistoryTool$1(),
|
|
1086
|
+
createGetFileContentTool$1(),
|
|
1087
|
+
createSearchCodebaseTool$1(),
|
|
1088
|
+
createGetRelatedTestsTool$1(),
|
|
1089
|
+
createGetFileDependenciesTool$1(),
|
|
1090
|
+
createAnalyzeDiffSectionTool$1(),
|
|
1091
|
+
createGetRecentCommitsTool$1(),
|
|
1092
|
+
createGroupFilesByConcernTool$1()
|
|
1093
|
+
];
|
|
1094
|
+
}
|
|
1095
|
+
function createGetFileHistoryTool$1() {
|
|
1096
|
+
return {
|
|
1097
|
+
name: "get_file_history",
|
|
1098
|
+
description: "Get git commit history for one or more files to understand their evolution and past changes",
|
|
1099
|
+
parameters: {
|
|
1100
|
+
type: "object",
|
|
1101
|
+
properties: {
|
|
1102
|
+
filePaths: {
|
|
1103
|
+
type: "array",
|
|
1104
|
+
description: "Array of file paths to get history for",
|
|
1105
|
+
items: { type: "string", description: "File path" }
|
|
1106
|
+
},
|
|
1107
|
+
limit: {
|
|
1108
|
+
type: "number",
|
|
1109
|
+
description: "Maximum number of commits to return (default: 10)",
|
|
1110
|
+
default: 10
|
|
1111
|
+
},
|
|
1112
|
+
format: {
|
|
1113
|
+
type: "string",
|
|
1114
|
+
description: 'Output format: "summary" for brief or "detailed" for full messages',
|
|
1115
|
+
enum: ["summary", "detailed"],
|
|
1116
|
+
default: "summary"
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
required: ["filePaths"]
|
|
1120
|
+
},
|
|
1121
|
+
execute: async (params, context) => {
|
|
1122
|
+
const { filePaths, limit = 10, format = "summary" } = params;
|
|
1123
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1124
|
+
const formatArg = format === "detailed" ? "--format=%H%n%an (%ae)%n%ad%n%s%n%n%b%n---" : "--format=%h - %s (%an, %ar)";
|
|
1125
|
+
const fileArgs = filePaths.join(" ");
|
|
1126
|
+
const command = `git log ${formatArg} -n ${limit} -- ${fileArgs}`;
|
|
1127
|
+
try {
|
|
1128
|
+
const output = await run(command, { cwd: workingDir });
|
|
1129
|
+
return output.stdout || "No history found for specified files";
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
throw new Error(`Failed to get file history: ${error.message}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function createGetFileContentTool$1() {
|
|
1137
|
+
return {
|
|
1138
|
+
name: "get_file_content",
|
|
1139
|
+
description: "Get the complete current content of a file to understand context around changes",
|
|
1140
|
+
parameters: {
|
|
1141
|
+
type: "object",
|
|
1142
|
+
properties: {
|
|
1143
|
+
filePath: {
|
|
1144
|
+
type: "string",
|
|
1145
|
+
description: "Path to the file"
|
|
1146
|
+
},
|
|
1147
|
+
includeLineNumbers: {
|
|
1148
|
+
type: "boolean",
|
|
1149
|
+
description: "Include line numbers in output (default: false)",
|
|
1150
|
+
default: false
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
required: ["filePath"]
|
|
1154
|
+
},
|
|
1155
|
+
execute: async (params, context) => {
|
|
1156
|
+
const { filePath, includeLineNumbers = false } = params;
|
|
1157
|
+
const storage = context?.storage;
|
|
1158
|
+
if (!storage) {
|
|
1159
|
+
throw new Error("Storage adapter not available in context");
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const content = await storage.readFile(filePath, "utf-8");
|
|
1163
|
+
if (includeLineNumbers) {
|
|
1164
|
+
const lines = content.split("\n");
|
|
1165
|
+
return lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
1166
|
+
}
|
|
1167
|
+
return content;
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function createSearchCodebaseTool$1() {
|
|
1175
|
+
return {
|
|
1176
|
+
name: "search_codebase",
|
|
1177
|
+
description: "Search for code patterns, function names, or text across the codebase using git grep",
|
|
1178
|
+
parameters: {
|
|
1179
|
+
type: "object",
|
|
1180
|
+
properties: {
|
|
1181
|
+
query: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
description: "Search pattern (can be plain text or regex)"
|
|
1184
|
+
},
|
|
1185
|
+
fileTypes: {
|
|
1186
|
+
type: "array",
|
|
1187
|
+
description: 'Limit search to specific file extensions (e.g., ["ts", "js"])',
|
|
1188
|
+
items: { type: "string", description: "File extension" }
|
|
1189
|
+
},
|
|
1190
|
+
contextLines: {
|
|
1191
|
+
type: "number",
|
|
1192
|
+
description: "Number of context lines to show around matches (default: 2)",
|
|
1193
|
+
default: 2
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
required: ["query"]
|
|
1197
|
+
},
|
|
1198
|
+
execute: async (params, context) => {
|
|
1199
|
+
const { query, fileTypes, contextLines = 2 } = params;
|
|
1200
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1201
|
+
let command = `git grep -n -C ${contextLines} "${query}"`;
|
|
1202
|
+
if (fileTypes && fileTypes.length > 0) {
|
|
1203
|
+
const patterns = fileTypes.map((ext) => `'*.${ext}'`).join(" ");
|
|
1204
|
+
command += ` -- ${patterns}`;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const output = await run(command, { cwd: workingDir });
|
|
1208
|
+
return output.stdout || "No matches found";
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error.message.includes("exit code 1")) {
|
|
1211
|
+
return "No matches found";
|
|
1212
|
+
}
|
|
1213
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function createGetRelatedTestsTool$1() {
|
|
1219
|
+
return {
|
|
1220
|
+
name: "get_related_tests",
|
|
1221
|
+
description: "Find test files related to production files to understand what the code is supposed to do",
|
|
1222
|
+
parameters: {
|
|
1223
|
+
type: "object",
|
|
1224
|
+
properties: {
|
|
1225
|
+
filePaths: {
|
|
1226
|
+
type: "array",
|
|
1227
|
+
description: "Production file paths",
|
|
1228
|
+
items: { type: "string", description: "File path" }
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
required: ["filePaths"]
|
|
1232
|
+
},
|
|
1233
|
+
execute: async (params, context) => {
|
|
1234
|
+
const { filePaths } = params;
|
|
1235
|
+
const storage = context?.storage;
|
|
1236
|
+
if (!storage) {
|
|
1237
|
+
throw new Error("Storage adapter not available in context");
|
|
1238
|
+
}
|
|
1239
|
+
const relatedTests = [];
|
|
1240
|
+
for (const filePath of filePaths) {
|
|
1241
|
+
const patterns = [
|
|
1242
|
+
filePath.replace(/\.(ts|js|tsx|jsx)$/, ".test.$1"),
|
|
1243
|
+
filePath.replace(/\.(ts|js|tsx|jsx)$/, ".spec.$1"),
|
|
1244
|
+
filePath.replace("/src/", "/tests/").replace("/lib/", "/tests/"),
|
|
1245
|
+
path.join("tests", filePath),
|
|
1246
|
+
path.join("test", filePath)
|
|
1247
|
+
];
|
|
1248
|
+
for (const pattern of patterns) {
|
|
1249
|
+
try {
|
|
1250
|
+
await storage.readFile(pattern, "utf-8");
|
|
1251
|
+
relatedTests.push(pattern);
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (relatedTests.length === 0) {
|
|
1257
|
+
return "No related test files found";
|
|
1258
|
+
}
|
|
1259
|
+
return `Found related test files:
|
|
1260
|
+
${relatedTests.join("\n")}`;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function createGetFileDependenciesTool$1() {
|
|
1265
|
+
return {
|
|
1266
|
+
name: "get_file_dependencies",
|
|
1267
|
+
description: "Find which files import or depend on the changed files to assess change impact",
|
|
1268
|
+
parameters: {
|
|
1269
|
+
type: "object",
|
|
1270
|
+
properties: {
|
|
1271
|
+
filePaths: {
|
|
1272
|
+
type: "array",
|
|
1273
|
+
description: "Files to analyze dependencies for",
|
|
1274
|
+
items: { type: "string", description: "File path" }
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
required: ["filePaths"]
|
|
1278
|
+
},
|
|
1279
|
+
execute: async (params, context) => {
|
|
1280
|
+
const { filePaths } = params;
|
|
1281
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1282
|
+
const results = [];
|
|
1283
|
+
for (const filePath of filePaths) {
|
|
1284
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
1285
|
+
const searchPatterns = [
|
|
1286
|
+
`from.*['"].*${fileName}`,
|
|
1287
|
+
`import.*['"].*${fileName}`,
|
|
1288
|
+
`require\\(['"].*${fileName}`
|
|
1289
|
+
];
|
|
1290
|
+
for (const pattern of searchPatterns) {
|
|
1291
|
+
try {
|
|
1292
|
+
const command = `git grep -l "${pattern}"`;
|
|
1293
|
+
const output = await run(command, { cwd: workingDir });
|
|
1294
|
+
if (output.stdout) {
|
|
1295
|
+
results.push(`Files importing ${filePath}:
|
|
1296
|
+
${output.stdout}`);
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return results.length > 0 ? results.join("\n\n") : "No files found that import the specified files";
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function createAnalyzeDiffSectionTool$1() {
|
|
1308
|
+
return {
|
|
1309
|
+
name: "analyze_diff_section",
|
|
1310
|
+
description: "Get expanded context around specific lines in a file to better understand changes",
|
|
1311
|
+
parameters: {
|
|
1312
|
+
type: "object",
|
|
1313
|
+
properties: {
|
|
1314
|
+
filePath: {
|
|
1315
|
+
type: "string",
|
|
1316
|
+
description: "File containing the section to analyze"
|
|
1317
|
+
},
|
|
1318
|
+
startLine: {
|
|
1319
|
+
type: "number",
|
|
1320
|
+
description: "Starting line number"
|
|
1321
|
+
},
|
|
1322
|
+
endLine: {
|
|
1323
|
+
type: "number",
|
|
1324
|
+
description: "Ending line number"
|
|
1325
|
+
},
|
|
1326
|
+
contextLines: {
|
|
1327
|
+
type: "number",
|
|
1328
|
+
description: "Number of additional context lines to show before and after (default: 10)",
|
|
1329
|
+
default: 10
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
required: ["filePath", "startLine", "endLine"]
|
|
1333
|
+
},
|
|
1334
|
+
execute: async (params, context) => {
|
|
1335
|
+
const { filePath, startLine, endLine, contextLines = 10 } = params;
|
|
1336
|
+
const storage = context?.storage;
|
|
1337
|
+
if (!storage) {
|
|
1338
|
+
throw new Error("Storage adapter not available in context");
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
const content = await storage.readFile(filePath, "utf-8");
|
|
1342
|
+
const lines = content.split("\n");
|
|
1343
|
+
const actualStart = Math.max(0, startLine - contextLines - 1);
|
|
1344
|
+
const actualEnd = Math.min(lines.length, endLine + contextLines);
|
|
1345
|
+
const section = lines.slice(actualStart, actualEnd).map((line, idx) => `${actualStart + idx + 1}: ${line}`).join("\n");
|
|
1346
|
+
return `Lines ${actualStart + 1}-${actualEnd} from ${filePath}:
|
|
1347
|
+
|
|
1348
|
+
${section}`;
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
throw new Error(`Failed to analyze diff section: ${error.message}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function createGetRecentCommitsTool$1() {
|
|
1356
|
+
return {
|
|
1357
|
+
name: "get_recent_commits",
|
|
1358
|
+
description: "Get recent commits that modified the same files to understand recent work in this area",
|
|
1359
|
+
parameters: {
|
|
1360
|
+
type: "object",
|
|
1361
|
+
properties: {
|
|
1362
|
+
filePaths: {
|
|
1363
|
+
type: "array",
|
|
1364
|
+
description: "Files to check for recent commits",
|
|
1365
|
+
items: { type: "string", description: "File path" }
|
|
1366
|
+
},
|
|
1367
|
+
since: {
|
|
1368
|
+
type: "string",
|
|
1369
|
+
description: 'Time period to look back (e.g., "1 week ago", "2 days ago")',
|
|
1370
|
+
default: "1 week ago"
|
|
1371
|
+
},
|
|
1372
|
+
limit: {
|
|
1373
|
+
type: "number",
|
|
1374
|
+
description: "Maximum number of commits to return (default: 5)",
|
|
1375
|
+
default: 5
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
required: ["filePaths"]
|
|
1379
|
+
},
|
|
1380
|
+
execute: async (params, context) => {
|
|
1381
|
+
const { filePaths, since = "1 week ago", limit = 5 } = params;
|
|
1382
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1383
|
+
const fileArgs = filePaths.join(" ");
|
|
1384
|
+
const command = `git log --format="%h - %s (%an, %ar)" --since="${since}" -n ${limit} -- ${fileArgs}`;
|
|
1385
|
+
try {
|
|
1386
|
+
const output = await run(command, { cwd: workingDir });
|
|
1387
|
+
return output.stdout || `No commits found in the specified time period (${since})`;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
throw new Error(`Failed to get recent commits: ${error.message}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function createGroupFilesByConcernTool$1() {
|
|
1395
|
+
return {
|
|
1396
|
+
name: "group_files_by_concern",
|
|
1397
|
+
description: "Analyze changed files and suggest logical groupings that might represent separate commits",
|
|
1398
|
+
parameters: {
|
|
1399
|
+
type: "object",
|
|
1400
|
+
properties: {
|
|
1401
|
+
filePaths: {
|
|
1402
|
+
type: "array",
|
|
1403
|
+
description: "All changed files to analyze",
|
|
1404
|
+
items: { type: "string", description: "File path" }
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
required: ["filePaths"]
|
|
1408
|
+
},
|
|
1409
|
+
execute: async (params) => {
|
|
1410
|
+
const { filePaths } = params;
|
|
1411
|
+
const groups = {};
|
|
1412
|
+
for (const filePath of filePaths) {
|
|
1413
|
+
const dir = path.dirname(filePath);
|
|
1414
|
+
const ext = path.extname(filePath);
|
|
1415
|
+
const basename = path.basename(filePath, ext);
|
|
1416
|
+
let category = "other";
|
|
1417
|
+
if (basename.includes(".test") || basename.includes(".spec") || dir.includes("test")) {
|
|
1418
|
+
category = "tests";
|
|
1419
|
+
} else if (filePath.includes("package.json") || filePath.includes("package-lock.json")) {
|
|
1420
|
+
category = "dependencies";
|
|
1421
|
+
} else if (ext === ".md" || basename === "README") {
|
|
1422
|
+
category = "documentation";
|
|
1423
|
+
} else if (dir.includes("src") || dir.includes("lib")) {
|
|
1424
|
+
category = `source:${dir.split("/").slice(0, 3).join("/")}`;
|
|
1425
|
+
}
|
|
1426
|
+
if (!groups[category]) {
|
|
1427
|
+
groups[category] = [];
|
|
1428
|
+
}
|
|
1429
|
+
groups[category].push(filePath);
|
|
1430
|
+
}
|
|
1431
|
+
const output = Object.entries(groups).map(([category, files]) => {
|
|
1432
|
+
return `${category} (${files.length} files):
|
|
1433
|
+
${files.map((f) => ` - ${f}`).join("\n")}`;
|
|
1434
|
+
}).join("\n\n");
|
|
1435
|
+
const groupCount = Object.keys(groups).length;
|
|
1436
|
+
const suggestion = groupCount > 1 ? `
|
|
1437
|
+
|
|
1438
|
+
Suggestion: These ${groupCount} groups might be better as separate commits if they represent different concerns.` : "\n\nSuggestion: All files appear to be related and should be in a single commit.";
|
|
1439
|
+
return output + suggestion;
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
async function runAgenticCommit(config) {
|
|
1444
|
+
const {
|
|
1445
|
+
changedFiles,
|
|
1446
|
+
diffContent,
|
|
1447
|
+
userDirection,
|
|
1448
|
+
logContext,
|
|
1449
|
+
model = "gpt-4o",
|
|
1450
|
+
maxIterations = 10,
|
|
1451
|
+
debug = false,
|
|
1452
|
+
debugRequestFile,
|
|
1453
|
+
debugResponseFile,
|
|
1454
|
+
storage,
|
|
1455
|
+
logger: logger2,
|
|
1456
|
+
openaiReasoning
|
|
1457
|
+
} = config;
|
|
1458
|
+
const toolRegistry = createToolRegistry({
|
|
1459
|
+
workingDirectory: process.cwd(),
|
|
1460
|
+
storage,
|
|
1461
|
+
logger: logger2
|
|
1462
|
+
});
|
|
1463
|
+
const tools = createCommitTools();
|
|
1464
|
+
toolRegistry.registerAll(tools);
|
|
1465
|
+
const systemPrompt = buildSystemPrompt$1();
|
|
1466
|
+
const userMessage = buildUserMessage$1(changedFiles, diffContent, userDirection, logContext);
|
|
1467
|
+
const messages = [
|
|
1468
|
+
{ role: "system", content: systemPrompt },
|
|
1469
|
+
{ role: "user", content: userMessage }
|
|
1470
|
+
];
|
|
1471
|
+
const agenticConfig = {
|
|
1472
|
+
messages,
|
|
1473
|
+
tools: toolRegistry,
|
|
1474
|
+
model,
|
|
1475
|
+
maxIterations,
|
|
1476
|
+
debug,
|
|
1477
|
+
debugRequestFile,
|
|
1478
|
+
debugResponseFile,
|
|
1479
|
+
storage,
|
|
1480
|
+
logger: logger2,
|
|
1481
|
+
openaiReasoning
|
|
1482
|
+
};
|
|
1483
|
+
const result = await runAgentic(agenticConfig);
|
|
1484
|
+
const parsed = parseAgenticResult$1(result.finalMessage);
|
|
1485
|
+
return {
|
|
1486
|
+
commitMessage: parsed.commitMessage,
|
|
1487
|
+
iterations: result.iterations,
|
|
1488
|
+
toolCallsExecuted: result.toolCallsExecuted,
|
|
1489
|
+
suggestedSplits: parsed.suggestedSplits,
|
|
1490
|
+
conversationHistory: result.conversationHistory,
|
|
1491
|
+
toolMetrics: result.toolMetrics
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function buildSystemPrompt$1() {
|
|
1495
|
+
return `You are an expert software engineer tasked with generating meaningful commit messages.
|
|
1496
|
+
|
|
1497
|
+
You have access to tools that let you investigate changes in detail:
|
|
1498
|
+
- get_file_history: View commit history for files
|
|
1499
|
+
- get_file_content: Read full file contents
|
|
1500
|
+
- search_codebase: Search for patterns across the codebase
|
|
1501
|
+
- get_related_tests: Find test files related to changes
|
|
1502
|
+
- get_file_dependencies: Understand file dependencies and imports
|
|
1503
|
+
- analyze_diff_section: Get expanded context around specific changes
|
|
1504
|
+
- get_recent_commits: See recent commits to the same files
|
|
1505
|
+
- group_files_by_concern: Suggest logical groupings of changed files
|
|
1506
|
+
|
|
1507
|
+
Your process should be:
|
|
1508
|
+
1. Analyze the changed files and diff to understand the scope
|
|
1509
|
+
2. Use tools to investigate specific changes that need more context
|
|
1510
|
+
3. Identify if changes represent one cohesive commit or multiple logical commits
|
|
1511
|
+
4. Generate a commit message (or multiple if splits suggested) that accurately describes changes
|
|
1512
|
+
|
|
1513
|
+
Guidelines:
|
|
1514
|
+
- Use tools strategically - don't call every tool on every file
|
|
1515
|
+
- Look at test changes to understand intent
|
|
1516
|
+
- Check recent history to avoid redundant messages
|
|
1517
|
+
- Consider suggesting split commits for unrelated changes
|
|
1518
|
+
- Synthesize findings into clear, informative commit messages
|
|
1519
|
+
- Follow conventional commit format when appropriate (feat:, fix:, refactor:, etc.)
|
|
1520
|
+
|
|
1521
|
+
Output format:
|
|
1522
|
+
When you're ready to provide the final commit message, format it as:
|
|
1523
|
+
|
|
1524
|
+
COMMIT_MESSAGE:
|
|
1525
|
+
[Your generated commit message here]
|
|
1526
|
+
|
|
1527
|
+
If you recommend splitting into multiple commits, also include:
|
|
1528
|
+
|
|
1529
|
+
SUGGESTED_SPLITS:
|
|
1530
|
+
Split 1:
|
|
1531
|
+
Files: [list of files]
|
|
1532
|
+
Rationale: [why these belong together]
|
|
1533
|
+
Message: [commit message for this split]
|
|
1534
|
+
|
|
1535
|
+
Split 2:
|
|
1536
|
+
...
|
|
1537
|
+
|
|
1538
|
+
If changes should remain as one commit, do not include SUGGESTED_SPLITS section.`;
|
|
1539
|
+
}
|
|
1540
|
+
function buildUserMessage$1(changedFiles, diffContent, userDirection, logContext) {
|
|
1541
|
+
let message = `I have staged changes that need a commit message.
|
|
1542
|
+
|
|
1543
|
+
Changed files (${changedFiles.length}):
|
|
1544
|
+
${changedFiles.map((f) => ` - ${f}`).join("\n")}
|
|
1545
|
+
|
|
1546
|
+
Diff:
|
|
1547
|
+
${diffContent}`;
|
|
1548
|
+
if (userDirection) {
|
|
1549
|
+
message += `
|
|
1550
|
+
|
|
1551
|
+
User direction: ${userDirection}`;
|
|
1552
|
+
}
|
|
1553
|
+
if (logContext) {
|
|
1554
|
+
message += `
|
|
1555
|
+
|
|
1556
|
+
Recent commit history for context:
|
|
1557
|
+
${logContext}`;
|
|
1558
|
+
}
|
|
1559
|
+
message += `
|
|
1560
|
+
|
|
1561
|
+
Please investigate these changes and generate an appropriate commit message.
|
|
1562
|
+
Use the available tools to gather additional context as needed.
|
|
1563
|
+
If these changes should be split into multiple commits, suggest the groupings.`;
|
|
1564
|
+
return message;
|
|
1565
|
+
}
|
|
1566
|
+
function parseAgenticResult$1(finalMessage) {
|
|
1567
|
+
const commitMatch = finalMessage.match(/COMMIT_MESSAGE:\s*\n([\s\S]*?)(?=\n\nSUGGESTED_SPLITS:|$)/);
|
|
1568
|
+
const commitMessage = commitMatch ? commitMatch[1].trim() : finalMessage.trim();
|
|
1569
|
+
const suggestedSplits = [];
|
|
1570
|
+
const splitsMatch = finalMessage.match(/SUGGESTED_SPLITS:\s*\n([\s\S]*)/);
|
|
1571
|
+
if (splitsMatch) {
|
|
1572
|
+
const splitsText = splitsMatch[1];
|
|
1573
|
+
const splitRegex = /Split \d+:\s*\nFiles: ([\s\S]*?)\nRationale: ([\s\S]*?)\nMessage: ([\s\S]*?)(?=\n\nSplit \d+:|$)/g;
|
|
1574
|
+
let match;
|
|
1575
|
+
while ((match = splitRegex.exec(splitsText)) !== null) {
|
|
1576
|
+
const filesText = match[1].trim();
|
|
1577
|
+
const files = filesText.split("\n").map((line) => line.trim().replace(/^[-*]\s*/, "")).filter((line) => line.length > 0);
|
|
1578
|
+
suggestedSplits.push({
|
|
1579
|
+
files,
|
|
1580
|
+
rationale: match[2].trim(),
|
|
1581
|
+
message: match[3].trim()
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return { commitMessage, suggestedSplits };
|
|
1586
|
+
}
|
|
1587
|
+
function createReleaseTools() {
|
|
1588
|
+
return [
|
|
1589
|
+
createGetFileHistoryTool(),
|
|
1590
|
+
createGetFileContentTool(),
|
|
1591
|
+
createSearchCodebaseTool(),
|
|
1592
|
+
createGetRelatedTestsTool(),
|
|
1593
|
+
createGetFileDependenciesTool(),
|
|
1594
|
+
createAnalyzeDiffSectionTool(),
|
|
1595
|
+
createGetRecentCommitsTool(),
|
|
1596
|
+
createGroupFilesByConcernTool(),
|
|
1597
|
+
createGetTagHistoryTool(),
|
|
1598
|
+
createComparePreviousReleaseTool(),
|
|
1599
|
+
createGetReleaseStatsTool(),
|
|
1600
|
+
createGetBreakingChangesTool(),
|
|
1601
|
+
createAnalyzeCommitPatternsTool()
|
|
1602
|
+
];
|
|
1603
|
+
}
|
|
1604
|
+
function createGetFileHistoryTool() {
|
|
1605
|
+
return {
|
|
1606
|
+
name: "get_file_history",
|
|
1607
|
+
description: "Get git commit history for one or more files to understand their evolution and past changes",
|
|
1608
|
+
parameters: {
|
|
1609
|
+
type: "object",
|
|
1610
|
+
properties: {
|
|
1611
|
+
filePaths: {
|
|
1612
|
+
type: "array",
|
|
1613
|
+
description: "Array of file paths to get history for",
|
|
1614
|
+
items: { type: "string", description: "File path" }
|
|
1615
|
+
},
|
|
1616
|
+
limit: {
|
|
1617
|
+
type: "number",
|
|
1618
|
+
description: "Maximum number of commits to return (default: 10)",
|
|
1619
|
+
default: 10
|
|
1620
|
+
},
|
|
1621
|
+
format: {
|
|
1622
|
+
type: "string",
|
|
1623
|
+
description: 'Output format: "summary" for brief or "detailed" for full messages',
|
|
1624
|
+
enum: ["summary", "detailed"],
|
|
1625
|
+
default: "summary"
|
|
1626
|
+
}
|
|
1627
|
+
},
|
|
1628
|
+
required: ["filePaths"]
|
|
1629
|
+
},
|
|
1630
|
+
execute: async (params, context) => {
|
|
1631
|
+
const { filePaths, limit = 10, format = "summary" } = params;
|
|
1632
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1633
|
+
const formatArg = format === "detailed" ? "--format=%H%n%an (%ae)%n%ad%n%s%n%n%b%n---" : "--format=%h - %s (%an, %ar)";
|
|
1634
|
+
const fileArgs = filePaths.join(" ");
|
|
1635
|
+
const command = `git log ${formatArg} -n ${limit} -- ${fileArgs}`;
|
|
1636
|
+
try {
|
|
1637
|
+
const output = await run(command, { cwd: workingDir });
|
|
1638
|
+
return output.stdout || "No history found for specified files";
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
throw new Error(`Failed to get file history: ${error.message}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function createGetFileContentTool() {
|
|
1646
|
+
return {
|
|
1647
|
+
name: "get_file_content",
|
|
1648
|
+
description: "Get the complete current content of a file to understand context around changes",
|
|
1649
|
+
parameters: {
|
|
1650
|
+
type: "object",
|
|
1651
|
+
properties: {
|
|
1652
|
+
filePath: {
|
|
1653
|
+
type: "string",
|
|
1654
|
+
description: "Path to the file"
|
|
1655
|
+
},
|
|
1656
|
+
includeLineNumbers: {
|
|
1657
|
+
type: "boolean",
|
|
1658
|
+
description: "Include line numbers in output (default: false)",
|
|
1659
|
+
default: false
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
required: ["filePath"]
|
|
1663
|
+
},
|
|
1664
|
+
execute: async (params, context) => {
|
|
1665
|
+
const { filePath, includeLineNumbers = false } = params;
|
|
1666
|
+
const storage = context?.storage;
|
|
1667
|
+
if (!storage) {
|
|
1668
|
+
throw new Error("Storage adapter not available in context");
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
const content = await storage.readFile(filePath, "utf-8");
|
|
1672
|
+
if (includeLineNumbers) {
|
|
1673
|
+
const lines = content.split("\n");
|
|
1674
|
+
return lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
1675
|
+
}
|
|
1676
|
+
return content;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function createSearchCodebaseTool() {
|
|
1684
|
+
return {
|
|
1685
|
+
name: "search_codebase",
|
|
1686
|
+
description: "Search for code patterns, function names, or text across the codebase using git grep",
|
|
1687
|
+
parameters: {
|
|
1688
|
+
type: "object",
|
|
1689
|
+
properties: {
|
|
1690
|
+
query: {
|
|
1691
|
+
type: "string",
|
|
1692
|
+
description: "Search pattern (can be plain text or regex)"
|
|
1693
|
+
},
|
|
1694
|
+
fileTypes: {
|
|
1695
|
+
type: "array",
|
|
1696
|
+
description: 'Limit search to specific file extensions (e.g., ["ts", "js"])',
|
|
1697
|
+
items: { type: "string", description: "File extension" }
|
|
1698
|
+
},
|
|
1699
|
+
contextLines: {
|
|
1700
|
+
type: "number",
|
|
1701
|
+
description: "Number of context lines to show around matches (default: 2)",
|
|
1702
|
+
default: 2
|
|
1703
|
+
}
|
|
1704
|
+
},
|
|
1705
|
+
required: ["query"]
|
|
1706
|
+
},
|
|
1707
|
+
execute: async (params, context) => {
|
|
1708
|
+
const { query, fileTypes, contextLines = 2 } = params;
|
|
1709
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1710
|
+
let command = `git grep -n -C ${contextLines} "${query}"`;
|
|
1711
|
+
if (fileTypes && fileTypes.length > 0) {
|
|
1712
|
+
const patterns = fileTypes.map((ext) => `'*.${ext}'`).join(" ");
|
|
1713
|
+
command += ` -- ${patterns}`;
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const output = await run(command, { cwd: workingDir });
|
|
1717
|
+
return output.stdout || "No matches found";
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
if (error.message.includes("exit code 1")) {
|
|
1720
|
+
return "No matches found";
|
|
1721
|
+
}
|
|
1722
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function createGetRelatedTestsTool() {
|
|
1728
|
+
return {
|
|
1729
|
+
name: "get_related_tests",
|
|
1730
|
+
description: "Find test files related to production files to understand what the code is supposed to do",
|
|
1731
|
+
parameters: {
|
|
1732
|
+
type: "object",
|
|
1733
|
+
properties: {
|
|
1734
|
+
filePaths: {
|
|
1735
|
+
type: "array",
|
|
1736
|
+
description: "Production file paths",
|
|
1737
|
+
items: { type: "string", description: "File path" }
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
required: ["filePaths"]
|
|
1741
|
+
},
|
|
1742
|
+
execute: async (params, context) => {
|
|
1743
|
+
const { filePaths } = params;
|
|
1744
|
+
const storage = context?.storage;
|
|
1745
|
+
if (!storage) {
|
|
1746
|
+
throw new Error("Storage adapter not available in context");
|
|
1747
|
+
}
|
|
1748
|
+
const relatedTests = [];
|
|
1749
|
+
for (const filePath of filePaths) {
|
|
1750
|
+
const patterns = [
|
|
1751
|
+
filePath.replace(/\.(ts|js|tsx|jsx)$/, ".test.$1"),
|
|
1752
|
+
filePath.replace(/\.(ts|js|tsx|jsx)$/, ".spec.$1"),
|
|
1753
|
+
filePath.replace("/src/", "/tests/").replace("/lib/", "/tests/"),
|
|
1754
|
+
path.join("tests", filePath),
|
|
1755
|
+
path.join("test", filePath)
|
|
1756
|
+
];
|
|
1757
|
+
for (const pattern of patterns) {
|
|
1758
|
+
try {
|
|
1759
|
+
await storage.readFile(pattern, "utf-8");
|
|
1760
|
+
relatedTests.push(pattern);
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (relatedTests.length === 0) {
|
|
1766
|
+
return "No related test files found";
|
|
1767
|
+
}
|
|
1768
|
+
return `Found related test files:
|
|
1769
|
+
${relatedTests.join("\n")}`;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function createGetFileDependenciesTool() {
|
|
1774
|
+
return {
|
|
1775
|
+
name: "get_file_dependencies",
|
|
1776
|
+
description: "Find which files import or depend on the changed files to assess change impact",
|
|
1777
|
+
parameters: {
|
|
1778
|
+
type: "object",
|
|
1779
|
+
properties: {
|
|
1780
|
+
filePaths: {
|
|
1781
|
+
type: "array",
|
|
1782
|
+
description: "Files to analyze dependencies for",
|
|
1783
|
+
items: { type: "string", description: "File path" }
|
|
1784
|
+
}
|
|
1785
|
+
},
|
|
1786
|
+
required: ["filePaths"]
|
|
1787
|
+
},
|
|
1788
|
+
execute: async (params, context) => {
|
|
1789
|
+
const { filePaths } = params;
|
|
1790
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1791
|
+
const results = [];
|
|
1792
|
+
for (const filePath of filePaths) {
|
|
1793
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
1794
|
+
const searchPatterns = [
|
|
1795
|
+
`from.*['"].*${fileName}`,
|
|
1796
|
+
`import.*['"].*${fileName}`,
|
|
1797
|
+
`require\\(['"].*${fileName}`
|
|
1798
|
+
];
|
|
1799
|
+
for (const pattern of searchPatterns) {
|
|
1800
|
+
try {
|
|
1801
|
+
const command = `git grep -l "${pattern}"`;
|
|
1802
|
+
const output = await run(command, { cwd: workingDir });
|
|
1803
|
+
if (output.stdout) {
|
|
1804
|
+
results.push(`Files importing ${filePath}:
|
|
1805
|
+
${output.stdout}`);
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return results.length > 0 ? results.join("\n\n") : "No files found that import the specified files";
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function createAnalyzeDiffSectionTool() {
|
|
1817
|
+
return {
|
|
1818
|
+
name: "analyze_diff_section",
|
|
1819
|
+
description: "Get expanded context around specific lines in a file to better understand changes",
|
|
1820
|
+
parameters: {
|
|
1821
|
+
type: "object",
|
|
1822
|
+
properties: {
|
|
1823
|
+
filePath: {
|
|
1824
|
+
type: "string",
|
|
1825
|
+
description: "File containing the section to analyze"
|
|
1826
|
+
},
|
|
1827
|
+
startLine: {
|
|
1828
|
+
type: "number",
|
|
1829
|
+
description: "Starting line number"
|
|
1830
|
+
},
|
|
1831
|
+
endLine: {
|
|
1832
|
+
type: "number",
|
|
1833
|
+
description: "Ending line number"
|
|
1834
|
+
},
|
|
1835
|
+
contextLines: {
|
|
1836
|
+
type: "number",
|
|
1837
|
+
description: "Number of additional context lines to show before and after (default: 10)",
|
|
1838
|
+
default: 10
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
required: ["filePath", "startLine", "endLine"]
|
|
1842
|
+
},
|
|
1843
|
+
execute: async (params, context) => {
|
|
1844
|
+
const { filePath, startLine, endLine, contextLines = 10 } = params;
|
|
1845
|
+
const storage = context?.storage;
|
|
1846
|
+
if (!storage) {
|
|
1847
|
+
throw new Error("Storage adapter not available in context");
|
|
1848
|
+
}
|
|
1849
|
+
try {
|
|
1850
|
+
const content = await storage.readFile(filePath, "utf-8");
|
|
1851
|
+
const lines = content.split("\n");
|
|
1852
|
+
const actualStart = Math.max(0, startLine - contextLines - 1);
|
|
1853
|
+
const actualEnd = Math.min(lines.length, endLine + contextLines);
|
|
1854
|
+
const section = lines.slice(actualStart, actualEnd).map((line, idx) => `${actualStart + idx + 1}: ${line}`).join("\n");
|
|
1855
|
+
return `Lines ${actualStart + 1}-${actualEnd} from ${filePath}:
|
|
1856
|
+
|
|
1857
|
+
${section}`;
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
throw new Error(`Failed to analyze diff section: ${error.message}`);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function createGetRecentCommitsTool() {
|
|
1865
|
+
return {
|
|
1866
|
+
name: "get_recent_commits",
|
|
1867
|
+
description: "Get recent commits that modified the same files to understand recent work in this area",
|
|
1868
|
+
parameters: {
|
|
1869
|
+
type: "object",
|
|
1870
|
+
properties: {
|
|
1871
|
+
filePaths: {
|
|
1872
|
+
type: "array",
|
|
1873
|
+
description: "Files to check for recent commits",
|
|
1874
|
+
items: { type: "string", description: "File path" }
|
|
1875
|
+
},
|
|
1876
|
+
since: {
|
|
1877
|
+
type: "string",
|
|
1878
|
+
description: 'Time period to look back (e.g., "1 week ago", "2 days ago")',
|
|
1879
|
+
default: "1 week ago"
|
|
1880
|
+
},
|
|
1881
|
+
limit: {
|
|
1882
|
+
type: "number",
|
|
1883
|
+
description: "Maximum number of commits to return (default: 5)",
|
|
1884
|
+
default: 5
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
required: ["filePaths"]
|
|
1888
|
+
},
|
|
1889
|
+
execute: async (params, context) => {
|
|
1890
|
+
const { filePaths, since = "1 week ago", limit = 5 } = params;
|
|
1891
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1892
|
+
const fileArgs = filePaths.join(" ");
|
|
1893
|
+
const command = `git log --format="%h - %s (%an, %ar)" --since="${since}" -n ${limit} -- ${fileArgs}`;
|
|
1894
|
+
try {
|
|
1895
|
+
const output = await run(command, { cwd: workingDir });
|
|
1896
|
+
return output.stdout || `No commits found in the specified time period (${since})`;
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
throw new Error(`Failed to get recent commits: ${error.message}`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function createGroupFilesByConcernTool() {
|
|
1904
|
+
return {
|
|
1905
|
+
name: "group_files_by_concern",
|
|
1906
|
+
description: "Analyze changed files and suggest logical groupings that might represent separate concerns",
|
|
1907
|
+
parameters: {
|
|
1908
|
+
type: "object",
|
|
1909
|
+
properties: {
|
|
1910
|
+
filePaths: {
|
|
1911
|
+
type: "array",
|
|
1912
|
+
description: "All changed files to analyze",
|
|
1913
|
+
items: { type: "string", description: "File path" }
|
|
1914
|
+
}
|
|
1915
|
+
},
|
|
1916
|
+
required: ["filePaths"]
|
|
1917
|
+
},
|
|
1918
|
+
execute: async (params) => {
|
|
1919
|
+
const { filePaths } = params;
|
|
1920
|
+
const groups = {};
|
|
1921
|
+
for (const filePath of filePaths) {
|
|
1922
|
+
const dir = path.dirname(filePath);
|
|
1923
|
+
const ext = path.extname(filePath);
|
|
1924
|
+
const basename = path.basename(filePath, ext);
|
|
1925
|
+
let category = "other";
|
|
1926
|
+
if (basename.includes(".test") || basename.includes(".spec") || dir.includes("test")) {
|
|
1927
|
+
category = "tests";
|
|
1928
|
+
} else if (filePath.includes("package.json") || filePath.includes("package-lock.json")) {
|
|
1929
|
+
category = "dependencies";
|
|
1930
|
+
} else if (ext === ".md" || basename === "README") {
|
|
1931
|
+
category = "documentation";
|
|
1932
|
+
} else if (dir.includes("src") || dir.includes("lib")) {
|
|
1933
|
+
category = `source:${dir.split("/").slice(0, 3).join("/")}`;
|
|
1934
|
+
}
|
|
1935
|
+
if (!groups[category]) {
|
|
1936
|
+
groups[category] = [];
|
|
1937
|
+
}
|
|
1938
|
+
groups[category].push(filePath);
|
|
1939
|
+
}
|
|
1940
|
+
const output = Object.entries(groups).map(([category, files]) => {
|
|
1941
|
+
return `${category} (${files.length} files):
|
|
1942
|
+
${files.map((f) => ` - ${f}`).join("\n")}`;
|
|
1943
|
+
}).join("\n\n");
|
|
1944
|
+
const groupCount = Object.keys(groups).length;
|
|
1945
|
+
const suggestion = groupCount > 1 ? `
|
|
1946
|
+
|
|
1947
|
+
Suggestion: These ${groupCount} groups represent different concerns in the release.` : "\n\nSuggestion: All files appear to be related to a single concern.";
|
|
1948
|
+
return output + suggestion;
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
function createGetTagHistoryTool() {
|
|
1953
|
+
return {
|
|
1954
|
+
name: "get_tag_history",
|
|
1955
|
+
description: "Get the history of previous release tags to understand release patterns and versioning",
|
|
1956
|
+
parameters: {
|
|
1957
|
+
type: "object",
|
|
1958
|
+
properties: {
|
|
1959
|
+
limit: {
|
|
1960
|
+
type: "number",
|
|
1961
|
+
description: "Number of recent tags to retrieve (default: 10)",
|
|
1962
|
+
default: 10
|
|
1963
|
+
},
|
|
1964
|
+
pattern: {
|
|
1965
|
+
type: "string",
|
|
1966
|
+
description: 'Filter tags by pattern (e.g., "v*" for version tags)'
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
execute: async (params, context) => {
|
|
1971
|
+
const { limit = 10, pattern } = params;
|
|
1972
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1973
|
+
let command = "git tag --sort=-creatordate";
|
|
1974
|
+
if (pattern) {
|
|
1975
|
+
command += ` -l "${pattern}"`;
|
|
1976
|
+
}
|
|
1977
|
+
command += ` | head -n ${limit}`;
|
|
1978
|
+
try {
|
|
1979
|
+
const output = await run(command, { cwd: workingDir });
|
|
1980
|
+
if (!output.stdout) {
|
|
1981
|
+
return "No tags found in repository";
|
|
1982
|
+
}
|
|
1983
|
+
const tags = output.stdout.trim().split("\n");
|
|
1984
|
+
const detailedInfo = [];
|
|
1985
|
+
for (const tag of tags) {
|
|
1986
|
+
try {
|
|
1987
|
+
const tagInfo = await run(`git show --quiet --format="%ci - %s" ${tag}`, { cwd: workingDir });
|
|
1988
|
+
detailedInfo.push(`${tag}: ${tagInfo.stdout.trim()}`);
|
|
1989
|
+
} catch {
|
|
1990
|
+
detailedInfo.push(`${tag}: (no info available)`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return `Recent release tags:
|
|
1994
|
+
${detailedInfo.join("\n")}`;
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
throw new Error(`Failed to get tag history: ${error.message}`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function createComparePreviousReleaseTool() {
|
|
2002
|
+
return {
|
|
2003
|
+
name: "compare_previous_release",
|
|
2004
|
+
description: "Compare this release with a previous release to understand what changed between versions",
|
|
2005
|
+
parameters: {
|
|
2006
|
+
type: "object",
|
|
2007
|
+
properties: {
|
|
2008
|
+
previousTag: {
|
|
2009
|
+
type: "string",
|
|
2010
|
+
description: "Previous release tag to compare against"
|
|
2011
|
+
},
|
|
2012
|
+
currentRef: {
|
|
2013
|
+
type: "string",
|
|
2014
|
+
description: "Current reference (default: HEAD)",
|
|
2015
|
+
default: "HEAD"
|
|
2016
|
+
},
|
|
2017
|
+
statsOnly: {
|
|
2018
|
+
type: "boolean",
|
|
2019
|
+
description: "Return only statistics, not full diff (default: true)",
|
|
2020
|
+
default: true
|
|
2021
|
+
}
|
|
2022
|
+
},
|
|
2023
|
+
required: ["previousTag"]
|
|
2024
|
+
},
|
|
2025
|
+
execute: async (params, context) => {
|
|
2026
|
+
const { previousTag, currentRef = "HEAD", statsOnly = true } = params;
|
|
2027
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
2028
|
+
try {
|
|
2029
|
+
const commitCountCmd = `git rev-list --count ${previousTag}..${currentRef}`;
|
|
2030
|
+
const commitCount = await run(commitCountCmd, { cwd: workingDir });
|
|
2031
|
+
const statsCmd = `git diff --stat ${previousTag}..${currentRef}`;
|
|
2032
|
+
const stats = await run(statsCmd, { cwd: workingDir });
|
|
2033
|
+
let result = `Comparison between ${previousTag} and ${currentRef}:
|
|
2034
|
+
|
|
2035
|
+
`;
|
|
2036
|
+
result += `Commits: ${commitCount.stdout.trim()}
|
|
2037
|
+
|
|
2038
|
+
`;
|
|
2039
|
+
result += `File changes:
|
|
2040
|
+
${stats.stdout}`;
|
|
2041
|
+
if (!statsOnly) {
|
|
2042
|
+
const logCmd = `git log --oneline ${previousTag}..${currentRef}`;
|
|
2043
|
+
const log = await run(logCmd, { cwd: workingDir });
|
|
2044
|
+
result += `
|
|
2045
|
+
|
|
2046
|
+
Commit summary:
|
|
2047
|
+
${log.stdout}`;
|
|
2048
|
+
}
|
|
2049
|
+
return result;
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
throw new Error(`Failed to compare releases: ${error.message}`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function createGetReleaseStatsTool() {
|
|
2057
|
+
return {
|
|
2058
|
+
name: "get_release_stats",
|
|
2059
|
+
description: "Get comprehensive statistics about the release including contributors, file changes, and commit patterns",
|
|
2060
|
+
parameters: {
|
|
2061
|
+
type: "object",
|
|
2062
|
+
properties: {
|
|
2063
|
+
fromRef: {
|
|
2064
|
+
type: "string",
|
|
2065
|
+
description: "Starting reference for the release range"
|
|
2066
|
+
},
|
|
2067
|
+
toRef: {
|
|
2068
|
+
type: "string",
|
|
2069
|
+
description: "Ending reference for the release range (default: HEAD)",
|
|
2070
|
+
default: "HEAD"
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
required: ["fromRef"]
|
|
2074
|
+
},
|
|
2075
|
+
execute: async (params, context) => {
|
|
2076
|
+
const { fromRef, toRef = "HEAD" } = params;
|
|
2077
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
2078
|
+
try {
|
|
2079
|
+
const results = [];
|
|
2080
|
+
const commitCount = await run(`git rev-list --count ${fromRef}..${toRef}`, { cwd: workingDir });
|
|
2081
|
+
results.push(`Total commits: ${commitCount.stdout.trim()}`);
|
|
2082
|
+
const contributors = await run(
|
|
2083
|
+
`git shortlog -sn ${fromRef}..${toRef}`,
|
|
2084
|
+
{ cwd: workingDir }
|
|
2085
|
+
);
|
|
2086
|
+
results.push(`
|
|
2087
|
+
Contributors:
|
|
2088
|
+
${contributors.stdout}`);
|
|
2089
|
+
const fileStats = await run(
|
|
2090
|
+
`git diff --shortstat ${fromRef}..${toRef}`,
|
|
2091
|
+
{ cwd: workingDir }
|
|
2092
|
+
);
|
|
2093
|
+
results.push(`
|
|
2094
|
+
File changes: ${fileStats.stdout.trim()}`);
|
|
2095
|
+
const topFiles = await run(
|
|
2096
|
+
`git diff --stat ${fromRef}..${toRef} | sort -k2 -rn | head -n 10`,
|
|
2097
|
+
{ cwd: workingDir }
|
|
2098
|
+
);
|
|
2099
|
+
results.push(`
|
|
2100
|
+
Top 10 most changed files:
|
|
2101
|
+
${topFiles.stdout}`);
|
|
2102
|
+
return results.join("\n");
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
throw new Error(`Failed to get release stats: ${error.message}`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function createGetBreakingChangesTool() {
|
|
2110
|
+
return {
|
|
2111
|
+
name: "get_breaking_changes",
|
|
2112
|
+
description: "Search for potential breaking changes by looking for specific patterns in commits and diffs",
|
|
2113
|
+
parameters: {
|
|
2114
|
+
type: "object",
|
|
2115
|
+
properties: {
|
|
2116
|
+
fromRef: {
|
|
2117
|
+
type: "string",
|
|
2118
|
+
description: "Starting reference for the release range"
|
|
2119
|
+
},
|
|
2120
|
+
toRef: {
|
|
2121
|
+
type: "string",
|
|
2122
|
+
description: "Ending reference for the release range (default: HEAD)",
|
|
2123
|
+
default: "HEAD"
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
required: ["fromRef"]
|
|
2127
|
+
},
|
|
2128
|
+
execute: async (params, context) => {
|
|
2129
|
+
const { fromRef, toRef = "HEAD" } = params;
|
|
2130
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
2131
|
+
const results = [];
|
|
2132
|
+
try {
|
|
2133
|
+
const breakingCommits = await run(
|
|
2134
|
+
`git log --grep="BREAKING CHANGE" --oneline ${fromRef}..${toRef}`,
|
|
2135
|
+
{ cwd: workingDir }
|
|
2136
|
+
);
|
|
2137
|
+
if (breakingCommits.stdout) {
|
|
2138
|
+
results.push(`Commits with BREAKING CHANGE:
|
|
2139
|
+
${breakingCommits.stdout}`);
|
|
2140
|
+
}
|
|
2141
|
+
const removedExports = await run(
|
|
2142
|
+
`git diff ${fromRef}..${toRef} | grep "^-export"`,
|
|
2143
|
+
{ cwd: workingDir }
|
|
2144
|
+
);
|
|
2145
|
+
if (removedExports.stdout) {
|
|
2146
|
+
results.push(`
|
|
2147
|
+
Removed exports (potential breaking):
|
|
2148
|
+
${removedExports.stdout}`);
|
|
2149
|
+
}
|
|
2150
|
+
const changedSignatures = await run(
|
|
2151
|
+
`git diff ${fromRef}..${toRef} | grep -E "^[-+].*function|^[-+].*const.*=.*=>|^[-+].*interface|^[-+].*type.*="`,
|
|
2152
|
+
{ cwd: workingDir }
|
|
2153
|
+
);
|
|
2154
|
+
if (changedSignatures.stdout) {
|
|
2155
|
+
results.push(`
|
|
2156
|
+
Changed function/type signatures (review for breaking changes):
|
|
2157
|
+
${changedSignatures.stdout.split("\n").slice(0, 20).join("\n")}`);
|
|
2158
|
+
}
|
|
2159
|
+
if (results.length === 0) {
|
|
2160
|
+
return "No obvious breaking changes detected. Manual review recommended for API changes.";
|
|
2161
|
+
}
|
|
2162
|
+
return results.join("\n\n");
|
|
2163
|
+
} catch {
|
|
2164
|
+
if (results.length > 0) {
|
|
2165
|
+
return results.join("\n\n");
|
|
2166
|
+
}
|
|
2167
|
+
return "No obvious breaking changes detected. Manual review recommended for API changes.";
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function createAnalyzeCommitPatternsTool() {
|
|
2173
|
+
return {
|
|
2174
|
+
name: "analyze_commit_patterns",
|
|
2175
|
+
description: "Analyze commit messages to identify patterns and themes in the release",
|
|
2176
|
+
parameters: {
|
|
2177
|
+
type: "object",
|
|
2178
|
+
properties: {
|
|
2179
|
+
fromRef: {
|
|
2180
|
+
type: "string",
|
|
2181
|
+
description: "Starting reference for the release range"
|
|
2182
|
+
},
|
|
2183
|
+
toRef: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "Ending reference for the release range (default: HEAD)",
|
|
2186
|
+
default: "HEAD"
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
required: ["fromRef"]
|
|
2190
|
+
},
|
|
2191
|
+
execute: async (params, context) => {
|
|
2192
|
+
const { fromRef, toRef = "HEAD" } = params;
|
|
2193
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
2194
|
+
try {
|
|
2195
|
+
const results = [];
|
|
2196
|
+
const commits = await run(
|
|
2197
|
+
`git log --format="%s" ${fromRef}..${toRef}`,
|
|
2198
|
+
{ cwd: workingDir }
|
|
2199
|
+
);
|
|
2200
|
+
const messages = commits.stdout.trim().split("\n");
|
|
2201
|
+
const types = {};
|
|
2202
|
+
const keywords = {};
|
|
2203
|
+
for (const msg of messages) {
|
|
2204
|
+
const conventionalMatch = msg.match(/^(\w+)(\(.+?\))?:/);
|
|
2205
|
+
if (conventionalMatch) {
|
|
2206
|
+
const type = conventionalMatch[1];
|
|
2207
|
+
types[type] = (types[type] || 0) + 1;
|
|
2208
|
+
}
|
|
2209
|
+
const words = msg.toLowerCase().match(/\b\w{5,}\b/g) || [];
|
|
2210
|
+
for (const word of words) {
|
|
2211
|
+
keywords[word] = (keywords[word] || 0) + 1;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (Object.keys(types).length > 0) {
|
|
2215
|
+
results.push("Commit types:");
|
|
2216
|
+
const sortedTypes = Object.entries(types).sort((a, b) => b[1] - a[1]).map(([type, count]) => ` ${type}: ${count}`).join("\n");
|
|
2217
|
+
results.push(sortedTypes);
|
|
2218
|
+
}
|
|
2219
|
+
const topKeywords = Object.entries(keywords).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([word, count]) => ` ${word}: ${count}`).join("\n");
|
|
2220
|
+
if (topKeywords) {
|
|
2221
|
+
results.push("\nTop keywords in commits:");
|
|
2222
|
+
results.push(topKeywords);
|
|
2223
|
+
}
|
|
2224
|
+
return results.join("\n");
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
throw new Error(`Failed to analyze commit patterns: ${error.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async function runAgenticRelease(config) {
|
|
2232
|
+
const {
|
|
2233
|
+
fromRef,
|
|
2234
|
+
toRef,
|
|
2235
|
+
logContent,
|
|
2236
|
+
diffContent,
|
|
2237
|
+
milestoneIssues,
|
|
2238
|
+
releaseFocus,
|
|
2239
|
+
userContext,
|
|
2240
|
+
model = "gpt-4o",
|
|
2241
|
+
maxIterations = 30,
|
|
2242
|
+
debug = false,
|
|
2243
|
+
debugRequestFile,
|
|
2244
|
+
debugResponseFile,
|
|
2245
|
+
storage,
|
|
2246
|
+
logger: logger2,
|
|
2247
|
+
openaiReasoning
|
|
2248
|
+
} = config;
|
|
2249
|
+
const toolRegistry = createToolRegistry({
|
|
2250
|
+
workingDirectory: process.cwd(),
|
|
2251
|
+
storage,
|
|
2252
|
+
logger: logger2
|
|
2253
|
+
});
|
|
2254
|
+
const tools = createReleaseTools();
|
|
2255
|
+
toolRegistry.registerAll(tools);
|
|
2256
|
+
const systemPrompt = buildSystemPrompt();
|
|
2257
|
+
const userMessage = buildUserMessage({
|
|
2258
|
+
fromRef,
|
|
2259
|
+
toRef,
|
|
2260
|
+
logContent,
|
|
2261
|
+
diffContent,
|
|
2262
|
+
milestoneIssues,
|
|
2263
|
+
releaseFocus,
|
|
2264
|
+
userContext
|
|
2265
|
+
});
|
|
2266
|
+
const messages = [
|
|
2267
|
+
{ role: "system", content: systemPrompt },
|
|
2268
|
+
{ role: "user", content: userMessage }
|
|
2269
|
+
];
|
|
2270
|
+
const agenticConfig = {
|
|
2271
|
+
messages,
|
|
2272
|
+
tools: toolRegistry,
|
|
2273
|
+
model,
|
|
2274
|
+
maxIterations,
|
|
2275
|
+
debug,
|
|
2276
|
+
debugRequestFile,
|
|
2277
|
+
debugResponseFile,
|
|
2278
|
+
storage,
|
|
2279
|
+
logger: logger2,
|
|
2280
|
+
openaiReasoning
|
|
2281
|
+
};
|
|
2282
|
+
const result = await runAgentic(agenticConfig);
|
|
2283
|
+
const parsed = parseAgenticResult(result.finalMessage);
|
|
2284
|
+
return {
|
|
2285
|
+
releaseNotes: parsed.releaseNotes,
|
|
2286
|
+
iterations: result.iterations,
|
|
2287
|
+
toolCallsExecuted: result.toolCallsExecuted,
|
|
2288
|
+
conversationHistory: result.conversationHistory,
|
|
2289
|
+
toolMetrics: result.toolMetrics
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
function buildSystemPrompt() {
|
|
2293
|
+
return `You are an expert software engineer and technical writer tasked with generating comprehensive, thoughtful release notes.
|
|
2294
|
+
|
|
2295
|
+
You have access to tools that let you investigate the release in detail:
|
|
2296
|
+
- get_file_history: View commit history for specific files
|
|
2297
|
+
- get_file_content: Read full file contents to understand context
|
|
2298
|
+
- search_codebase: Search for patterns across the codebase
|
|
2299
|
+
- get_related_tests: Find test files to understand functionality
|
|
2300
|
+
- get_file_dependencies: Understand file dependencies and impact
|
|
2301
|
+
- analyze_diff_section: Get expanded context around specific changes
|
|
2302
|
+
- get_recent_commits: See recent commits to the same files
|
|
2303
|
+
- group_files_by_concern: Identify logical groupings of changes
|
|
2304
|
+
- get_tag_history: View previous release tags and patterns
|
|
2305
|
+
- compare_previous_release: Compare with previous releases
|
|
2306
|
+
- get_release_stats: Get comprehensive statistics about the release
|
|
2307
|
+
- get_breaking_changes: Identify potential breaking changes
|
|
2308
|
+
- analyze_commit_patterns: Identify themes and patterns in commits
|
|
2309
|
+
|
|
2310
|
+
Your process should be:
|
|
2311
|
+
1. Analyze the commit log and diff to understand the overall scope of changes
|
|
2312
|
+
2. Use tools strategically to investigate significant changes that need more context
|
|
2313
|
+
3. Look at previous releases to understand how this release fits into the project's evolution
|
|
2314
|
+
4. Identify patterns, themes, and connections between changes
|
|
2315
|
+
5. Check for breaking changes and significant architectural shifts
|
|
2316
|
+
6. Understand the "why" behind changes by examining commit messages, issues, and code
|
|
2317
|
+
7. Synthesize findings into comprehensive, thoughtful release notes
|
|
2318
|
+
|
|
2319
|
+
Guidelines:
|
|
2320
|
+
- Use tools strategically - focus on understanding significant changes
|
|
2321
|
+
- Look at test changes to understand intent and functionality
|
|
2322
|
+
- Check previous releases to provide context and compare scope
|
|
2323
|
+
- Identify patterns and themes across multiple commits
|
|
2324
|
+
- Consider the audience and what context they need
|
|
2325
|
+
- Be thorough and analytical, especially for large releases
|
|
2326
|
+
- Follow the release notes format and best practices provided
|
|
2327
|
+
|
|
2328
|
+
Output format:
|
|
2329
|
+
When you're ready to provide the final release notes, format them as JSON:
|
|
2330
|
+
|
|
2331
|
+
RELEASE_NOTES:
|
|
2332
|
+
{
|
|
2333
|
+
"title": "A concise, single-line title capturing the most significant changes",
|
|
2334
|
+
"body": "The detailed release notes in Markdown format, following best practices for structure, depth, and analysis"
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
The release notes should:
|
|
2338
|
+
- Demonstrate genuine understanding of the changes
|
|
2339
|
+
- Provide context and explain implications
|
|
2340
|
+
- Connect related changes to reveal patterns
|
|
2341
|
+
- Be substantial and analytical, not formulaic
|
|
2342
|
+
- Sound like they were written by a human who studied the changes
|
|
2343
|
+
- Be grounded in actual commits and issues (no hallucinations)`;
|
|
2344
|
+
}
|
|
2345
|
+
function buildUserMessage(params) {
|
|
2346
|
+
const { fromRef, toRef, logContent, diffContent, milestoneIssues, releaseFocus, userContext } = params;
|
|
2347
|
+
let message = `I need comprehensive release notes for changes from ${fromRef} to ${toRef}.
|
|
2348
|
+
|
|
2349
|
+
## Commit Log
|
|
2350
|
+
${logContent}
|
|
2351
|
+
|
|
2352
|
+
## Diff Summary
|
|
2353
|
+
${diffContent}`;
|
|
2354
|
+
if (milestoneIssues) {
|
|
2355
|
+
message += `
|
|
2356
|
+
|
|
2357
|
+
## Resolved Issues from Milestone
|
|
2358
|
+
${milestoneIssues}`;
|
|
2359
|
+
}
|
|
2360
|
+
if (releaseFocus) {
|
|
2361
|
+
message += `
|
|
2362
|
+
|
|
2363
|
+
## Release Focus
|
|
2364
|
+
${releaseFocus}
|
|
2365
|
+
|
|
2366
|
+
This is the PRIMARY GUIDE for how to frame and structure the release notes. Use this to determine emphasis and narrative.`;
|
|
2367
|
+
}
|
|
2368
|
+
if (userContext) {
|
|
2369
|
+
message += `
|
|
2370
|
+
|
|
2371
|
+
## Additional Context
|
|
2372
|
+
${userContext}`;
|
|
2373
|
+
}
|
|
2374
|
+
message += `
|
|
2375
|
+
|
|
2376
|
+
Please investigate these changes thoroughly and generate comprehensive release notes that:
|
|
2377
|
+
1. Demonstrate deep understanding of what changed and why
|
|
2378
|
+
2. Provide context about how changes relate to each other
|
|
2379
|
+
3. Explain implications for users and developers
|
|
2380
|
+
4. Connect this release to previous releases and project evolution
|
|
2381
|
+
5. Identify any breaking changes or significant architectural shifts
|
|
2382
|
+
6. Follow best practices for technical writing and release notes
|
|
2383
|
+
|
|
2384
|
+
Use the available tools to gather additional context as needed. Take your time to understand the changes deeply before writing the release notes.`;
|
|
2385
|
+
return message;
|
|
2386
|
+
}
|
|
2387
|
+
function parseAgenticResult(finalMessage) {
|
|
2388
|
+
const jsonMatch = finalMessage.match(/RELEASE_NOTES:\s*\n([\s\S]*)/);
|
|
2389
|
+
if (jsonMatch) {
|
|
2390
|
+
try {
|
|
2391
|
+
const jsonStr = jsonMatch[1].trim();
|
|
2392
|
+
const parsed = JSON.parse(jsonStr);
|
|
2393
|
+
if (parsed.title && parsed.body) {
|
|
2394
|
+
return {
|
|
2395
|
+
releaseNotes: {
|
|
2396
|
+
title: parsed.title,
|
|
2397
|
+
body: parsed.body
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
const lines = finalMessage.split("\n");
|
|
2405
|
+
let title = "";
|
|
2406
|
+
let body = "";
|
|
2407
|
+
let inBody = false;
|
|
2408
|
+
for (const line of lines) {
|
|
2409
|
+
if (!title && line.trim() && !line.startsWith("#")) {
|
|
2410
|
+
title = line.trim();
|
|
2411
|
+
} else if (title && line.trim()) {
|
|
2412
|
+
inBody = true;
|
|
2413
|
+
}
|
|
2414
|
+
if (inBody) {
|
|
2415
|
+
body += line + "\n";
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
if (!title || !body) {
|
|
2419
|
+
title = "Release Notes";
|
|
2420
|
+
body = finalMessage;
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
releaseNotes: {
|
|
2424
|
+
title: title.trim(),
|
|
2425
|
+
body: body.trim()
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
798
2429
|
export {
|
|
2430
|
+
AgenticExecutor,
|
|
799
2431
|
OpenAIError,
|
|
800
2432
|
STANDARD_CHOICES,
|
|
801
2433
|
SecureTempFile,
|
|
2434
|
+
ToolRegistry,
|
|
802
2435
|
cleanupTempFile,
|
|
803
2436
|
createCommitPrompt,
|
|
2437
|
+
createCommitTools,
|
|
804
2438
|
createCompletion,
|
|
805
2439
|
createCompletionWithRetry,
|
|
806
2440
|
createNoOpLogger,
|
|
807
2441
|
createReleasePrompt,
|
|
2442
|
+
createReleaseTools,
|
|
808
2443
|
createReviewPrompt,
|
|
809
2444
|
createSecureTempFile,
|
|
2445
|
+
createToolRegistry,
|
|
810
2446
|
editContentInEditor,
|
|
811
2447
|
getLLMFeedbackInEditor,
|
|
812
2448
|
getLogger,
|
|
@@ -817,6 +2453,9 @@ export {
|
|
|
817
2453
|
isRateLimitError,
|
|
818
2454
|
isTokenLimitError,
|
|
819
2455
|
requireTTY,
|
|
2456
|
+
runAgentic,
|
|
2457
|
+
runAgenticCommit,
|
|
2458
|
+
runAgenticRelease,
|
|
820
2459
|
setLogger,
|
|
821
2460
|
transcribeAudio,
|
|
822
2461
|
tryLoadWinston
|