ai-credit 1.0.0 โ†’ 1.0.1

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/ai-credit.svg)](https://www.npmjs.com/package/ai-credit)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A command-line tool to track and analyze AI coding assistants' contributions in your codebase. Supports **Claude Code**, **Codex CLI**, **Gemini CLI**, and **Aider**.
6
+ A command-line tool to track and analyze AI coding assistants' contributions in your codebase. Supports **Claude Code**, **Codex CLI**, **Gemini CLI**, and **Opencode**.
7
7
 
8
8
  ## Quick Start
9
9
 
@@ -20,7 +20,7 @@ ai-credit
20
20
 
21
21
  - ๐Ÿ” **Auto-detection**: Automatically finds AI tool session data on your system
22
22
  - ๐Ÿ“Š **Detailed Statistics**: Lines of code, files modified, contribution ratios
23
- - ๐Ÿค– **Multi-tool Support**: Claude Code, Codex CLI, Gemini CLI, Aider
23
+ - ๐Ÿค– **Multi-tool Support**: Claude Code, Codex CLI, Gemini CLI, Opencode
24
24
  - ๐Ÿ“ˆ **Visual Reports**: Console, JSON, and Markdown output formats
25
25
  - ๐Ÿ“… **Timeline View**: Track AI contributions over time
26
26
  - ๐Ÿ“ **File-level Analysis**: See which files have the most AI contributions
@@ -54,7 +54,7 @@ npx ai-credit [path]
54
54
  # Options:
55
55
  # -f, --format Output format (console/json/markdown)
56
56
  # -o, --output Output file path
57
- # -t, --tools AI tools to analyze (claude,codex,gemini,aider,all)
57
+ # -t, --tools AI tools to analyze (claude,codex,gemini,opencode,all)
58
58
  # -v, --verbose Show detailed output
59
59
  ```
60
60
 
@@ -72,7 +72,6 @@ Shows which AI tools have data available on your system:
72
72
  Claude Code ~/.claude/projects/ โœ“ Available
73
73
  Codex CLI ~/.codex/sessions/ โœ“ Available
74
74
  Gemini CLI ~/.gemini/tmp/ โœ— Not found
75
- Aider .aider.chat.history.md โœ— Not found
76
75
  ```
77
76
 
78
77
  ### File-level Analysis
@@ -123,14 +122,14 @@ Lists all AI sessions for the repository.
123
122
  โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ
124
123
  โ”‚ Claude Code โ”‚ 15 โ”‚ 30 โ”‚ +2500 โ”‚
125
124
  โ”‚ Codex CLI โ”‚ 10 โ”‚ 20 โ”‚ +1000 โ”‚
126
- โ”‚ Aider โ”‚ 3 โ”‚ 5 โ”‚ +250 โ”‚
125
+ โ”‚ Opencode โ”‚ 3 โ”‚ 5 โ”‚ +250 โ”‚
127
126
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
128
127
 
129
128
  ๐Ÿ“ˆ Contribution Distribution
130
129
 
131
130
  Claude Code โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 66.7%
132
131
  Codex CLI โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 26.7%
133
- Aider โ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 6.7%
132
+ Opencode โ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 6.7%
134
133
  ```
135
134
 
136
135
  ## Supported AI Tools
@@ -140,7 +139,7 @@ Lists all AI sessions for the repository.
140
139
  | Claude Code | `~/.claude/projects/<path>/` | JSONL |
141
140
  | Codex CLI | `~/.codex/sessions/YYYY/MM/DD/` | JSONL |
142
141
  | Gemini CLI | `~/.gemini/tmp/<hash>/chats/` | JSON |
143
- | Aider | `.aider.chat.history.md` | Markdown |
142
+ | Opencode | `~/.local/share/opencode/` | JSON |
144
143
 
145
144
  ## How It Works: The JSON Parsing Logic
146
145
 
@@ -23,15 +23,31 @@ export declare class ContributionAnalyzer {
23
23
  */
24
24
  private getRepoFiles;
25
25
  /**
26
- * Count total lines in all repository files
26
+ * Build a file index for verification and totals
27
27
  */
28
- private countTotalLines;
28
+ private buildRepoFileIndex;
29
+ /**
30
+ * Sum total lines from the repository index
31
+ */
32
+ private sumRepoLines;
33
+ /**
34
+ * Split content into non-empty lines
35
+ */
36
+ private splitNonEmptyLines;
37
+ /**
38
+ * Get added lines from a change, falling back to content
39
+ */
40
+ private getAddedLines;
41
+ /**
42
+ * Count verified added lines that still exist in the repo file
43
+ */
44
+ private countVerifiedAddedLines;
29
45
  /**
30
46
  * Compute statistics by AI tool
31
47
  */
32
48
  private computeToolStats;
33
49
  /**
34
- * Compute statistics by file using Verified Existence logic
50
+ * Compute statistics by file
35
51
  */
36
52
  private computeFileStats;
37
53
  }
package/dist/analyzer.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { glob } from 'glob';
4
- import { AITool, } from './types.js';
5
- import { ClaudeScanner, CodexScanner, GeminiScanner, AiderScanner, OpencodeScanner, } from './scanners/index.js';
4
+ import { ClaudeScanner, CodexScanner, GeminiScanner, OpencodeScanner, } from './scanners/index.js';
6
5
  /**
7
6
  * Main analyzer that coordinates all scanners and computes statistics
8
7
  */
@@ -15,7 +14,6 @@ export class ContributionAnalyzer {
15
14
  new ClaudeScanner(),
16
15
  new CodexScanner(),
17
16
  new GeminiScanner(),
18
- new AiderScanner(),
19
17
  new OpencodeScanner(),
20
18
  ];
21
19
  }
@@ -29,13 +27,6 @@ export class ContributionAnalyzer {
29
27
  available.push(scanner.tool);
30
28
  }
31
29
  }
32
- // Special check for Aider (project-local)
33
- const aiderScanner = this.scanners.find(s => s.tool === AITool.AIDER);
34
- if (aiderScanner?.isAvailableForProject(this.projectPath)) {
35
- if (!available.includes(AITool.AIDER)) {
36
- available.push(AITool.AIDER);
37
- }
38
- }
39
30
  return available;
40
31
  }
41
32
  /**
@@ -66,31 +57,20 @@ export class ContributionAnalyzer {
66
57
  const sessions = this.scanAllSessions(tools);
67
58
  // Get repository file stats
68
59
  const repoFiles = this.getRepoFiles();
69
- const totalLines = this.countTotalLines(repoFiles);
60
+ const repoFileIndex = this.buildRepoFileIndex(repoFiles);
61
+ const totalLines = this.sumRepoLines(repoFileIndex);
70
62
  // Compute statistics
71
- const byTool = this.computeToolStats(sessions);
72
- const byFile = this.computeFileStats(sessions, repoFiles);
63
+ const byTool = this.computeToolStats(sessions, repoFileIndex);
64
+ const byFile = this.computeFileStats(sessions, repoFileIndex);
73
65
  // Count AI-touched files and lines (only count files that exist in repo)
74
66
  let aiTouchedFiles = 0;
75
67
  let aiContributedLines = 0;
76
- for (const [, stats] of byFile) {
77
- if (stats.aiContributedLines > 0) {
68
+ for (const [filePath, stats] of byFile) {
69
+ // Only count files that exist in the repo
70
+ if (stats.aiContributedLines > 0 && repoFiles.includes(filePath)) {
78
71
  aiTouchedFiles++;
79
- aiContributedLines += stats.aiContributedLines;
80
- // Aggregate verified lines to ToolStats and ModelStats
81
- for (const [tool, count] of stats.contributions) {
82
- const toolStats = byTool.get(tool);
83
- if (toolStats) {
84
- toolStats.verifiedLines += count;
85
- // Distribute verified lines to models based on their activity share
86
- if (toolStats.linesAdded > 0) {
87
- for (const [, modelStats] of toolStats.byModel) {
88
- const ratio = modelStats.linesAdded / toolStats.linesAdded;
89
- modelStats.verifiedLines += Math.round(count * ratio);
90
- }
91
- }
92
- }
93
- }
72
+ // Cap contribution at actual file lines to avoid >100%
73
+ aiContributedLines += Math.min(stats.aiContributedLines, stats.totalLines);
94
74
  }
95
75
  }
96
76
  return {
@@ -158,25 +138,81 @@ export class ContributionAnalyzer {
158
138
  }
159
139
  }
160
140
  /**
161
- * Count total lines in all repository files
141
+ * Build a file index for verification and totals
162
142
  */
163
- countTotalLines(files) {
164
- let total = 0;
143
+ buildRepoFileIndex(files) {
144
+ const index = new Map();
165
145
  for (const file of files) {
166
146
  try {
167
147
  const content = fs.readFileSync(path.join(this.projectPath, file), 'utf-8');
168
- total += content.split('\n').length;
148
+ const totalLines = content.split('\n').length;
149
+ const normalizedLines = content.split(/\r?\n/);
150
+ const nonEmptyLines = normalizedLines.filter(line => line.length > 0).length;
151
+ const lineSet = new Set(normalizedLines.filter(line => line.length > 0));
152
+ index.set(file, { totalLines, nonEmptyLines, lineSet });
169
153
  }
170
154
  catch {
171
- // Ignore unreadable files
155
+ index.set(file, { totalLines: 0, nonEmptyLines: 0, lineSet: new Set() });
172
156
  }
173
157
  }
158
+ return index;
159
+ }
160
+ /**
161
+ * Sum total lines from the repository index
162
+ */
163
+ sumRepoLines(repoFileIndex) {
164
+ let total = 0;
165
+ for (const info of repoFileIndex.values()) {
166
+ total += info.totalLines;
167
+ }
174
168
  return total;
175
169
  }
170
+ /**
171
+ * Split content into non-empty lines
172
+ */
173
+ splitNonEmptyLines(content) {
174
+ if (!content)
175
+ return [];
176
+ return content.split(/\r?\n/).filter(line => line.length > 0);
177
+ }
178
+ /**
179
+ * Get added lines from a change, falling back to content
180
+ */
181
+ getAddedLines(change) {
182
+ if (change.addedLines && change.addedLines.length > 0) {
183
+ return change.addedLines;
184
+ }
185
+ if (change.content) {
186
+ return this.splitNonEmptyLines(change.content);
187
+ }
188
+ return [];
189
+ }
190
+ /**
191
+ * Count verified added lines that still exist in the repo file
192
+ */
193
+ countVerifiedAddedLines(change, fileInfo) {
194
+ if (!fileInfo)
195
+ return 0;
196
+ const addedLines = this.getAddedLines(change);
197
+ if (addedLines.length === 0)
198
+ return 0;
199
+ let matched = 0;
200
+ for (const line of addedLines) {
201
+ if (line.length === 0)
202
+ continue;
203
+ if (fileInfo.lineSet.has(line)) {
204
+ matched++;
205
+ }
206
+ }
207
+ if (change.linesAdded > 0) {
208
+ return Math.min(matched, change.linesAdded);
209
+ }
210
+ return Math.min(matched, addedLines.length);
211
+ }
176
212
  /**
177
213
  * Compute statistics by AI tool
178
214
  */
179
- computeToolStats(sessions) {
215
+ computeToolStats(sessions, repoFileIndex) {
180
216
  const stats = new Map();
181
217
  // Track unique files per tool across all sessions
182
218
  const filesByTool = new Map();
@@ -194,7 +230,6 @@ export class ContributionAnalyzer {
194
230
  linesAdded: 0,
195
231
  linesRemoved: 0,
196
232
  netLines: 0,
197
- verifiedLines: 0,
198
233
  byModel: new Map(),
199
234
  };
200
235
  stats.set(session.tool, toolStats);
@@ -204,7 +239,9 @@ export class ContributionAnalyzer {
204
239
  const toolFiles = filesByTool.get(session.tool);
205
240
  for (const change of session.changes) {
206
241
  toolFiles.add(change.filePath);
207
- toolStats.linesAdded += change.linesAdded;
242
+ const fileInfo = repoFileIndex.get(change.filePath);
243
+ const verifiedAdded = this.countVerifiedAddedLines(change, fileInfo);
244
+ toolStats.linesAdded += verifiedAdded;
208
245
  toolStats.linesRemoved += change.linesRemoved;
209
246
  if (change.changeType === 'create') {
210
247
  toolStats.filesCreated++;
@@ -218,21 +255,20 @@ export class ContributionAnalyzer {
218
255
  if (!modelStats) {
219
256
  modelStats = {
220
257
  model: modelName,
221
- sessionsCount: 0,
258
+ sessionsCount: 0, // Will be counted below (approximated by session presence)
222
259
  filesCreated: 0,
223
260
  filesModified: 0,
224
261
  totalFiles: 0,
225
262
  linesAdded: 0,
226
263
  linesRemoved: 0,
227
264
  netLines: 0,
228
- verifiedLines: 0,
229
265
  };
230
266
  toolStats.byModel.set(modelName, modelStats);
231
267
  filesByModel.set(`${session.tool}:${modelName}`, new Set());
232
268
  }
233
269
  const modelFiles = filesByModel.get(`${session.tool}:${modelName}`);
234
270
  modelFiles.add(change.filePath);
235
- modelStats.linesAdded += change.linesAdded;
271
+ modelStats.linesAdded += verifiedAdded;
236
272
  modelStats.linesRemoved += change.linesRemoved;
237
273
  if (change.changeType === 'create') {
238
274
  modelStats.filesCreated++;
@@ -241,7 +277,10 @@ export class ContributionAnalyzer {
241
277
  modelStats.filesModified++;
242
278
  }
243
279
  }
244
- // Count sessions per model
280
+ // Count sessions per model (if any change in session is attributed to model)
281
+ // This is a bit tricky if a session has mixed models, but usually it's one per session
282
+ const sessionModel = session.model || 'unknown';
283
+ // Use a set to track which models appeared in this session to avoid double counting if mixed
245
284
  const modelsInSession = new Set();
246
285
  if (session.model)
247
286
  modelsInSession.add(session.model);
@@ -270,87 +309,52 @@ export class ContributionAnalyzer {
270
309
  return stats;
271
310
  }
272
311
  /**
273
- * Compute statistics by file using Verified Existence logic
312
+ * Compute statistics by file
274
313
  */
275
- computeFileStats(sessions, repoFiles) {
314
+ computeFileStats(sessions, repoFileIndex) {
276
315
  const stats = new Map();
277
- // 1. Group changes by file
278
- const fileChanges = new Map();
316
+ // Initialize stats for all repo files
317
+ for (const [file, info] of repoFileIndex) {
318
+ stats.set(file, {
319
+ filePath: file,
320
+ totalLines: info.nonEmptyLines,
321
+ aiContributedLines: 0,
322
+ aiContributionRatio: 0,
323
+ contributions: new Map(),
324
+ });
325
+ }
326
+ // Accumulate AI contributions
279
327
  for (const session of sessions) {
280
328
  for (const change of session.changes) {
281
- if (!fileChanges.has(change.filePath)) {
282
- fileChanges.set(change.filePath, []);
329
+ let fileStats = stats.get(change.filePath);
330
+ if (!fileStats) {
331
+ // File might have been deleted or renamed - still track it
332
+ fileStats = {
333
+ filePath: change.filePath,
334
+ totalLines: 0,
335
+ aiContributedLines: 0,
336
+ aiContributionRatio: 0,
337
+ contributions: new Map(),
338
+ };
339
+ stats.set(change.filePath, fileStats);
283
340
  }
284
- fileChanges.get(change.filePath).push(change);
341
+ const fileInfo = repoFileIndex.get(change.filePath);
342
+ const verifiedAdded = this.countVerifiedAddedLines(change, fileInfo);
343
+ fileStats.aiContributedLines += verifiedAdded;
344
+ const currentToolContrib = fileStats.contributions.get(session.tool) || 0;
345
+ fileStats.contributions.set(session.tool, currentToolContrib + verifiedAdded);
285
346
  }
286
347
  }
287
- // 2. Process each file in the repository
288
- for (const filePath of repoFiles) {
289
- const fullPath = path.join(this.projectPath, filePath);
290
- let content = '';
291
- try {
292
- content = fs.readFileSync(fullPath, 'utf-8');
348
+ // Calculate ratios - cap at 100%
349
+ for (const [, fileStats] of stats) {
350
+ if (fileStats.totalLines > 0) {
351
+ // Cap the ratio at 1.0 (100%)
352
+ fileStats.aiContributionRatio = Math.min(fileStats.aiContributedLines / fileStats.totalLines, 1.0);
293
353
  }
294
- catch {
295
- continue;
296
- }
297
- const fileLines = content.split('\n');
298
- const totalLines = fileLines.length;
299
- // Track which lines in current file are accounted for (claimed)
300
- // We use exact string matching on trimmed lines.
301
- const contentMap = new Map();
302
- for (let i = 0; i < totalLines; i++) {
303
- const line = fileLines[i].trim();
304
- if (!line)
305
- continue; // Skip empty lines for matching
306
- if (!contentMap.has(line)) {
307
- contentMap.set(line, []);
308
- }
309
- contentMap.get(line).push(i);
354
+ else if (fileStats.aiContributedLines > 0) {
355
+ // File was deleted but had AI contributions (cannot verify)
356
+ fileStats.aiContributionRatio = 0;
310
357
  }
311
- const claimedIndices = new Set();
312
- const contributions = new Map();
313
- // Get changes for this file
314
- const changes = fileChanges.get(filePath) || [];
315
- // Sort changes: Newest First (Reverse Chronological)
316
- // This ensures the most recent author gets credit for a line
317
- changes.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
318
- let aiContributedCount = 0;
319
- for (const change of changes) {
320
- if (!change.content)
321
- continue;
322
- const changeLines = change.content.split('\n');
323
- let matchedForThisChange = 0;
324
- for (const line of changeLines) {
325
- const trimmed = line.trim();
326
- if (!trimmed)
327
- continue;
328
- const indices = contentMap.get(trimmed);
329
- if (indices) {
330
- // Find the first unclaimed index for this content
331
- for (const idx of indices) {
332
- if (!claimedIndices.has(idx)) {
333
- claimedIndices.add(idx);
334
- matchedForThisChange++;
335
- // Line is claimed by this tool
336
- break;
337
- }
338
- }
339
- }
340
- }
341
- if (matchedForThisChange > 0) {
342
- aiContributedCount += matchedForThisChange;
343
- const toolTotal = contributions.get(change.tool) || 0;
344
- contributions.set(change.tool, toolTotal + matchedForThisChange);
345
- }
346
- }
347
- stats.set(filePath, {
348
- filePath,
349
- totalLines,
350
- aiContributedLines: aiContributedCount,
351
- aiContributionRatio: totalLines > 0 ? aiContributedCount / totalLines : 0,
352
- contributions,
353
- });
354
358
  }
355
359
  return stats;
356
360
  }
package/dist/cli.js CHANGED
@@ -135,14 +135,12 @@ const TOOL_MAP = {
135
135
  claude: AITool.CLAUDE_CODE,
136
136
  codex: AITool.CODEX,
137
137
  gemini: AITool.GEMINI,
138
- aider: AITool.AIDER,
139
138
  opencode: AITool.OPENCODE,
140
139
  };
141
140
  const TOOL_INFO = {
142
141
  [AITool.CLAUDE_CODE]: { name: 'Claude Code', path: '~/.claude/projects/' },
143
142
  [AITool.CODEX]: { name: 'Codex CLI', path: '~/.codex/sessions/' },
144
143
  [AITool.GEMINI]: { name: 'Gemini CLI', path: '~/.gemini/tmp/' },
145
- [AITool.AIDER]: { name: 'Aider', path: '.aider.chat.history.md' },
146
144
  [AITool.OPENCODE]: { name: 'Opencode', path: '~/.local/share/opencode/' },
147
145
  };
148
146
  /**
@@ -163,8 +161,8 @@ function parseTools(toolStr) {
163
161
  }
164
162
  const program = new Command();
165
163
  program
166
- .name('ai-credit')
167
- .description('Track and analyze AI coding assistant contributions')
164
+ .name('ai-contrib')
165
+ .description('CLI tool to track and analyze AI coding assistants\' contributions in your codebase')
168
166
  .version('1.0.0');
169
167
  // Main scan command
170
168
  program
@@ -172,7 +170,7 @@ program
172
170
  .description('Scan repository for AI contributions')
173
171
  .option('-f, --format <format>', 'Output format (console, json, markdown)', 'console')
174
172
  .option('-o, --output <file>', 'Output file path (for json/markdown formats)')
175
- .option('-t, --tools <tools>', 'AI tools to analyze (claude,codex,gemini,aider or all)', 'all')
173
+ .option('-t, --tools <tools>', 'AI tools to analyze (claude,codex,gemini,opencode or all)', 'all')
176
174
  .option('-v, --verbose', 'Show detailed output including files and timeline')
177
175
  .action(async (repoPath = '.', options) => {
178
176
  const resolvedPath = path.resolve(repoPath);
@@ -301,7 +299,6 @@ program
301
299
  [AITool.CLAUDE_CODE]: chalk.hex('#D97757'),
302
300
  [AITool.CODEX]: chalk.hex('#00A67E'),
303
301
  [AITool.GEMINI]: chalk.hex('#4796E3'),
304
- [AITool.AIDER]: chalk.hex('#D93B3B'),
305
302
  [AITool.OPENCODE]: chalk.yellow,
306
303
  };
307
304
  // Show last 20 sessions
package/dist/reporter.js CHANGED
@@ -9,7 +9,6 @@ const TOOL_NAMES = {
9
9
  [AITool.CLAUDE_CODE]: 'Claude Code',
10
10
  [AITool.CODEX]: 'Codex CLI',
11
11
  [AITool.GEMINI]: 'Gemini CLI',
12
- [AITool.AIDER]: 'Aider',
13
12
  [AITool.OPENCODE]: 'Opencode',
14
13
  };
15
14
  /**
@@ -19,7 +18,6 @@ const TOOL_COLORS = {
19
18
  [AITool.CLAUDE_CODE]: chalk.hex('#D97757'),
20
19
  [AITool.CODEX]: chalk.hex('#00A67E'),
21
20
  [AITool.GEMINI]: chalk.hex('#4796E3'),
22
- [AITool.AIDER]: chalk.hex('#D93B3B'),
23
21
  [AITool.OPENCODE]: chalk.yellow,
24
22
  };
25
23
  /**
@@ -55,7 +53,7 @@ export class ConsoleReporter {
55
53
  * Print overview statistics
56
54
  */
57
55
  printOverview(stats) {
58
- console.log(chalk.bold(' ๐Ÿ“Š Overview'));
56
+ console.log(chalk.bold('๐Ÿ“Š Overview'));
59
57
  const table = new Table({
60
58
  head: ['Metric', 'Value', 'AI Contribution'].map(h => chalk.bold(h)),
61
59
  style: { head: [], border: [] },
@@ -78,14 +76,16 @@ export class ConsoleReporter {
78
76
  console.log(chalk.yellow('No AI contributions found.'));
79
77
  return;
80
78
  }
81
- console.log(chalk.bold(' ๐Ÿค– Contribution by AI Tool'));
79
+ console.log(chalk.bold('๐Ÿค– Contribution by AI Tool'));
82
80
  const table = new Table({
83
81
  head: ['Tool / Model', 'Sessions', 'Files', 'Lines Added', 'Lines Removed', 'Share'].map(h => chalk.bold(h)),
84
82
  style: { head: [], border: [] },
85
83
  });
86
84
  const totalLines = Array.from(stats.byTool.values())
87
85
  .reduce((sum, t) => sum + t.linesAdded, 0);
88
- for (const [tool, toolStats] of stats.byTool) {
86
+ const sortedTools = Array.from(stats.byTool.entries())
87
+ .sort((a, b) => b[1].linesAdded - a[1].linesAdded);
88
+ for (const [tool, toolStats] of sortedTools) {
89
89
  const share = totalLines > 0
90
90
  ? ((toolStats.linesAdded / totalLines) * 100).toFixed(1)
91
91
  : '0.0';
@@ -134,20 +134,25 @@ export class ConsoleReporter {
134
134
  console.log(chalk.bold('๐Ÿ“ˆ Contribution Distribution'));
135
135
  console.log();
136
136
  // Build slices: proportion each AI tool's share of repo lines + Unknown/Human
137
- // Now using Verified Lines (Verified Existence logic)
137
+ const totalAILinesAdded = Array.from(stats.byTool.values())
138
+ .reduce((sum, t) => sum + t.linesAdded, 0);
138
139
  const slices = [];
139
- for (const [tool, toolStats] of stats.byTool) {
140
- if (toolStats.verifiedLines > 0) {
140
+ const sortedTools = Array.from(stats.byTool.entries())
141
+ .sort((a, b) => b[1].linesAdded - a[1].linesAdded);
142
+ for (const [tool, toolStats] of sortedTools) {
143
+ const toolRepoLines = totalAILinesAdded > 0
144
+ ? Math.round(stats.aiContributedLines * (toolStats.linesAdded / totalAILinesAdded))
145
+ : 0;
146
+ if (toolRepoLines > 0) {
141
147
  const color = TOOL_COLORS[tool] || chalk.white;
142
- slices.push({ label: TOOL_NAMES[tool], value: toolStats.verifiedLines, color: (s) => color(s) });
148
+ slices.push({ label: TOOL_NAMES[tool], value: toolRepoLines, color: (s) => color(s) });
143
149
  }
144
150
  }
145
- const humanLines = Math.max(0, stats.totalLines - stats.aiContributedLines);
151
+ const humanLines = stats.totalLines - stats.aiContributedLines;
146
152
  if (humanLines > 0) {
147
153
  slices.push({ label: 'Unknown/Human', value: humanLines, color: (s) => chalk.gray(s) });
148
154
  }
149
155
  const total = slices.reduce((sum, s) => sum + s.value, 0);
150
- // Note: total should be approx stats.totalLines (or equal if we assume aiContributedLines is sum of verifiedLines)
151
156
  if (total === 0)
152
157
  return;
153
158
  // Render stacked horizontal bar
@@ -161,14 +166,10 @@ export class ConsoleReporter {
161
166
  // Adjust rounding so total width matches barWidth
162
167
  const totalWidth = segments.reduce((s, seg) => s + seg.width, 0);
163
168
  if (totalWidth !== barWidth && segments.length > 0) {
164
- // Just subtract/add from the largest or last segment to make it fit
165
169
  segments[segments.length - 1].width += barWidth - totalWidth;
166
170
  }
167
171
  for (const seg of segments) {
168
- // Ensure width is not negative if adjustment failed heavily
169
- if (seg.width > 0) {
170
- bar += seg.color('โ–ˆ'.repeat(seg.width));
171
- }
172
+ bar += seg.color('โ–ˆ'.repeat(seg.width));
172
173
  }
173
174
  console.log(` ${bar}`);
174
175
  console.log();
@@ -265,7 +266,9 @@ export class JsonReporter {
265
266
  ai_line_ratio: stats.totalLines > 0 ? stats.aiContributedLines / stats.totalLines : 0,
266
267
  total_sessions: stats.sessions.length,
267
268
  },
268
- by_tool: Object.fromEntries(Array.from(stats.byTool.entries()).map(([tool, toolStats]) => [
269
+ by_tool: Object.fromEntries(Array.from(stats.byTool.entries())
270
+ .sort((a, b) => b[1].linesAdded - a[1].linesAdded)
271
+ .map(([tool, toolStats]) => [
269
272
  tool,
270
273
  {
271
274
  sessions_count: toolStats.sessionsCount,
@@ -336,7 +339,9 @@ export class MarkdownReporter {
336
339
  lines.push('|------|----------|-------|-------------|---------------|-------|');
337
340
  const totalLines = Array.from(stats.byTool.values())
338
341
  .reduce((sum, t) => sum + t.linesAdded, 0);
339
- for (const [tool, toolStats] of stats.byTool) {
342
+ const sortedTools = Array.from(stats.byTool.entries())
343
+ .sort((a, b) => b[1].linesAdded - a[1].linesAdded);
344
+ for (const [tool, toolStats] of sortedTools) {
340
345
  const share = totalLines > 0
341
346
  ? ((toolStats.linesAdded / totalLines) * 100).toFixed(1)
342
347
  : '0.0';
@@ -114,6 +114,7 @@ export class AiderScanner extends BaseScanner {
114
114
  timestamp: sessionTimestamp,
115
115
  tool: this.tool,
116
116
  content: code,
117
+ addedLines: this.extractNonEmptyLines(code),
117
118
  });
118
119
  }
119
120
  }
@@ -134,6 +135,8 @@ export class AiderScanner extends BaseScanner {
134
135
  changeType: 'modify',
135
136
  timestamp: sessionTimestamp,
136
137
  tool: this.tool,
138
+ content: replaceContent,
139
+ addedLines: this.extractNonEmptyLines(replaceContent),
137
140
  });
138
141
  }
139
142
  }
@@ -155,6 +158,8 @@ export class AiderScanner extends BaseScanner {
155
158
  changeType: 'modify',
156
159
  timestamp: sessionTimestamp,
157
160
  tool: this.tool,
161
+ content: replaceContent,
162
+ addedLines: this.extractNonEmptyLines(replaceContent),
158
163
  });
159
164
  }
160
165
  }
@@ -187,6 +192,12 @@ export class AiderScanner extends BaseScanner {
187
192
  // Accumulate lines
188
193
  existing.linesAdded += change.linesAdded;
189
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
+ }
190
201
  }
191
202
  }
192
203
  return Array.from(byFile.values()).filter(c => c.filePath !== 'unknown');
@@ -33,6 +33,22 @@ export declare abstract class BaseScanner {
33
33
  * Count lines in a string
34
34
  */
35
35
  protected countLines(content: string | undefined): number;
36
+ /**
37
+ * Split content into lines, preserving whitespace
38
+ */
39
+ protected splitLines(content: string | undefined): string[];
40
+ /**
41
+ * Extract non-empty lines from content
42
+ */
43
+ protected extractNonEmptyLines(content: string | undefined): string[];
44
+ /**
45
+ * Compute added lines using LCS diff (non-empty lines only)
46
+ */
47
+ protected diffAddedLines(before: string | undefined, after: string | undefined): string[];
48
+ /**
49
+ * Extract added lines from a unified diff
50
+ */
51
+ protected extractAddedLinesFromDiff(diff: string | undefined): string[];
36
52
  /**
37
53
  * Normalize file path relative to project
38
54
  */
@@ -30,6 +30,83 @@ export class BaseScanner {
30
30
  return 0;
31
31
  return content.split('\n').filter(line => line.length > 0).length;
32
32
  }
33
+ /**
34
+ * Split content into lines, preserving whitespace
35
+ */
36
+ splitLines(content) {
37
+ if (!content)
38
+ return [];
39
+ return content.split(/\r?\n/);
40
+ }
41
+ /**
42
+ * Extract non-empty lines from content
43
+ */
44
+ extractNonEmptyLines(content) {
45
+ return this.splitLines(content).filter(line => line.length > 0);
46
+ }
47
+ /**
48
+ * Compute added lines using LCS diff (non-empty lines only)
49
+ */
50
+ diffAddedLines(before, after) {
51
+ const beforeLines = this.extractNonEmptyLines(before);
52
+ const afterLines = this.extractNonEmptyLines(after);
53
+ if (beforeLines.length === 0)
54
+ return afterLines;
55
+ if (afterLines.length === 0)
56
+ return [];
57
+ const m = beforeLines.length;
58
+ const n = afterLines.length;
59
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
60
+ for (let i = 1; i <= m; i++) {
61
+ for (let j = 1; j <= n; j++) {
62
+ if (beforeLines[i - 1] === afterLines[j - 1]) {
63
+ dp[i][j] = dp[i - 1][j - 1] + 1;
64
+ }
65
+ else {
66
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
67
+ }
68
+ }
69
+ }
70
+ const added = [];
71
+ let i = m;
72
+ let j = n;
73
+ while (i > 0 && j > 0) {
74
+ if (beforeLines[i - 1] === afterLines[j - 1]) {
75
+ i--;
76
+ j--;
77
+ }
78
+ else if (dp[i - 1][j] >= dp[i][j - 1]) {
79
+ i--;
80
+ }
81
+ else {
82
+ added.push(afterLines[j - 1]);
83
+ j--;
84
+ }
85
+ }
86
+ while (j > 0) {
87
+ added.push(afterLines[j - 1]);
88
+ j--;
89
+ }
90
+ return added.reverse();
91
+ }
92
+ /**
93
+ * Extract added lines from a unified diff
94
+ */
95
+ extractAddedLinesFromDiff(diff) {
96
+ if (!diff)
97
+ return [];
98
+ const lines = diff.split(/\r?\n/);
99
+ const added = [];
100
+ for (const line of lines) {
101
+ if (line.startsWith('+') && !line.startsWith('+++')) {
102
+ const content = line.slice(1);
103
+ if (content.length > 0) {
104
+ added.push(content);
105
+ }
106
+ }
107
+ }
108
+ return added;
109
+ }
33
110
  /**
34
111
  * Normalize file path relative to project
35
112
  */
@@ -171,20 +171,29 @@ export class ClaudeScanner extends BaseScanner {
171
171
  let changeType = 'modify';
172
172
  let linesAdded = 0;
173
173
  let linesRemoved = 0;
174
+ let addedLines = [];
174
175
  if (writeOps.includes(toolName)) {
175
176
  changeType = oldContent ? 'modify' : 'create';
176
177
  linesAdded = this.countLines(newContent);
177
178
  linesRemoved = this.countLines(oldContent);
179
+ addedLines = this.extractNonEmptyLines(newContent);
178
180
  }
179
181
  else if (editOps.includes(toolName)) {
180
182
  changeType = 'modify';
181
183
  linesAdded = this.countLines(newContent);
182
184
  linesRemoved = this.countLines(oldContent);
185
+ if (oldContent && newContent) {
186
+ addedLines = this.diffAddedLines(oldContent, newContent);
187
+ }
188
+ else {
189
+ addedLines = this.extractNonEmptyLines(newContent);
190
+ }
183
191
  }
184
192
  else {
185
193
  // Unknown tool, try to extract what we can
186
194
  if (newContent) {
187
195
  linesAdded = this.countLines(newContent);
196
+ addedLines = this.extractNonEmptyLines(newContent);
188
197
  }
189
198
  }
190
199
  if (linesAdded === 0 && linesRemoved === 0)
@@ -197,6 +206,7 @@ export class ClaudeScanner extends BaseScanner {
197
206
  timestamp: timestamp ? new Date(timestamp) : new Date(),
198
207
  tool: this.tool,
199
208
  content: newContent,
209
+ addedLines,
200
210
  model,
201
211
  };
202
212
  }
@@ -53,6 +53,7 @@ export class CodexScanner extends BaseScanner {
53
53
  const changes = [];
54
54
  let sessionTimestamp = null;
55
55
  let sessionProjectPath = null;
56
+ let sessionModel;
56
57
  for (const entry of entries) {
57
58
  const payload = entry.payload || {};
58
59
  // Extract timestamp
@@ -68,6 +69,19 @@ export class CodexScanner extends BaseScanner {
68
69
  sessionProjectPath = payload.cwd;
69
70
  }
70
71
  }
72
+ // Extract model if available (turn_context/session_meta often include it)
73
+ if (!sessionModel) {
74
+ const rawModel = payload.model || payload.model_id || payload.modelId || payload.modelName;
75
+ if (typeof rawModel === 'string' && rawModel) {
76
+ sessionModel = rawModel;
77
+ }
78
+ else if (rawModel && typeof rawModel === 'object') {
79
+ const modelName = rawModel.name || rawModel.id || rawModel.model;
80
+ if (typeof modelName === 'string' && modelName) {
81
+ sessionModel = modelName;
82
+ }
83
+ }
84
+ }
71
85
  // Handle custom_tool_call (apply_patch) โ€” the primary way Codex writes files
72
86
  if (entry.type === 'response_item' && payload.type === 'custom_tool_call') {
73
87
  const patchChanges = this.parseApplyPatch(payload, projectPath, entry.timestamp);
@@ -129,6 +143,7 @@ export class CodexScanner extends BaseScanner {
129
143
  totalFilesChanged: new Set(changes.map(c => c.filePath)).size,
130
144
  totalLinesAdded: changes.reduce((sum, c) => sum + c.linesAdded, 0),
131
145
  totalLinesRemoved: changes.reduce((sum, c) => sum + c.linesRemoved, 0),
146
+ model: sessionModel,
132
147
  };
133
148
  }
134
149
  /**
@@ -165,11 +180,12 @@ export class CodexScanner extends BaseScanner {
165
180
  if (!input)
166
181
  return [];
167
182
  const changes = [];
168
- const lines = input.split('\n');
183
+ const lines = input.split(/\r?\n/);
169
184
  let currentFile = null;
170
185
  let changeType = 'modify';
171
186
  let linesAdded = 0;
172
187
  let linesRemoved = 0;
188
+ let addedLines = [];
173
189
  const flushFile = () => {
174
190
  if (currentFile && (linesAdded > 0 || linesRemoved > 0)) {
175
191
  const filePath = this.normalizePath(currentFile, projectPath);
@@ -180,13 +196,15 @@ export class CodexScanner extends BaseScanner {
180
196
  changeType,
181
197
  timestamp: timestamp ? new Date(timestamp) : new Date(),
182
198
  tool: this.tool,
183
- content: '',
199
+ content: addedLines.join('\n'),
200
+ addedLines,
184
201
  });
185
202
  }
186
203
  currentFile = null;
187
204
  linesAdded = 0;
188
205
  linesRemoved = 0;
189
206
  changeType = 'modify';
207
+ addedLines = [];
190
208
  };
191
209
  for (const line of lines) {
192
210
  if (line.startsWith('*** Update File:')) {
@@ -215,6 +233,7 @@ export class CodexScanner extends BaseScanner {
215
233
  else if (currentFile) {
216
234
  if (line.startsWith('+')) {
217
235
  linesAdded++;
236
+ addedLines.push(line.substring(1));
218
237
  }
219
238
  else if (line.startsWith('-')) {
220
239
  linesRemoved++;
@@ -240,9 +259,11 @@ export class CodexScanner extends BaseScanner {
240
259
  let changeType = 'modify';
241
260
  let linesAdded = 0;
242
261
  let linesRemoved = 0;
262
+ let addedLines = [];
243
263
  if (writeOps.includes(funcName)) {
244
264
  changeType = 'create';
245
265
  linesAdded = this.countLines(newContent);
266
+ addedLines = this.extractNonEmptyLines(newContent);
246
267
  }
247
268
  else if (editOps.includes(funcName)) {
248
269
  changeType = 'modify';
@@ -252,6 +273,13 @@ export class CodexScanner extends BaseScanner {
252
273
  const diffStats = this.parseDiff(args.diff);
253
274
  linesAdded = diffStats.added;
254
275
  linesRemoved = diffStats.removed;
276
+ addedLines = this.extractAddedLinesFromDiff(args.diff);
277
+ }
278
+ else if (oldContent && newContent) {
279
+ addedLines = this.diffAddedLines(oldContent, newContent);
280
+ }
281
+ else {
282
+ addedLines = this.extractNonEmptyLines(newContent);
255
283
  }
256
284
  }
257
285
  else {
@@ -267,6 +295,7 @@ export class CodexScanner extends BaseScanner {
267
295
  timestamp: timestamp ? new Date(timestamp) : new Date(),
268
296
  tool: this.tool,
269
297
  content: newContent,
298
+ addedLines,
270
299
  };
271
300
  }
272
301
  /**
@@ -230,19 +230,28 @@ export class GeminiScanner extends BaseScanner {
230
230
  // Calculate diff stats
231
231
  let linesAdded = 0;
232
232
  let linesRemoved = 0;
233
+ let addedLines = [];
233
234
  if (writeOps.includes(funcName)) {
234
235
  linesAdded = this.countLines(newContent);
235
236
  linesRemoved = this.countLines(oldContent); // Usually 0 for write, unless overwriting
237
+ addedLines = this.extractNonEmptyLines(newContent);
236
238
  }
237
239
  else if (editOps.includes(funcName)) {
238
240
  // Use LCS for edits to be accurate
239
241
  const stats = this.calculateDiffStats(oldContent, newContent);
240
242
  linesAdded = stats.added;
241
243
  linesRemoved = stats.removed;
244
+ if (oldContent && newContent) {
245
+ addedLines = this.diffAddedLines(oldContent, newContent);
246
+ }
247
+ else {
248
+ addedLines = this.extractNonEmptyLines(newContent);
249
+ }
242
250
  }
243
251
  else {
244
252
  if (newContent) {
245
253
  linesAdded = this.countLines(newContent);
254
+ addedLines = this.extractNonEmptyLines(newContent);
246
255
  }
247
256
  }
248
257
  if (linesAdded === 0 && linesRemoved === 0)
@@ -255,6 +264,7 @@ export class GeminiScanner extends BaseScanner {
255
264
  timestamp: timestamp || new Date(),
256
265
  tool: this.tool,
257
266
  content: newContent,
267
+ addedLines,
258
268
  model,
259
269
  };
260
270
  }
@@ -2,5 +2,4 @@ export { BaseScanner } from './base.js';
2
2
  export { ClaudeScanner } from './claude.js';
3
3
  export { CodexScanner } from './codex.js';
4
4
  export { GeminiScanner } from './gemini.js';
5
- export { AiderScanner } from './aider.js';
6
5
  export { OpencodeScanner } from './opencode.js';
@@ -2,5 +2,4 @@ export { BaseScanner } from './base.js';
2
2
  export { ClaudeScanner } from './claude.js';
3
3
  export { CodexScanner } from './codex.js';
4
4
  export { GeminiScanner } from './gemini.js';
5
- export { AiderScanner } from './aider.js';
6
5
  export { OpencodeScanner } from './opencode.js';
@@ -160,7 +160,8 @@ export class OpencodeScanner extends BaseScanner {
160
160
  : 'modify';
161
161
  // Use opencode's provided diff stats if available (most accurate)
162
162
  // additions/deletions are pre-calculated by opencode
163
- const linesAdded = typeof diff.additions === 'number' ? diff.additions : 0;
163
+ const addedLines = this.diffAddedLines(beforeContent, afterContent);
164
+ const linesAdded = typeof diff.additions === 'number' ? diff.additions : addedLines.length;
164
165
  const linesRemoved = typeof diff.deletions === 'number' ? diff.deletions : 0;
165
166
  return {
166
167
  filePath,
@@ -171,6 +172,7 @@ export class OpencodeScanner extends BaseScanner {
171
172
  tool: this.tool,
172
173
  model,
173
174
  content: afterContent,
175
+ addedLines,
174
176
  };
175
177
  }
176
178
  /**
package/dist/types.d.ts CHANGED
@@ -5,7 +5,6 @@ export declare enum AITool {
5
5
  CLAUDE_CODE = "claude",
6
6
  CODEX = "codex",
7
7
  GEMINI = "gemini",
8
- AIDER = "aider",
9
8
  OPENCODE = "opencode"
10
9
  }
11
10
  /**
@@ -19,6 +18,7 @@ export interface FileChange {
19
18
  timestamp: Date;
20
19
  tool: AITool;
21
20
  content?: string;
21
+ addedLines?: string[];
22
22
  model?: string;
23
23
  }
24
24
  /**
@@ -47,7 +47,6 @@ export interface ModelStats {
47
47
  linesAdded: number;
48
48
  linesRemoved: number;
49
49
  netLines: number;
50
- verifiedLines: number;
51
50
  }
52
51
  /**
53
52
  * Statistics for a single AI tool
@@ -61,7 +60,6 @@ export interface ToolStats {
61
60
  linesAdded: number;
62
61
  linesRemoved: number;
63
62
  netLines: number;
64
- verifiedLines: number;
65
63
  byModel: Map<string, ModelStats>;
66
64
  }
67
65
  /**
package/dist/types.js CHANGED
@@ -6,6 +6,5 @@ export var AITool;
6
6
  AITool["CLAUDE_CODE"] = "claude";
7
7
  AITool["CODEX"] = "codex";
8
8
  AITool["GEMINI"] = "gemini";
9
- AITool["AIDER"] = "aider";
10
9
  AITool["OPENCODE"] = "opencode";
11
10
  })(AITool || (AITool = {}));
package/package.json CHANGED
@@ -1,18 +1,12 @@
1
1
  {
2
2
  "name": "ai-credit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",
7
7
  "bin": {
8
8
  "ai-credit": "./dist/cli.js"
9
9
  },
10
- "scripts": {
11
- "build": "tsc",
12
- "start": "node dist/cli.js",
13
- "dev": "tsc && node dist/cli.js",
14
- "prepublishOnly": "npm run build"
15
- },
16
10
  "keywords": [
17
11
  "ai",
18
12
  "contribution",
@@ -42,5 +36,10 @@
42
36
  "@types/node": "^20.14.0",
43
37
  "tsx": "^4.15.0",
44
38
  "typescript": "^5.4.5"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "start": "node dist/cli.js",
43
+ "dev": "tsc && node dist/cli.js"
45
44
  }
46
- }
45
+ }