ai-credit 1.0.4 → 1.0.5
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 +45 -2
- package/dist/scanners/aider.d.ts +35 -0
- package/dist/scanners/aider.js +205 -0
- package/dist/scanners/base.d.ts +7 -0
- package/dist/scanners/base.js +34 -0
- package/dist/scanners/claude.js +15 -5
- package/dist/scanners/codex.js +16 -5
- package/dist/scanners/gemini.js +9 -3
- package/dist/scanners/opencode.js +3 -2
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/ai-credit)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
A command-line tool to track and analyze AI coding assistants' contributions in your codebase (macOS/Linux). Supports **Claude Code**, **Codex CLI**, **Gemini CLI**, and **Opencode**.
|
|
6
|
+
A command-line tool to track and analyze AI coding assistants' contributions in your codebase (macOS/Linux/Windows). Supports **Claude Code**, **Codex CLI**, **Gemini CLI**, and **Opencode**.
|
|
7
|
+
|
|
8
|
+
<img width="700" height="700" alt="image" src="https://github.com/user-attachments/assets/48545b91-8d20-4946-bc1c-f55762c01539" />
|
|
7
9
|
|
|
8
10
|
## Quick Start
|
|
9
11
|
|
|
@@ -18,7 +20,7 @@ ai-credit
|
|
|
18
20
|
|
|
19
21
|
## Features
|
|
20
22
|
|
|
21
|
-
- 🔍 **Auto-detection**: Automatically finds AI tool session data on your system (macOS/Linux)
|
|
23
|
+
- 🔍 **Auto-detection**: Automatically finds AI tool session data on your system (macOS/Linux/Windows)
|
|
22
24
|
- 📊 **Detailed Statistics**: Lines of code, files modified, contribution ratios
|
|
23
25
|
- 🤖 **Multi-tool Support**: Claude Code, Codex CLI, Gemini CLI, Opencode
|
|
24
26
|
- 📈 **Visual Reports**: Console, JSON, and Markdown output formats
|
|
@@ -274,6 +276,46 @@ Here's a detailed breakdown of the parsing method for each supported tool:
|
|
|
274
276
|
}
|
|
275
277
|
```
|
|
276
278
|
|
|
279
|
+
### 4. Opencode
|
|
280
|
+
|
|
281
|
+
- **File Format**: JSON (`.json`) session and message files.
|
|
282
|
+
- **Scan Paths**:
|
|
283
|
+
- Sessions: `~/.local/share/opencode/storage/session/**/*.json`
|
|
284
|
+
- Messages: `~/.local/share/opencode/storage/message/<session-id>/*.json`
|
|
285
|
+
- **Parsing Logic**:
|
|
286
|
+
1. The scanner reads all session JSON files (stored under project-hash subfolders).
|
|
287
|
+
2. It filters sessions by project path using `directory` or `projectPath` in the session metadata.
|
|
288
|
+
3. If message files exist for the session, it parses each message and looks for `summary.diffs`.
|
|
289
|
+
4. If no message-level diffs are found, it falls back to `summary.diffs` in the session file.
|
|
290
|
+
5. Each diff entry provides:
|
|
291
|
+
- `file`: relative file path
|
|
292
|
+
- `before`: previous content
|
|
293
|
+
- `after`: new content
|
|
294
|
+
- `additions` / `deletions`: optional precomputed line counts
|
|
295
|
+
6. Lines added/removed are taken from `additions`/`deletions` when present, otherwise computed from `before`/`after`.
|
|
296
|
+
7. The scanner also extracts the model from message data (e.g., `model.modelID`) when available.
|
|
297
|
+
|
|
298
|
+
**Example (Simplified Opencode Message JSON):**
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"sessionID": "sess_abc123",
|
|
303
|
+
"time": { "created": "2026-02-02T10:15:00Z" },
|
|
304
|
+
"model": { "modelID": "kimi-k2.5-free" },
|
|
305
|
+
"summary": {
|
|
306
|
+
"diffs": [
|
|
307
|
+
{
|
|
308
|
+
"file": "src/index.ts",
|
|
309
|
+
"before": "console.log('old');\n",
|
|
310
|
+
"after": "console.log('new');\n",
|
|
311
|
+
"additions": 1,
|
|
312
|
+
"deletions": 1
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
277
319
|
### Summary
|
|
278
320
|
|
|
279
321
|
In essence, `ai-credit` features a specialized scanner for each supported AI tool. Each scanner is programmed to know its corresponding tool's log storage location and data structure. During analysis, the main program invokes all available scanners, collects all `FileChange` events related to the target project, and then aggregates, deduplicates, and analyzes these events to generate the final contribution report.
|
|
@@ -340,6 +382,7 @@ This methodology ensures that:
|
|
|
340
382
|
- Accuracy depends on the completeness of AI tool session logs
|
|
341
383
|
- Some AI tools may not record all file operations
|
|
342
384
|
- Files ignored by the root `.gitignore` are excluded from Total Files/Lines
|
|
385
|
+
- Windows support for some tools depends on their session storage format compatibility
|
|
343
386
|
|
|
344
387
|
## Contributing
|
|
345
388
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AISession, AITool } from '../types.js';
|
|
2
|
+
import { BaseScanner } from './base.js';
|
|
3
|
+
/**
|
|
4
|
+
* Scanner for Aider sessions
|
|
5
|
+
*
|
|
6
|
+
* Aider stores chat history in:
|
|
7
|
+
* <project>/.aider.chat.history.md
|
|
8
|
+
* <project>/.aider.input.history
|
|
9
|
+
* <project>/.aider/
|
|
10
|
+
*
|
|
11
|
+
* The file contains markdown-formatted conversation with
|
|
12
|
+
* code blocks that show file changes.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AiderScanner extends BaseScanner {
|
|
15
|
+
get tool(): AITool;
|
|
16
|
+
get storagePath(): string;
|
|
17
|
+
/**
|
|
18
|
+
* For Aider, storage is project-local
|
|
19
|
+
*/
|
|
20
|
+
protected resolveStoragePath(): string;
|
|
21
|
+
scan(projectPath: string): AISession[];
|
|
22
|
+
/**
|
|
23
|
+
* Check if Aider history exists in the project
|
|
24
|
+
*/
|
|
25
|
+
isAvailable(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Check if Aider history exists for a specific project
|
|
28
|
+
*/
|
|
29
|
+
isAvailableForProject(projectPath: string): boolean;
|
|
30
|
+
parseSessionFile(filePath: string, projectPath: string): AISession | null;
|
|
31
|
+
/**
|
|
32
|
+
* Deduplicate changes, keeping the latest for each file
|
|
33
|
+
*/
|
|
34
|
+
private deduplicateChanges;
|
|
35
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { AITool } from '../types.js';
|
|
5
|
+
import { BaseScanner } from './base.js';
|
|
6
|
+
/**
|
|
7
|
+
* Scanner for Aider sessions
|
|
8
|
+
*
|
|
9
|
+
* Aider stores chat history in:
|
|
10
|
+
* <project>/.aider.chat.history.md
|
|
11
|
+
* <project>/.aider.input.history
|
|
12
|
+
* <project>/.aider/
|
|
13
|
+
*
|
|
14
|
+
* The file contains markdown-formatted conversation with
|
|
15
|
+
* code blocks that show file changes.
|
|
16
|
+
*/
|
|
17
|
+
export class AiderScanner extends BaseScanner {
|
|
18
|
+
get tool() {
|
|
19
|
+
return AITool.AIDER;
|
|
20
|
+
}
|
|
21
|
+
get storagePath() {
|
|
22
|
+
return '.aider.chat.history.md';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* For Aider, storage is project-local
|
|
26
|
+
*/
|
|
27
|
+
resolveStoragePath() {
|
|
28
|
+
return this.storagePath;
|
|
29
|
+
}
|
|
30
|
+
scan(projectPath) {
|
|
31
|
+
const sessions = [];
|
|
32
|
+
// Check for main history file
|
|
33
|
+
const historyFile = path.join(projectPath, '.aider.chat.history.md');
|
|
34
|
+
if (fs.existsSync(historyFile)) {
|
|
35
|
+
const session = this.parseSessionFile(historyFile, projectPath);
|
|
36
|
+
if (session && session.changes.length > 0) {
|
|
37
|
+
sessions.push(session);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Also check .aider directory for additional history
|
|
41
|
+
const aiderDir = path.join(projectPath, '.aider');
|
|
42
|
+
if (fs.existsSync(aiderDir)) {
|
|
43
|
+
try {
|
|
44
|
+
const files = glob.sync('**/*.md', { cwd: aiderDir });
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
if (file.includes('history') || file.includes('chat')) {
|
|
47
|
+
const session = this.parseSessionFile(path.join(aiderDir, file), projectPath);
|
|
48
|
+
if (session && session.changes.length > 0) {
|
|
49
|
+
sessions.push(session);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Ignore errors
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sessions;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if Aider history exists in the project
|
|
62
|
+
*/
|
|
63
|
+
isAvailable() {
|
|
64
|
+
// For Aider, we can't check globally - it's project-specific
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if Aider history exists for a specific project
|
|
69
|
+
*/
|
|
70
|
+
isAvailableForProject(projectPath) {
|
|
71
|
+
const historyFile = path.join(projectPath, this.storagePath);
|
|
72
|
+
const aiderDir = path.join(projectPath, '.aider');
|
|
73
|
+
return fs.existsSync(historyFile) || fs.existsSync(aiderDir);
|
|
74
|
+
}
|
|
75
|
+
parseSessionFile(filePath, projectPath) {
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const changes = [];
|
|
84
|
+
const fileStats = fs.statSync(filePath);
|
|
85
|
+
const sessionTimestamp = fileStats.mtime;
|
|
86
|
+
// Parse the markdown content to find file changes
|
|
87
|
+
// Aider uses various patterns:
|
|
88
|
+
// Pattern 1: ```language path/to/file.py
|
|
89
|
+
const codeBlockRegex = /```(\w+)?\s+([^\n`]+)\n([\s\S]*?)```/g;
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
92
|
+
const [, language, filePathRaw, code] = match;
|
|
93
|
+
// Skip if it looks like a diff or command output
|
|
94
|
+
if (filePathRaw.startsWith('>') || filePathRaw.startsWith('$') || filePathRaw.startsWith('#')) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Clean up the file path
|
|
98
|
+
const cleanPath = filePathRaw.trim();
|
|
99
|
+
// Skip if path contains spaces (likely not a real path) or is too long
|
|
100
|
+
if (!cleanPath || cleanPath.includes(' ') || cleanPath.length > 200) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Skip common non-file patterns
|
|
104
|
+
if (cleanPath.match(/^(bash|shell|console|output|diff|patch|error|warning|note|example)/i)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const linesAdded = this.countLines(code);
|
|
108
|
+
if (linesAdded > 0) {
|
|
109
|
+
changes.push({
|
|
110
|
+
filePath: this.normalizePath(cleanPath, projectPath),
|
|
111
|
+
linesAdded,
|
|
112
|
+
linesRemoved: 0,
|
|
113
|
+
changeType: 'modify',
|
|
114
|
+
timestamp: sessionTimestamp,
|
|
115
|
+
tool: this.tool,
|
|
116
|
+
content: code,
|
|
117
|
+
addedLines: this.extractNonEmptyLines(code),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Pattern 2: SEARCH/REPLACE blocks (Aider's edit format)
|
|
122
|
+
// Look for file context before SEARCH/REPLACE
|
|
123
|
+
const fileEditRegex = /(?:^|\n)([^\n]+\.[a-zA-Z]+)\n```[^\n]*\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
|
|
124
|
+
while ((match = fileEditRegex.exec(content)) !== null) {
|
|
125
|
+
const [, filePathRaw, searchContent, replaceContent] = match;
|
|
126
|
+
const cleanPath = filePathRaw.trim();
|
|
127
|
+
if (cleanPath && !cleanPath.includes(' ')) {
|
|
128
|
+
const linesRemoved = this.countLines(searchContent);
|
|
129
|
+
const linesAdded = this.countLines(replaceContent);
|
|
130
|
+
if (linesAdded > 0 || linesRemoved > 0) {
|
|
131
|
+
changes.push({
|
|
132
|
+
filePath: this.normalizePath(cleanPath, projectPath),
|
|
133
|
+
linesAdded,
|
|
134
|
+
linesRemoved,
|
|
135
|
+
changeType: 'modify',
|
|
136
|
+
timestamp: sessionTimestamp,
|
|
137
|
+
tool: this.tool,
|
|
138
|
+
content: replaceContent,
|
|
139
|
+
addedLines: this.extractNonEmptyLines(replaceContent),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Pattern 3: Standalone SEARCH/REPLACE without file context
|
|
145
|
+
const standaloneEditRegex = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
|
|
146
|
+
while ((match = standaloneEditRegex.exec(content)) !== null) {
|
|
147
|
+
const [fullMatch, searchContent, replaceContent] = match;
|
|
148
|
+
// Skip if already captured by fileEditRegex
|
|
149
|
+
if (content.indexOf(fullMatch) !== match.index)
|
|
150
|
+
continue;
|
|
151
|
+
const linesRemoved = this.countLines(searchContent);
|
|
152
|
+
const linesAdded = this.countLines(replaceContent);
|
|
153
|
+
if (linesAdded > 0 || linesRemoved > 0) {
|
|
154
|
+
changes.push({
|
|
155
|
+
filePath: 'unknown',
|
|
156
|
+
linesAdded,
|
|
157
|
+
linesRemoved,
|
|
158
|
+
changeType: 'modify',
|
|
159
|
+
timestamp: sessionTimestamp,
|
|
160
|
+
tool: this.tool,
|
|
161
|
+
content: replaceContent,
|
|
162
|
+
addedLines: this.extractNonEmptyLines(replaceContent),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Deduplicate changes by file path
|
|
167
|
+
const uniqueChanges = this.deduplicateChanges(changes);
|
|
168
|
+
if (uniqueChanges.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
return {
|
|
171
|
+
id: this.generateSessionId(filePath),
|
|
172
|
+
tool: this.tool,
|
|
173
|
+
timestamp: sessionTimestamp,
|
|
174
|
+
projectPath,
|
|
175
|
+
changes: uniqueChanges,
|
|
176
|
+
totalFilesChanged: new Set(uniqueChanges.map(c => c.filePath)).size,
|
|
177
|
+
totalLinesAdded: uniqueChanges.reduce((sum, c) => sum + c.linesAdded, 0),
|
|
178
|
+
totalLinesRemoved: uniqueChanges.reduce((sum, c) => sum + c.linesRemoved, 0),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Deduplicate changes, keeping the latest for each file
|
|
183
|
+
*/
|
|
184
|
+
deduplicateChanges(changes) {
|
|
185
|
+
const byFile = new Map();
|
|
186
|
+
for (const change of changes) {
|
|
187
|
+
const existing = byFile.get(change.filePath);
|
|
188
|
+
if (!existing) {
|
|
189
|
+
byFile.set(change.filePath, { ...change });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Accumulate lines
|
|
193
|
+
existing.linesAdded += change.linesAdded;
|
|
194
|
+
existing.linesRemoved += change.linesRemoved;
|
|
195
|
+
if (change.addedLines && change.addedLines.length > 0) {
|
|
196
|
+
if (!existing.addedLines) {
|
|
197
|
+
existing.addedLines = [];
|
|
198
|
+
}
|
|
199
|
+
existing.addedLines.push(...change.addedLines);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Array.from(byFile.values()).filter(c => c.filePath !== 'unknown');
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/scanners/base.d.ts
CHANGED
|
@@ -45,6 +45,13 @@ export declare abstract class BaseScanner {
|
|
|
45
45
|
* Compute added lines using LCS diff (non-empty lines only)
|
|
46
46
|
*/
|
|
47
47
|
protected diffAddedLines(before: string | undefined, after: string | undefined): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Compute added/removed line counts using LCS diff (non-empty lines only)
|
|
50
|
+
*/
|
|
51
|
+
protected diffLineCounts(before: string | undefined, after: string | undefined): {
|
|
52
|
+
added: number;
|
|
53
|
+
removed: number;
|
|
54
|
+
};
|
|
48
55
|
/**
|
|
49
56
|
* Extract added lines from a unified diff
|
|
50
57
|
*/
|
package/dist/scanners/base.js
CHANGED
|
@@ -89,6 +89,40 @@ export class BaseScanner {
|
|
|
89
89
|
}
|
|
90
90
|
return added.reverse();
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Compute added/removed line counts using LCS diff (non-empty lines only)
|
|
94
|
+
*/
|
|
95
|
+
diffLineCounts(before, after) {
|
|
96
|
+
const beforeLines = this.extractNonEmptyLines(before);
|
|
97
|
+
const afterLines = this.extractNonEmptyLines(after);
|
|
98
|
+
if (beforeLines.length === 0)
|
|
99
|
+
return { added: afterLines.length, removed: 0 };
|
|
100
|
+
if (afterLines.length === 0)
|
|
101
|
+
return { added: 0, removed: beforeLines.length };
|
|
102
|
+
const m = beforeLines.length;
|
|
103
|
+
const n = afterLines.length;
|
|
104
|
+
let prev = new Array(n + 1).fill(0);
|
|
105
|
+
let curr = new Array(n + 1).fill(0);
|
|
106
|
+
for (let i = 1; i <= m; i++) {
|
|
107
|
+
for (let j = 1; j <= n; j++) {
|
|
108
|
+
if (beforeLines[i - 1] === afterLines[j - 1]) {
|
|
109
|
+
curr[j] = prev[j - 1] + 1;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
curr[j] = Math.max(prev[j], curr[j - 1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const temp = prev;
|
|
116
|
+
prev = curr;
|
|
117
|
+
curr = temp;
|
|
118
|
+
curr.fill(0);
|
|
119
|
+
}
|
|
120
|
+
const lcs = prev[n];
|
|
121
|
+
return {
|
|
122
|
+
added: afterLines.length - lcs,
|
|
123
|
+
removed: beforeLines.length - lcs,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
92
126
|
/**
|
|
93
127
|
* Extract added lines from a unified diff
|
|
94
128
|
*/
|
package/dist/scanners/claude.js
CHANGED
|
@@ -187,18 +187,28 @@ export class ClaudeScanner extends BaseScanner {
|
|
|
187
187
|
let addedLines = [];
|
|
188
188
|
if (writeOps.includes(toolName)) {
|
|
189
189
|
changeType = oldContent ? 'modify' : 'create';
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
191
|
+
linesAdded = stats.added;
|
|
192
|
+
linesRemoved = stats.removed;
|
|
193
|
+
if (oldContent && newContent) {
|
|
194
|
+
addedLines = this.diffAddedLines(oldContent, newContent);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
addedLines = this.extractNonEmptyLines(newContent);
|
|
198
|
+
}
|
|
193
199
|
}
|
|
194
200
|
else if (editOps.includes(toolName)) {
|
|
195
201
|
changeType = 'modify';
|
|
196
|
-
linesAdded = this.countLines(newContent);
|
|
197
|
-
linesRemoved = this.countLines(oldContent);
|
|
198
202
|
if (oldContent && newContent) {
|
|
203
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
204
|
+
linesAdded = stats.added;
|
|
205
|
+
linesRemoved = stats.removed;
|
|
199
206
|
addedLines = this.diffAddedLines(oldContent, newContent);
|
|
200
207
|
}
|
|
201
208
|
else {
|
|
209
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
210
|
+
linesAdded = stats.added;
|
|
211
|
+
linesRemoved = stats.removed;
|
|
202
212
|
addedLines = this.extractNonEmptyLines(newContent);
|
|
203
213
|
}
|
|
204
214
|
}
|
package/dist/scanners/codex.js
CHANGED
|
@@ -316,14 +316,19 @@ export class CodexScanner extends BaseScanner {
|
|
|
316
316
|
let linesRemoved = 0;
|
|
317
317
|
let addedLines = [];
|
|
318
318
|
if (writeOps.includes(funcName)) {
|
|
319
|
-
changeType = 'create';
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
changeType = oldContent ? 'modify' : 'create';
|
|
320
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
321
|
+
linesAdded = stats.added;
|
|
322
|
+
linesRemoved = stats.removed;
|
|
323
|
+
if (oldContent && newContent) {
|
|
324
|
+
addedLines = this.diffAddedLines(oldContent, newContent);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
addedLines = this.extractNonEmptyLines(newContent);
|
|
328
|
+
}
|
|
322
329
|
}
|
|
323
330
|
else if (editOps.includes(funcName)) {
|
|
324
331
|
changeType = 'modify';
|
|
325
|
-
linesAdded = this.countLines(newContent);
|
|
326
|
-
linesRemoved = this.countLines(oldContent);
|
|
327
332
|
if ((funcName === 'apply_diff' || funcName === 'patch') && args.diff) {
|
|
328
333
|
const diffStats = this.parseDiff(args.diff);
|
|
329
334
|
linesAdded = diffStats.added;
|
|
@@ -331,9 +336,15 @@ export class CodexScanner extends BaseScanner {
|
|
|
331
336
|
addedLines = this.extractAddedLinesFromDiff(args.diff);
|
|
332
337
|
}
|
|
333
338
|
else if (oldContent && newContent) {
|
|
339
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
340
|
+
linesAdded = stats.added;
|
|
341
|
+
linesRemoved = stats.removed;
|
|
334
342
|
addedLines = this.diffAddedLines(oldContent, newContent);
|
|
335
343
|
}
|
|
336
344
|
else {
|
|
345
|
+
const stats = this.diffLineCounts(oldContent, newContent);
|
|
346
|
+
linesAdded = stats.added;
|
|
347
|
+
linesRemoved = stats.removed;
|
|
337
348
|
addedLines = this.extractNonEmptyLines(newContent);
|
|
338
349
|
}
|
|
339
350
|
}
|
package/dist/scanners/gemini.js
CHANGED
|
@@ -284,9 +284,15 @@ export class GeminiScanner extends BaseScanner {
|
|
|
284
284
|
let linesRemoved = 0;
|
|
285
285
|
let addedLines = [];
|
|
286
286
|
if (writeOps.includes(funcName)) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
const stats = this.calculateDiffStats(oldContent, newContent);
|
|
288
|
+
linesAdded = stats.added;
|
|
289
|
+
linesRemoved = stats.removed;
|
|
290
|
+
if (oldContent && newContent) {
|
|
291
|
+
addedLines = this.diffAddedLines(oldContent, newContent);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
addedLines = this.extractNonEmptyLines(newContent);
|
|
295
|
+
}
|
|
290
296
|
}
|
|
291
297
|
else if (editOps.includes(funcName)) {
|
|
292
298
|
// Use LCS for edits to be accurate
|
|
@@ -162,9 +162,10 @@ export class OpencodeScanner extends BaseScanner {
|
|
|
162
162
|
: 'modify';
|
|
163
163
|
// Use opencode's provided diff stats if available (most accurate)
|
|
164
164
|
// additions/deletions are pre-calculated by opencode
|
|
165
|
+
const diffStats = this.diffLineCounts(beforeContent, afterContent);
|
|
165
166
|
const addedLines = this.diffAddedLines(beforeContent, afterContent);
|
|
166
|
-
const linesAdded = typeof diff.additions === 'number' ? diff.additions :
|
|
167
|
-
const linesRemoved = typeof diff.deletions === 'number' ? diff.deletions :
|
|
167
|
+
const linesAdded = typeof diff.additions === 'number' ? diff.additions : diffStats.added;
|
|
168
|
+
const linesRemoved = typeof diff.deletions === 'number' ? diff.deletions : diffStats.removed;
|
|
168
169
|
return {
|
|
169
170
|
filePath,
|
|
170
171
|
linesAdded,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-credit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "CLI tool to track and analyze AI coding assistants' contributions in your codebase",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"start": "node dist/cli.js",
|
|
13
13
|
"dev": "tsc && node dist/cli.js",
|
|
14
|
-
"
|
|
14
|
+
"v-patch": "npm version patch",
|
|
15
|
+
"v-minor": "npm version minor",
|
|
16
|
+
"v-major": "npm version major",
|
|
17
|
+
"pub": "npm run build && npm publish --access public",
|
|
15
18
|
"prepublishOnly": "npm run build"
|
|
16
19
|
},
|
|
17
20
|
"keywords": [
|