@eldrforge/ai-service 0.1.16 → 0.1.17
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/LICENSE +1 -1
- package/README.md +7 -7
- package/dist/index.js +200 -36
- package/dist/index.js.map +1 -1
- package/dist/src/tools/commit-tools.d.ts.map +1 -1
- package/examples/README.md +3 -3
- package/package.json +4 -4
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ AI-powered content generation library with agentic capabilities for commit messa
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`@eldrforge/ai-service` is a TypeScript library that provides intelligent content generation powered by OpenAI's GPT models. It was extracted from the [kodrdriv](https://github.com/
|
|
7
|
+
`@eldrforge/ai-service` is a TypeScript library that provides intelligent content generation powered by OpenAI's GPT models. It was extracted from the [kodrdriv](https://github.com/grunnverk/kodrdriv) automation tool to enable standalone use in any Node.js application.
|
|
8
8
|
|
|
9
9
|
### Key Features
|
|
10
10
|
|
|
@@ -1198,12 +1198,12 @@ Monitor usage with the `toolMetrics` data.
|
|
|
1198
1198
|
|
|
1199
1199
|
## Contributing
|
|
1200
1200
|
|
|
1201
|
-
Contributions are welcome! This library was extracted from [kodrdriv](https://github.com/
|
|
1201
|
+
Contributions are welcome! This library was extracted from [kodrdriv](https://github.com/grunnverk/kodrdriv).
|
|
1202
1202
|
|
|
1203
1203
|
### Development Setup
|
|
1204
1204
|
|
|
1205
1205
|
```bash
|
|
1206
|
-
git clone https://github.com/
|
|
1206
|
+
git clone https://github.com/grunnverk/ai-service.git
|
|
1207
1207
|
cd ai-service
|
|
1208
1208
|
npm install
|
|
1209
1209
|
npm run build
|
|
@@ -1224,15 +1224,15 @@ Apache-2.0
|
|
|
1224
1224
|
|
|
1225
1225
|
## Related Projects
|
|
1226
1226
|
|
|
1227
|
-
- **[kodrdriv](https://github.com/
|
|
1227
|
+
- **[kodrdriv](https://github.com/grunnverk/kodrdriv)** - Full automation toolkit that uses this library
|
|
1228
1228
|
- **[@eldrforge/git-tools](https://www.npmjs.com/package/@eldrforge/git-tools)** - Git utility functions
|
|
1229
1229
|
- **[@riotprompt/riotprompt](https://www.npmjs.com/package/@riotprompt/riotprompt)** - Structured prompt builder
|
|
1230
1230
|
|
|
1231
1231
|
## Support
|
|
1232
1232
|
|
|
1233
|
-
- 📖 [Full Documentation](https://github.com/
|
|
1234
|
-
- 🐛 [Issue Tracker](https://github.com/
|
|
1235
|
-
- 💬 [Discussions](https://github.com/
|
|
1233
|
+
- 📖 [Full Documentation](https://github.com/grunnverk/ai-service)
|
|
1234
|
+
- 🐛 [Issue Tracker](https://github.com/grunnverk/ai-service/issues)
|
|
1235
|
+
- 💬 [Discussions](https://github.com/grunnverk/ai-service/discussions)
|
|
1236
1236
|
|
|
1237
1237
|
## Changelog
|
|
1238
1238
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OpenAI } from "openai";
|
|
2
2
|
import { safeJsonParse, run, localBranchExists, isBranchInSyncWithRemote, safeSyncBranchWithRemote } from "@eldrforge/git-tools";
|
|
3
|
-
import fs$1 from "fs";
|
|
3
|
+
import * as fs$1 from "fs";
|
|
4
|
+
import fs__default from "fs";
|
|
4
5
|
import { spawnSync } from "child_process";
|
|
5
6
|
import * as path from "path";
|
|
6
7
|
import path__default from "path";
|
|
@@ -296,7 +297,7 @@ async function transcribeAudio(filePath, options = { model: "whisper-1" }) {
|
|
|
296
297
|
await options.storage.writeTemp(debugFile, JSON.stringify(requestData, null, 2));
|
|
297
298
|
logger2.debug("Wrote request debug file to %s", debugFile);
|
|
298
299
|
}
|
|
299
|
-
audioStream =
|
|
300
|
+
audioStream = fs__default.createReadStream(filePath);
|
|
300
301
|
if (audioStream && typeof audioStream.on === "function") {
|
|
301
302
|
audioStream.on("error", (streamError) => {
|
|
302
303
|
logger2.error("Audio stream error: %s", streamError.message);
|
|
@@ -1143,7 +1144,8 @@ function createCommitTools() {
|
|
|
1143
1144
|
createGetFileDependenciesTool$1(),
|
|
1144
1145
|
createAnalyzeDiffSectionTool$1(),
|
|
1145
1146
|
createGetRecentCommitsTool$1(),
|
|
1146
|
-
createGroupFilesByConcernTool$1()
|
|
1147
|
+
createGroupFilesByConcernTool$1(),
|
|
1148
|
+
createGetFileModificationTimesTool()
|
|
1147
1149
|
];
|
|
1148
1150
|
}
|
|
1149
1151
|
function createGetFileHistoryTool$1() {
|
|
@@ -1562,6 +1564,115 @@ Suggestion: These ${groupCount} groups might be better as separate commits if th
|
|
|
1562
1564
|
}
|
|
1563
1565
|
};
|
|
1564
1566
|
}
|
|
1567
|
+
function createGetFileModificationTimesTool() {
|
|
1568
|
+
return {
|
|
1569
|
+
name: "get_file_modification_times",
|
|
1570
|
+
description: "Get modification timestamps for changed files. Temporal proximity is one signal (among others) that can help identify related changes. Files modified close together MAY be part of the same logical change, but this is not guaranteed - always cross-reference with logical groupings.",
|
|
1571
|
+
category: "Organization",
|
|
1572
|
+
cost: "cheap",
|
|
1573
|
+
examples: [
|
|
1574
|
+
{
|
|
1575
|
+
scenario: "Identify files modified together vs at different times",
|
|
1576
|
+
params: { filePaths: ["src/auth.ts", "src/user.ts", "README.md", "tests/auth.test.ts"] },
|
|
1577
|
+
expectedResult: "Files with timestamps grouped by temporal proximity"
|
|
1578
|
+
}
|
|
1579
|
+
],
|
|
1580
|
+
parameters: {
|
|
1581
|
+
type: "object",
|
|
1582
|
+
properties: {
|
|
1583
|
+
filePaths: {
|
|
1584
|
+
type: "array",
|
|
1585
|
+
description: "All changed files to analyze",
|
|
1586
|
+
items: { type: "string", description: "File path" }
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
required: ["filePaths"]
|
|
1590
|
+
},
|
|
1591
|
+
execute: async (params, context) => {
|
|
1592
|
+
const { filePaths } = params;
|
|
1593
|
+
const workingDir = context?.workingDirectory || process.cwd();
|
|
1594
|
+
const fileInfos = [];
|
|
1595
|
+
const errors = [];
|
|
1596
|
+
for (const filePath of filePaths) {
|
|
1597
|
+
try {
|
|
1598
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(workingDir, filePath);
|
|
1599
|
+
const stat = fs$1.statSync(fullPath);
|
|
1600
|
+
fileInfos.push({
|
|
1601
|
+
file: filePath,
|
|
1602
|
+
mtime: stat.mtime,
|
|
1603
|
+
mtimeMs: stat.mtimeMs
|
|
1604
|
+
});
|
|
1605
|
+
} catch {
|
|
1606
|
+
errors.push(filePath);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
fileInfos.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1610
|
+
const CLUSTER_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
1611
|
+
const clusters = [];
|
|
1612
|
+
let currentCluster = [];
|
|
1613
|
+
for (const info of fileInfos) {
|
|
1614
|
+
if (currentCluster.length === 0) {
|
|
1615
|
+
currentCluster.push(info);
|
|
1616
|
+
} else {
|
|
1617
|
+
const lastFile = currentCluster[currentCluster.length - 1];
|
|
1618
|
+
if (info.mtimeMs - lastFile.mtimeMs <= CLUSTER_THRESHOLD_MS) {
|
|
1619
|
+
currentCluster.push(info);
|
|
1620
|
+
} else {
|
|
1621
|
+
clusters.push(currentCluster);
|
|
1622
|
+
currentCluster = [info];
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (currentCluster.length > 0) {
|
|
1627
|
+
clusters.push(currentCluster);
|
|
1628
|
+
}
|
|
1629
|
+
let output = `## File Modification Times (sorted oldest to newest)
|
|
1630
|
+
|
|
1631
|
+
`;
|
|
1632
|
+
for (const info of fileInfos) {
|
|
1633
|
+
const timeStr = info.mtime.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
1634
|
+
output += `${timeStr} ${info.file}
|
|
1635
|
+
`;
|
|
1636
|
+
}
|
|
1637
|
+
if (errors.length > 0) {
|
|
1638
|
+
output += `
|
|
1639
|
+
## Files not found (possibly deleted):
|
|
1640
|
+
`;
|
|
1641
|
+
output += errors.map((f) => ` - ${f}`).join("\n");
|
|
1642
|
+
}
|
|
1643
|
+
output += `
|
|
1644
|
+
|
|
1645
|
+
## Temporal Clusters (files modified within 10 minutes of each other)
|
|
1646
|
+
|
|
1647
|
+
`;
|
|
1648
|
+
if (clusters.length === 1) {
|
|
1649
|
+
output += `All ${fileInfos.length} files were modified in a single work session.
|
|
1650
|
+
`;
|
|
1651
|
+
} else {
|
|
1652
|
+
output += `Found ${clusters.length} distinct work sessions:
|
|
1653
|
+
|
|
1654
|
+
`;
|
|
1655
|
+
clusters.forEach((cluster, idx) => {
|
|
1656
|
+
const startTime = cluster[0].mtime.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
1657
|
+
const endTime = cluster[cluster.length - 1].mtime.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
1658
|
+
const durationMs = cluster[cluster.length - 1].mtimeMs - cluster[0].mtimeMs;
|
|
1659
|
+
const durationMins = Math.round(durationMs / 6e4);
|
|
1660
|
+
output += `### Session ${idx + 1} (${cluster.length} files, ${durationMins} min span)
|
|
1661
|
+
`;
|
|
1662
|
+
output += `Time: ${startTime} to ${endTime}
|
|
1663
|
+
`;
|
|
1664
|
+
output += `Files:
|
|
1665
|
+
`;
|
|
1666
|
+
output += cluster.map((f) => ` - ${f.file}`).join("\n");
|
|
1667
|
+
output += "\n\n";
|
|
1668
|
+
});
|
|
1669
|
+
output += `
|
|
1670
|
+
**Note**: These ${clusters.length} temporal clusters may indicate separate work sessions. Cross-reference with logical groupings to determine if they represent distinct changes worth splitting.`;
|
|
1671
|
+
}
|
|
1672
|
+
return output;
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1565
1676
|
async function runAgenticCommit(config) {
|
|
1566
1677
|
const {
|
|
1567
1678
|
changedFiles,
|
|
@@ -1633,32 +1744,70 @@ ${toolGuidance}
|
|
|
1633
1744
|
|
|
1634
1745
|
## Your Task
|
|
1635
1746
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
Think about:
|
|
1639
|
-
- What problem does this solve?
|
|
1640
|
-
- How do the changes work together?
|
|
1641
|
-
- What should reviewers focus on?
|
|
1642
|
-
- Are there any important implications?
|
|
1747
|
+
Analyze the staged changes and determine the best way to commit them. Your primary goal is to create **meaningful, atomic commits** that each represent a single logical change.
|
|
1643
1748
|
|
|
1644
|
-
|
|
1749
|
+
**CRITICAL**: When there are many changed files, especially after a long work session (multiple hours), you should almost always split them into multiple commits. A single commit with 10+ files usually indicates multiple distinct changes that should be separated.
|
|
1645
1750
|
|
|
1646
|
-
|
|
1751
|
+
Think about:
|
|
1752
|
+
- What distinct features, fixes, or improvements are represented?
|
|
1753
|
+
- Are there natural groupings by functionality, module, or purpose?
|
|
1754
|
+
- When were files modified relative to each other?
|
|
1755
|
+
- What should reviewers focus on in each commit?
|
|
1756
|
+
|
|
1757
|
+
## Investigation Strategy
|
|
1758
|
+
|
|
1759
|
+
For any non-trivial set of changes, gather multiple signals to understand how to group commits:
|
|
1760
|
+
|
|
1761
|
+
1. **Check file modification times** using \`get_file_modification_times\`
|
|
1762
|
+
- This reveals *when* files were changed relative to each other
|
|
1763
|
+
- Files modified close together *may* be part of the same logical change
|
|
1764
|
+
- Large temporal gaps *can* indicate separate work sessions
|
|
1765
|
+
- Note: temporal proximity is one signal, not a guarantee of relatedness
|
|
1766
|
+
|
|
1767
|
+
2. **Understand logical relationships** using \`group_files_by_concern\`
|
|
1768
|
+
- Groups by module, type (tests, docs, source), and directory structure
|
|
1769
|
+
- Reveals which files are functionally related regardless of when they were modified
|
|
1770
|
+
|
|
1771
|
+
3. **Cross-reference both signals** to make informed decisions:
|
|
1772
|
+
- Files modified together AND logically related → strong candidate for same commit
|
|
1773
|
+
- Files modified apart BUT logically related → might still belong together
|
|
1774
|
+
- Files modified together BUT logically unrelated → consider splitting
|
|
1775
|
+
- Use your judgment - neither signal is definitive on its own
|
|
1776
|
+
|
|
1777
|
+
4. **Investigate further** as needed:
|
|
1778
|
+
- get_file_content - see full context when diffs are unclear
|
|
1779
|
+
- get_file_history - understand how code evolved
|
|
1780
|
+
- get_file_dependencies - assess impact of changes
|
|
1781
|
+
- get_recent_commits - avoid duplicate messages
|
|
1782
|
+
- get_related_tests - understand behavior changes
|
|
1783
|
+
- search_codebase - find usage patterns
|
|
1784
|
+
|
|
1785
|
+
## When to Split Commits
|
|
1786
|
+
|
|
1787
|
+
**Prefer multiple commits when:**
|
|
1788
|
+
- Changes span multiple hours of work (check modification times!)
|
|
1789
|
+
- Different logical features or fixes are included
|
|
1790
|
+
- Test changes could be separated from production code changes
|
|
1791
|
+
- Documentation updates are unrelated to code changes
|
|
1792
|
+
- Refactoring is mixed with feature work
|
|
1793
|
+
- Configuration changes are mixed with implementation changes
|
|
1794
|
+
- Files in different modules/packages are changed for different reasons
|
|
1795
|
+
|
|
1796
|
+
**Keep as one commit when:**
|
|
1797
|
+
- All changes are part of a single, cohesive feature
|
|
1798
|
+
- Files were modified together in a focused work session
|
|
1799
|
+
- Test changes directly test the production code changes
|
|
1800
|
+
- The changes tell a single coherent story
|
|
1801
|
+
|
|
1802
|
+
**Default bias**: When in doubt about whether to split, **prefer splitting**. It's better to have too many focused commits than too few bloated ones. A good commit should be understandable and reviewable in isolation.
|
|
1803
|
+
|
|
1804
|
+
## Important Context Guidelines
|
|
1805
|
+
|
|
1806
|
+
If additional context is provided (from context files or other sources), use your judgment:
|
|
1647
1807
|
- If the context is relevant to these specific changes, incorporate it
|
|
1648
1808
|
- If the context describes unrelated changes or other packages, ignore it
|
|
1649
1809
|
- Don't force connections between unrelated information
|
|
1650
|
-
- Focus on accurately describing what actually changed
|
|
1651
|
-
|
|
1652
|
-
## Investigation Approach
|
|
1653
|
-
|
|
1654
|
-
Use tools based on what you need to know:
|
|
1655
|
-
- group_files_by_concern - understand how files relate
|
|
1656
|
-
- get_file_content - see full context when diffs are unclear
|
|
1657
|
-
- get_file_history - understand how code evolved
|
|
1658
|
-
- get_file_dependencies - assess impact of changes
|
|
1659
|
-
- get_recent_commits - avoid duplicate messages
|
|
1660
|
-
- get_related_tests - understand behavior changes
|
|
1661
|
-
- search_codebase - find usage patterns
|
|
1810
|
+
- Focus on accurately describing what actually changed
|
|
1662
1811
|
|
|
1663
1812
|
## Writing Style
|
|
1664
1813
|
|
|
@@ -1677,25 +1826,32 @@ Follow conventional commit format when appropriate (feat:, fix:, refactor:, docs
|
|
|
1677
1826
|
When ready, format your response as:
|
|
1678
1827
|
|
|
1679
1828
|
COMMIT_MESSAGE:
|
|
1680
|
-
[Your commit message here]
|
|
1829
|
+
[Your commit message here - only for the FIRST group of changes if splitting]
|
|
1681
1830
|
|
|
1682
|
-
If changes should be split into multiple commits:
|
|
1831
|
+
If changes should be split into multiple commits (which is often the case with large changesets):
|
|
1683
1832
|
|
|
1684
1833
|
SUGGESTED_SPLITS:
|
|
1685
1834
|
Split 1:
|
|
1686
1835
|
Files: [list of files]
|
|
1687
|
-
Rationale: [why these belong together]
|
|
1836
|
+
Rationale: [why these belong together - mention temporal clustering and/or logical relationship]
|
|
1688
1837
|
Message: [commit message for this split]
|
|
1689
1838
|
|
|
1690
1839
|
Split 2:
|
|
1840
|
+
Files: [list of files]
|
|
1841
|
+
Rationale: [why these belong together]
|
|
1842
|
+
Message: [commit message for this split]
|
|
1843
|
+
|
|
1844
|
+
Split 3:
|
|
1691
1845
|
...
|
|
1692
1846
|
|
|
1693
1847
|
Output only the commit message and splits. No conversational remarks or follow-up offers.`;
|
|
1694
1848
|
}
|
|
1695
1849
|
function buildUserMessage$1(changedFiles, diffContent, userDirection, logContext) {
|
|
1850
|
+
const fileCount = changedFiles.length;
|
|
1851
|
+
const manyFiles = fileCount >= 5;
|
|
1696
1852
|
let message = `I have staged changes that need a commit message.
|
|
1697
1853
|
|
|
1698
|
-
Changed files (${
|
|
1854
|
+
Changed files (${fileCount}):
|
|
1699
1855
|
${changedFiles.map((f) => ` - ${f}`).join("\n")}
|
|
1700
1856
|
|
|
1701
1857
|
Diff:
|
|
@@ -1713,16 +1869,24 @@ ${logContext}`;
|
|
|
1713
1869
|
}
|
|
1714
1870
|
message += `
|
|
1715
1871
|
|
|
1716
|
-
|
|
1717
|
-
- What problem does this solve?
|
|
1718
|
-
- How do the changes work together?
|
|
1719
|
-
- Should this be one commit or multiple?
|
|
1872
|
+
## Your Analysis Task
|
|
1720
1873
|
|
|
1721
|
-
|
|
1722
|
-
Don't force connections that don't exist - if context doesn't apply to this package,
|
|
1723
|
-
simply ignore it and focus on the actual changes.
|
|
1874
|
+
${manyFiles ? `With ${fileCount} files changed, consider whether these represent multiple distinct changes that should be split into separate commits.
|
|
1724
1875
|
|
|
1725
|
-
|
|
1876
|
+
` : ""}Gather signals to understand the changes:
|
|
1877
|
+
1. Use \`get_file_modification_times\` to see when files were modified relative to each other
|
|
1878
|
+
2. Use \`group_files_by_concern\` to understand how files relate by type/purpose
|
|
1879
|
+
3. Cross-reference both signals - temporal proximity and logical relatedness - to determine the best commit structure
|
|
1880
|
+
|
|
1881
|
+
Consider:
|
|
1882
|
+
- What distinct features, fixes, or improvements are included?
|
|
1883
|
+
- Are files that were modified together also logically related?
|
|
1884
|
+
- Would separate commits make the history more understandable?
|
|
1885
|
+
|
|
1886
|
+
${manyFiles ? "With many files, consider whether multiple focused commits would be clearer than one large commit." : "If changes represent multiple logical concerns, suggest splits."}
|
|
1887
|
+
|
|
1888
|
+
If context information is provided, use it only if relevant to these specific changes.
|
|
1889
|
+
Don't force connections that don't exist - focus on what actually changed.`;
|
|
1726
1890
|
return message;
|
|
1727
1891
|
}
|
|
1728
1892
|
function parseAgenticResult$1(finalMessage) {
|