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/LICENSE +21 -0
- package/README.md +330 -0
- package/dist/analyzer.d.ts +37 -0
- package/dist/analyzer.js +357 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +383 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/reporter.d.ts +60 -0
- package/dist/reporter.js +372 -0
- package/dist/scanners/aider.d.ts +35 -0
- package/dist/scanners/aider.js +194 -0
- package/dist/scanners/base.d.ts +56 -0
- package/dist/scanners/base.js +88 -0
- package/dist/scanners/claude.d.ts +30 -0
- package/dist/scanners/claude.js +203 -0
- package/dist/scanners/codex.d.ts +54 -0
- package/dist/scanners/codex.js +311 -0
- package/dist/scanners/gemini.d.ts +35 -0
- package/dist/scanners/gemini.js +318 -0
- package/dist/scanners/index.d.ts +6 -0
- package/dist/scanners/index.js +6 -0
- package/dist/scanners/opencode.d.ts +40 -0
- package/dist/scanners/opencode.js +210 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.js +11 -0
- package/package.json +46 -0
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();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
}
|