ai-credit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
4
+ import { fileURLToPath } from 'url';
5
+ import chalk from 'chalk';
6
+ import * as path from 'path';
7
+ import * as fs from 'fs';
8
+ import { ContributionAnalyzer } from './analyzer.js';
9
+ import { ConsoleReporter, JsonReporter, MarkdownReporter } from './reporter.js';
10
+ import { AITool } from './types.js';
11
+ // Worker thread: run analysis off the main thread so ora can animate
12
+ if (!isMainThread) {
13
+ const { projectPath, tools } = workerData;
14
+ const analyzer = new ContributionAnalyzer(projectPath);
15
+ const stats = analyzer.analyze(tools);
16
+ // ContributionStats contains Maps which can't be transferred directly
17
+ // Serialize to JSON-safe format
18
+ parentPort.postMessage(JSON.stringify(stats, (_key, value) => {
19
+ if (value instanceof Map)
20
+ return { __type: 'Map', entries: Array.from(value.entries()) };
21
+ return value;
22
+ }));
23
+ process.exit(0);
24
+ }
25
+ /**
26
+ * Run analyzer in a worker thread to keep the main thread free for spinner animation
27
+ */
28
+ function analyzeInWorker(projectPath, tools) {
29
+ return new Promise((resolve, reject) => {
30
+ const worker = new Worker(fileURLToPath(import.meta.url), {
31
+ workerData: { projectPath, tools },
32
+ });
33
+ worker.on('message', (msg) => {
34
+ const parsed = JSON.parse(msg, (_key, value) => {
35
+ if (value && value.__type === 'Map')
36
+ return new Map(value.entries);
37
+ return value;
38
+ });
39
+ // Restore Date objects from ISO strings
40
+ if (parsed.scanTime)
41
+ parsed.scanTime = new Date(parsed.scanTime);
42
+ if (parsed.sessions) {
43
+ for (const s of parsed.sessions) {
44
+ if (s.timestamp)
45
+ s.timestamp = new Date(s.timestamp);
46
+ if (s.changes)
47
+ for (const c of s.changes) {
48
+ if (c.timestamp)
49
+ c.timestamp = new Date(c.timestamp);
50
+ }
51
+ }
52
+ }
53
+ resolve(parsed);
54
+ });
55
+ worker.on('error', reject);
56
+ worker.on('exit', (code) => {
57
+ if (code !== 0)
58
+ reject(new Error(`Worker exited with code ${code}`));
59
+ });
60
+ });
61
+ }
62
+ /**
63
+ * Rainbow text animation (same algorithm as chalk-animation)
64
+ * Each character gets a hue from a full rainbow spread across the string,
65
+ * shifting by 5 degrees per frame for smooth flow.
66
+ */
67
+ function rainbowText(str, frame) {
68
+ const len = str.length;
69
+ if (len === 0)
70
+ return str;
71
+ let result = '';
72
+ for (let i = 0; i < len; i++) {
73
+ if (str[i] === ' ') {
74
+ result += ' ';
75
+ continue;
76
+ }
77
+ const hue = ((i / len) * 360 - frame * 5 % 360 + 360) % 360;
78
+ const [r, g, b] = hsvToRgb(hue, 1, 1);
79
+ result += chalk.rgb(r, g, b)(str[i]);
80
+ }
81
+ return result;
82
+ }
83
+ function hsvToRgb(h, s, v) {
84
+ const c = v * s;
85
+ const x = c * (1 - Math.abs((h / 60) % 2 - 1));
86
+ const m = v - c;
87
+ let r = 0, g = 0, b = 0;
88
+ if (h < 60) {
89
+ r = c;
90
+ g = x;
91
+ }
92
+ else if (h < 120) {
93
+ r = x;
94
+ g = c;
95
+ }
96
+ else if (h < 180) {
97
+ g = c;
98
+ b = x;
99
+ }
100
+ else if (h < 240) {
101
+ g = x;
102
+ b = c;
103
+ }
104
+ else if (h < 300) {
105
+ r = x;
106
+ b = c;
107
+ }
108
+ else {
109
+ r = c;
110
+ b = x;
111
+ }
112
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
113
+ }
114
+ function startRainbowLoading(text) {
115
+ let frame = 0;
116
+ process.stderr.write('\u001B[?25l'); // hide cursor
117
+ const interval = setInterval(() => {
118
+ process.stderr.write(`\r${rainbowText(text, frame)}`);
119
+ frame++;
120
+ }, 15);
121
+ return {
122
+ stop() {
123
+ clearInterval(interval);
124
+ process.stderr.write(`\r${' '.repeat(text.length)}\r`);
125
+ process.stderr.write('\u001B[?25h'); // show cursor
126
+ },
127
+ fail(msg) {
128
+ clearInterval(interval);
129
+ process.stderr.write(`\r${chalk.red('✖')} ${msg}\n`);
130
+ process.stderr.write('\u001B[?25h');
131
+ },
132
+ };
133
+ }
134
+ const TOOL_MAP = {
135
+ claude: AITool.CLAUDE_CODE,
136
+ codex: AITool.CODEX,
137
+ gemini: AITool.GEMINI,
138
+ aider: AITool.AIDER,
139
+ opencode: AITool.OPENCODE,
140
+ };
141
+ const TOOL_INFO = {
142
+ [AITool.CLAUDE_CODE]: { name: 'Claude Code', path: '~/.claude/projects/' },
143
+ [AITool.CODEX]: { name: 'Codex CLI', path: '~/.codex/sessions/' },
144
+ [AITool.GEMINI]: { name: 'Gemini CLI', path: '~/.gemini/tmp/' },
145
+ [AITool.AIDER]: { name: 'Aider', path: '.aider.chat.history.md' },
146
+ [AITool.OPENCODE]: { name: 'Opencode', path: '~/.local/share/opencode/' },
147
+ };
148
+ /**
149
+ * Parse tool string into array of AITool enums
150
+ */
151
+ function parseTools(toolStr) {
152
+ if (!toolStr || toolStr.toLowerCase() === 'all') {
153
+ return undefined;
154
+ }
155
+ const tools = [];
156
+ for (const t of toolStr.toLowerCase().split(',')) {
157
+ const trimmed = t.trim();
158
+ if (trimmed in TOOL_MAP) {
159
+ tools.push(TOOL_MAP[trimmed]);
160
+ }
161
+ }
162
+ return tools.length > 0 ? tools : undefined;
163
+ }
164
+ const program = new Command();
165
+ program
166
+ .name('ai-credit')
167
+ .description('Track and analyze AI coding assistant contributions')
168
+ .version('1.0.0');
169
+ // Main scan command
170
+ program
171
+ .command('scan [path]')
172
+ .description('Scan repository for AI contributions')
173
+ .option('-f, --format <format>', 'Output format (console, json, markdown)', 'console')
174
+ .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')
176
+ .option('-v, --verbose', 'Show detailed output including files and timeline')
177
+ .action(async (repoPath = '.', options) => {
178
+ const resolvedPath = path.resolve(repoPath);
179
+ if (!fs.existsSync(resolvedPath)) {
180
+ console.error(chalk.red(`Error: Path '${repoPath}' does not exist.`));
181
+ process.exit(1);
182
+ }
183
+ const spinner = startRainbowLoading('Analyzing repository...');
184
+ try {
185
+ const tools = parseTools(options.tools);
186
+ const stats = await analyzeInWorker(resolvedPath, tools);
187
+ spinner.stop();
188
+ const format = options.format;
189
+ if (format === 'console') {
190
+ const reporter = new ConsoleReporter();
191
+ reporter.printSummary(stats);
192
+ if (options.verbose) {
193
+ reporter.printFiles(stats);
194
+ reporter.printTimeline(stats);
195
+ }
196
+ }
197
+ else if (format === 'json') {
198
+ const reporter = new JsonReporter();
199
+ if (options.output) {
200
+ reporter.save(stats, options.output);
201
+ console.log(chalk.green(`Report saved to ${options.output}`));
202
+ }
203
+ else {
204
+ console.log(reporter.generate(stats));
205
+ }
206
+ }
207
+ else if (format === 'markdown') {
208
+ const reporter = new MarkdownReporter();
209
+ if (options.output) {
210
+ reporter.save(stats, options.output);
211
+ console.log(chalk.green(`Report saved to ${options.output}`));
212
+ }
213
+ else {
214
+ console.log(reporter.generate(stats));
215
+ }
216
+ }
217
+ }
218
+ catch (error) {
219
+ spinner.fail('Analysis failed');
220
+ console.error(chalk.red(`Error: ${error}`));
221
+ process.exit(1);
222
+ }
223
+ });
224
+ // List command
225
+ program
226
+ .command('list')
227
+ .description('List detected AI tools with available data')
228
+ .action(() => {
229
+ console.log();
230
+ console.log(chalk.bold('🔍 Detected AI Tools'));
231
+ console.log();
232
+ const analyzer = new ContributionAnalyzer('.');
233
+ const available = analyzer.getAvailableTools();
234
+ for (const tool of Object.values(AITool)) {
235
+ const info = TOOL_INFO[tool];
236
+ const status = available.includes(tool)
237
+ ? chalk.green('✓ Available')
238
+ : chalk.dim('✗ Not found');
239
+ console.log(` ${info.name.padEnd(15)} ${info.path.padEnd(30)} ${status}`);
240
+ }
241
+ console.log();
242
+ });
243
+ // Files command
244
+ program
245
+ .command('files [path]')
246
+ .description('Show file-level AI contribution details')
247
+ .option('-n, --limit <number>', 'Number of files to show', '20')
248
+ .action(async (repoPath = '.', options) => {
249
+ const resolvedPath = path.resolve(repoPath);
250
+ const spinner = startRainbowLoading('Analyzing files...');
251
+ try {
252
+ const stats = await analyzeInWorker(resolvedPath);
253
+ spinner.stop();
254
+ const reporter = new ConsoleReporter();
255
+ reporter.printFiles(stats, parseInt(options.limit, 10));
256
+ }
257
+ catch (error) {
258
+ spinner.fail('Analysis failed');
259
+ console.error(chalk.red(`Error: ${error}`));
260
+ process.exit(1);
261
+ }
262
+ });
263
+ // History command
264
+ program
265
+ .command('history [path]')
266
+ .description('Show AI contribution history/timeline')
267
+ .option('-n, --limit <number>', 'Number of entries to show', '20')
268
+ .action(async (repoPath = '.', options) => {
269
+ const resolvedPath = path.resolve(repoPath);
270
+ const spinner = startRainbowLoading('Loading history...');
271
+ try {
272
+ const stats = await analyzeInWorker(resolvedPath);
273
+ spinner.stop();
274
+ const reporter = new ConsoleReporter();
275
+ reporter.printTimeline(stats, parseInt(options.limit, 10));
276
+ }
277
+ catch (error) {
278
+ spinner.fail('Analysis failed');
279
+ console.error(chalk.red(`Error: ${error}`));
280
+ process.exit(1);
281
+ }
282
+ });
283
+ // Sessions command
284
+ program
285
+ .command('sessions [path]')
286
+ .description('List all AI sessions for the repository')
287
+ .option('-t, --tools <tools>', 'AI tools to include', 'all')
288
+ .action(async (repoPath = '.', options) => {
289
+ const resolvedPath = path.resolve(repoPath);
290
+ const analyzer = new ContributionAnalyzer(resolvedPath);
291
+ const tools = parseTools(options.tools);
292
+ const sessions = analyzer.scanAllSessions(tools);
293
+ console.log();
294
+ console.log(chalk.bold(`📋 AI Sessions for ${resolvedPath}`));
295
+ console.log();
296
+ if (sessions.length === 0) {
297
+ console.log(chalk.yellow('No sessions found.'));
298
+ return;
299
+ }
300
+ const toolColors = {
301
+ [AITool.CLAUDE_CODE]: chalk.hex('#D97757'),
302
+ [AITool.CODEX]: chalk.hex('#00A67E'),
303
+ [AITool.GEMINI]: chalk.hex('#4796E3'),
304
+ [AITool.AIDER]: chalk.hex('#D93B3B'),
305
+ [AITool.OPENCODE]: chalk.yellow,
306
+ };
307
+ // Show last 20 sessions
308
+ const recentSessions = sessions.slice(-20);
309
+ for (const session of recentSessions) {
310
+ const color = toolColors[session.tool] || chalk.white;
311
+ const toolName = TOOL_INFO[session.tool].name;
312
+ const date = session.timestamp.toLocaleString('en-US', {
313
+ year: 'numeric',
314
+ month: '2-digit',
315
+ day: '2-digit',
316
+ hour: '2-digit',
317
+ minute: '2-digit',
318
+ });
319
+ console.log(` ${color(toolName.padEnd(12))} ` +
320
+ `${chalk.dim(date)} ` +
321
+ `Files: ${session.totalFilesChanged.toString().padStart(3)} ` +
322
+ `Lines: ${chalk.green(`+${session.totalLinesAdded}`)}`);
323
+ }
324
+ console.log();
325
+ console.log(chalk.dim(`Total: ${sessions.length} sessions`));
326
+ console.log();
327
+ });
328
+ // Default command (scan current directory)
329
+ program
330
+ .argument('[path]', 'Repository path to analyze', '.')
331
+ .option('-f, --format <format>', 'Output format (console, json, markdown)', 'console')
332
+ .option('-o, --output <file>', 'Output file path')
333
+ .option('-t, --tools <tools>', 'AI tools to analyze', 'all')
334
+ .option('-v, --verbose', 'Show detailed output')
335
+ .action(async (repoPath, options) => {
336
+ // If no subcommand is provided, run scan
337
+ const resolvedPath = path.resolve(repoPath);
338
+ if (!fs.existsSync(resolvedPath)) {
339
+ console.error(chalk.red(`Error: Path '${repoPath}' does not exist.`));
340
+ process.exit(1);
341
+ }
342
+ const spinner = startRainbowLoading('Analyzing repository...');
343
+ try {
344
+ const tools = parseTools(options.tools);
345
+ const stats = await analyzeInWorker(resolvedPath, tools);
346
+ spinner.stop();
347
+ const format = options.format;
348
+ if (format === 'console') {
349
+ const reporter = new ConsoleReporter();
350
+ reporter.printSummary(stats);
351
+ if (options.verbose) {
352
+ reporter.printFiles(stats);
353
+ reporter.printTimeline(stats);
354
+ }
355
+ }
356
+ else if (format === 'json') {
357
+ const reporter = new JsonReporter();
358
+ if (options.output) {
359
+ reporter.save(stats, options.output);
360
+ console.log(chalk.green(`Report saved to ${options.output}`));
361
+ }
362
+ else {
363
+ console.log(reporter.generate(stats));
364
+ }
365
+ }
366
+ else if (format === 'markdown') {
367
+ const reporter = new MarkdownReporter();
368
+ if (options.output) {
369
+ reporter.save(stats, options.output);
370
+ console.log(chalk.green(`Report saved to ${options.output}`));
371
+ }
372
+ else {
373
+ console.log(reporter.generate(stats));
374
+ }
375
+ }
376
+ }
377
+ catch (error) {
378
+ spinner.fail('Analysis failed');
379
+ console.error(chalk.red(`Error: ${error}`));
380
+ process.exit(1);
381
+ }
382
+ });
383
+ program.parse();
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './analyzer.js';
3
+ export * from './reporter.js';
4
+ export * from './scanners/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './analyzer.js';
3
+ export * from './reporter.js';
4
+ export * from './scanners/index.js';
@@ -0,0 +1,60 @@
1
+ import { ContributionStats } from './types.js';
2
+ /**
3
+ * Console reporter for terminal output
4
+ */
5
+ export declare class ConsoleReporter {
6
+ /**
7
+ * Print the full summary report
8
+ */
9
+ printSummary(stats: ContributionStats): void;
10
+ /**
11
+ * Print report header
12
+ */
13
+ private printHeader;
14
+ /**
15
+ * Print overview statistics
16
+ */
17
+ private printOverview;
18
+ /**
19
+ * Print breakdown by AI tool
20
+ */
21
+ private printToolBreakdown;
22
+ /**
23
+ * Print distribution pie chart showing all code proportions
24
+ */
25
+ private printDistributionBar;
26
+ /**
27
+ * Print file-level statistics
28
+ */
29
+ printFiles(stats: ContributionStats, limit?: number): void;
30
+ /**
31
+ * Print timeline of AI activity
32
+ */
33
+ printTimeline(stats: ContributionStats, limit?: number): void;
34
+ }
35
+ /**
36
+ * JSON reporter for structured output
37
+ */
38
+ export declare class JsonReporter {
39
+ /**
40
+ * Generate JSON report
41
+ */
42
+ generate(stats: ContributionStats): string;
43
+ /**
44
+ * Save JSON report to file
45
+ */
46
+ save(stats: ContributionStats, outputPath: string): void;
47
+ }
48
+ /**
49
+ * Markdown reporter for documentation
50
+ */
51
+ export declare class MarkdownReporter {
52
+ /**
53
+ * Generate Markdown report
54
+ */
55
+ generate(stats: ContributionStats): string;
56
+ /**
57
+ * Save Markdown report to file
58
+ */
59
+ save(stats: ContributionStats, outputPath: string): void;
60
+ }