@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 CHANGED
@@ -175,7 +175,7 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2025 Calen Varek
178
+ Copyright 2025 Tim O'Brien
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
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/calenvarek/kodrdriv) automation tool to enable standalone use in any Node.js application.
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/calenvarek/kodrdriv).
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/calenvarek/ai-service.git
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/calenvarek/kodrdriv)** - Full automation toolkit that uses this library
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/calenvarek/ai-service)
1234
- - 🐛 [Issue Tracker](https://github.com/calenvarek/ai-service/issues)
1235
- - 💬 [Discussions](https://github.com/calenvarek/ai-service/discussions)
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 = fs$1.createReadStream(filePath);
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
- Write a commit message that clearly explains what changed and why. Your message should help teammates understand the changes without needing to read the diff.
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
- Use the available tools to investigate the changes. The more you understand, the better your message will be.
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
- **Important**: If additional context is provided (from context files or other sources), use your judgment:
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 in this commit
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 (${changedFiles.length}):
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
- Analyze these changes and write a clear commit message. Consider:
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
- If context information is provided, use it only if relevant to these specific changes.
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
- Investigate as needed to write an accurate, helpful message.`;
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) {