@eldrforge/ai-service 0.1.10 → 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/dist/index.js CHANGED
@@ -1082,17 +1082,17 @@ function createToolRegistry(context) {
1082
1082
  }
1083
1083
  function createCommitTools() {
1084
1084
  return [
1085
- createGetFileHistoryTool(),
1086
- createGetFileContentTool(),
1087
- createSearchCodebaseTool(),
1088
- createGetRelatedTestsTool(),
1089
- createGetFileDependenciesTool(),
1090
- createAnalyzeDiffSectionTool(),
1091
- createGetRecentCommitsTool(),
1092
- createGroupFilesByConcernTool()
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
1093
  ];
1094
1094
  }
1095
- function createGetFileHistoryTool() {
1095
+ function createGetFileHistoryTool$1() {
1096
1096
  return {
1097
1097
  name: "get_file_history",
1098
1098
  description: "Get git commit history for one or more files to understand their evolution and past changes",
@@ -1133,7 +1133,7 @@ function createGetFileHistoryTool() {
1133
1133
  }
1134
1134
  };
1135
1135
  }
1136
- function createGetFileContentTool() {
1136
+ function createGetFileContentTool$1() {
1137
1137
  return {
1138
1138
  name: "get_file_content",
1139
1139
  description: "Get the complete current content of a file to understand context around changes",
@@ -1171,7 +1171,7 @@ function createGetFileContentTool() {
1171
1171
  }
1172
1172
  };
1173
1173
  }
1174
- function createSearchCodebaseTool() {
1174
+ function createSearchCodebaseTool$1() {
1175
1175
  return {
1176
1176
  name: "search_codebase",
1177
1177
  description: "Search for code patterns, function names, or text across the codebase using git grep",
@@ -1215,7 +1215,7 @@ function createSearchCodebaseTool() {
1215
1215
  }
1216
1216
  };
1217
1217
  }
1218
- function createGetRelatedTestsTool() {
1218
+ function createGetRelatedTestsTool$1() {
1219
1219
  return {
1220
1220
  name: "get_related_tests",
1221
1221
  description: "Find test files related to production files to understand what the code is supposed to do",
@@ -1261,7 +1261,7 @@ ${relatedTests.join("\n")}`;
1261
1261
  }
1262
1262
  };
1263
1263
  }
1264
- function createGetFileDependenciesTool() {
1264
+ function createGetFileDependenciesTool$1() {
1265
1265
  return {
1266
1266
  name: "get_file_dependencies",
1267
1267
  description: "Find which files import or depend on the changed files to assess change impact",
@@ -1304,7 +1304,7 @@ ${output.stdout}`);
1304
1304
  }
1305
1305
  };
1306
1306
  }
1307
- function createAnalyzeDiffSectionTool() {
1307
+ function createAnalyzeDiffSectionTool$1() {
1308
1308
  return {
1309
1309
  name: "analyze_diff_section",
1310
1310
  description: "Get expanded context around specific lines in a file to better understand changes",
@@ -1352,7 +1352,7 @@ ${section}`;
1352
1352
  }
1353
1353
  };
1354
1354
  }
1355
- function createGetRecentCommitsTool() {
1355
+ function createGetRecentCommitsTool$1() {
1356
1356
  return {
1357
1357
  name: "get_recent_commits",
1358
1358
  description: "Get recent commits that modified the same files to understand recent work in this area",
@@ -1391,7 +1391,7 @@ function createGetRecentCommitsTool() {
1391
1391
  }
1392
1392
  };
1393
1393
  }
1394
- function createGroupFilesByConcernTool() {
1394
+ function createGroupFilesByConcernTool$1() {
1395
1395
  return {
1396
1396
  name: "group_files_by_concern",
1397
1397
  description: "Analyze changed files and suggest logical groupings that might represent separate commits",
@@ -1462,8 +1462,8 @@ async function runAgenticCommit(config) {
1462
1462
  });
1463
1463
  const tools = createCommitTools();
1464
1464
  toolRegistry.registerAll(tools);
1465
- const systemPrompt = buildSystemPrompt();
1466
- const userMessage = buildUserMessage(changedFiles, diffContent, userDirection, logContext);
1465
+ const systemPrompt = buildSystemPrompt$1();
1466
+ const userMessage = buildUserMessage$1(changedFiles, diffContent, userDirection, logContext);
1467
1467
  const messages = [
1468
1468
  { role: "system", content: systemPrompt },
1469
1469
  { role: "user", content: userMessage }
@@ -1481,7 +1481,7 @@ async function runAgenticCommit(config) {
1481
1481
  openaiReasoning
1482
1482
  };
1483
1483
  const result = await runAgentic(agenticConfig);
1484
- const parsed = parseAgenticResult(result.finalMessage);
1484
+ const parsed = parseAgenticResult$1(result.finalMessage);
1485
1485
  return {
1486
1486
  commitMessage: parsed.commitMessage,
1487
1487
  iterations: result.iterations,
@@ -1491,7 +1491,7 @@ async function runAgenticCommit(config) {
1491
1491
  toolMetrics: result.toolMetrics
1492
1492
  };
1493
1493
  }
1494
- function buildSystemPrompt() {
1494
+ function buildSystemPrompt$1() {
1495
1495
  return `You are an expert software engineer tasked with generating meaningful commit messages.
1496
1496
 
1497
1497
  You have access to tools that let you investigate changes in detail:
@@ -1537,7 +1537,7 @@ Split 2:
1537
1537
 
1538
1538
  If changes should remain as one commit, do not include SUGGESTED_SPLITS section.`;
1539
1539
  }
1540
- function buildUserMessage(changedFiles, diffContent, userDirection, logContext) {
1540
+ function buildUserMessage$1(changedFiles, diffContent, userDirection, logContext) {
1541
1541
  let message = `I have staged changes that need a commit message.
1542
1542
 
1543
1543
  Changed files (${changedFiles.length}):
@@ -1563,7 +1563,7 @@ Use the available tools to gather additional context as needed.
1563
1563
  If these changes should be split into multiple commits, suggest the groupings.`;
1564
1564
  return message;
1565
1565
  }
1566
- function parseAgenticResult(finalMessage) {
1566
+ function parseAgenticResult$1(finalMessage) {
1567
1567
  const commitMatch = finalMessage.match(/COMMIT_MESSAGE:\s*\n([\s\S]*?)(?=\n\nSUGGESTED_SPLITS:|$)/);
1568
1568
  const commitMessage = commitMatch ? commitMatch[1].trim() : finalMessage.trim();
1569
1569
  const suggestedSplits = [];
@@ -1584,6 +1584,848 @@ function parseAgenticResult(finalMessage) {
1584
1584
  }
1585
1585
  return { commitMessage, suggestedSplits };
1586
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
+ }
1587
2429
  export {
1588
2430
  AgenticExecutor,
1589
2431
  OpenAIError,
@@ -1597,6 +2439,7 @@ export {
1597
2439
  createCompletionWithRetry,
1598
2440
  createNoOpLogger,
1599
2441
  createReleasePrompt,
2442
+ createReleaseTools,
1600
2443
  createReviewPrompt,
1601
2444
  createSecureTempFile,
1602
2445
  createToolRegistry,
@@ -1612,6 +2455,7 @@ export {
1612
2455
  requireTTY,
1613
2456
  runAgentic,
1614
2457
  runAgenticCommit,
2458
+ runAgenticRelease,
1615
2459
  setLogger,
1616
2460
  transcribeAudio,
1617
2461
  tryLoadWinston