@kernel.chat/kbot 3.37.0 → 3.39.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.
Files changed (38) hide show
  1. package/README.md +111 -7
  2. package/dist/agent-teams.d.ts +55 -0
  3. package/dist/agent-teams.d.ts.map +1 -0
  4. package/dist/agent-teams.js +135 -0
  5. package/dist/agent-teams.js.map +1 -0
  6. package/dist/autonomous-contributor.d.ts +88 -0
  7. package/dist/autonomous-contributor.d.ts.map +1 -0
  8. package/dist/autonomous-contributor.js +862 -0
  9. package/dist/autonomous-contributor.js.map +1 -0
  10. package/dist/collective-network.d.ts +102 -0
  11. package/dist/collective-network.d.ts.map +1 -0
  12. package/dist/collective-network.js +634 -0
  13. package/dist/collective-network.js.map +1 -0
  14. package/dist/community-autopilot.d.ts +98 -0
  15. package/dist/community-autopilot.d.ts.map +1 -0
  16. package/dist/community-autopilot.js +676 -0
  17. package/dist/community-autopilot.js.map +1 -0
  18. package/dist/cross-device-sync.d.ts +36 -0
  19. package/dist/cross-device-sync.d.ts.map +1 -0
  20. package/dist/cross-device-sync.js +532 -0
  21. package/dist/cross-device-sync.js.map +1 -0
  22. package/dist/forge-marketplace-server.d.ts +23 -0
  23. package/dist/forge-marketplace-server.d.ts.map +1 -0
  24. package/dist/forge-marketplace-server.js +457 -0
  25. package/dist/forge-marketplace-server.js.map +1 -0
  26. package/dist/hooks-integration.d.ts +89 -0
  27. package/dist/hooks-integration.d.ts.map +1 -0
  28. package/dist/hooks-integration.js +457 -0
  29. package/dist/hooks-integration.js.map +1 -0
  30. package/dist/plugin/index.d.ts +71 -0
  31. package/dist/plugin/index.d.ts.map +1 -0
  32. package/dist/plugin/index.js +133 -0
  33. package/dist/plugin/index.js.map +1 -0
  34. package/dist/voice-loop.d.ts +59 -0
  35. package/dist/voice-loop.d.ts.map +1 -0
  36. package/dist/voice-loop.js +525 -0
  37. package/dist/voice-loop.js.map +1 -0
  38. package/package.json +1 -1
@@ -0,0 +1,862 @@
1
+ // kbot Autonomous Contributor — Point at any repo, analyze, propose fixes
2
+ //
3
+ // Clones any public GitHub repo, runs bootstrap + guardian analysis,
4
+ // identifies actionable improvements, and generates a contribution report
5
+ // with proposed fixes. v1 is analysis-only — no automatic PR creation.
6
+ //
7
+ // Usage:
8
+ // import { runAutonomousContributor, listGoodFirstIssues } from './autonomous-contributor.js'
9
+ // const report = await runAutonomousContributor('https://github.com/owner/repo')
10
+ // const issues = await listGoodFirstIssues('https://github.com/owner/repo')
11
+ import { existsSync, readFileSync, mkdirSync, rmSync, readdirSync, statSync, writeFileSync } from 'node:fs';
12
+ import { join, relative, extname } from 'node:path';
13
+ import { tmpdir, homedir } from 'node:os';
14
+ import { execSync } from 'node:child_process';
15
+ // ── Constants ──
16
+ const IGNORED_DIRS = new Set([
17
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt',
18
+ 'coverage', '.cache', '.turbo', '.parcel-cache', '__pycache__',
19
+ 'vendor', 'target', '.output', '.vercel', '.tox', '.mypy_cache',
20
+ '.pytest_cache', 'venv', '.venv', 'env',
21
+ ]);
22
+ const CODE_EXTENSIONS = new Set([
23
+ '.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.py', '.rs',
24
+ '.go', '.java', '.rb', '.php', '.swift', '.kt', '.c', '.cpp', '.h',
25
+ '.cs', '.vue', '.svelte', '.astro',
26
+ ]);
27
+ const SEVERITY_ORDER = {
28
+ 'info': 0,
29
+ 'warn': 1,
30
+ 'critical': 2,
31
+ };
32
+ const REPORT_DIR = join(homedir(), '.kbot', 'contributions');
33
+ // ── Helpers ──
34
+ function ensureDir(dir) {
35
+ if (!existsSync(dir))
36
+ mkdirSync(dir, { recursive: true });
37
+ }
38
+ function execQuiet(cmd, cwd, timeoutMs = 30000) {
39
+ try {
40
+ return execSync(cmd, {
41
+ encoding: 'utf-8',
42
+ timeout: timeoutMs,
43
+ stdio: ['pipe', 'pipe', 'pipe'],
44
+ cwd,
45
+ }).trim();
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /** Extract "owner/repo" from a GitHub URL or owner/repo string */
52
+ function parseRepoIdentifier(input) {
53
+ // Full URL: https://github.com/owner/repo or git@github.com:owner/repo.git
54
+ const httpsMatch = input.match(/github\.com\/([^/]+\/[^/.]+)/);
55
+ if (httpsMatch)
56
+ return httpsMatch[1].replace(/\.git$/, '');
57
+ const sshMatch = input.match(/github\.com:([^/]+\/[^/.]+)/);
58
+ if (sshMatch)
59
+ return sshMatch[1].replace(/\.git$/, '');
60
+ // Already in owner/repo format
61
+ if (/^[^/]+\/[^/]+$/.test(input))
62
+ return input;
63
+ throw new Error(`Cannot parse repo identifier from: ${input}`);
64
+ }
65
+ function cloneUrl(repoId) {
66
+ return `https://github.com/${repoId}.git`;
67
+ }
68
+ // ── File Walking ──
69
+ function walkCodeFiles(dir, maxFiles, files = []) {
70
+ if (files.length >= maxFiles)
71
+ return files;
72
+ let entries;
73
+ try {
74
+ entries = readdirSync(dir);
75
+ }
76
+ catch {
77
+ return files;
78
+ }
79
+ for (const entry of entries) {
80
+ if (files.length >= maxFiles)
81
+ break;
82
+ const fullPath = join(dir, entry);
83
+ let stats;
84
+ try {
85
+ stats = statSync(fullPath);
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ if (stats.isDirectory()) {
91
+ if (!IGNORED_DIRS.has(entry) && !entry.startsWith('.')) {
92
+ walkCodeFiles(fullPath, maxFiles, files);
93
+ }
94
+ }
95
+ else if (stats.isFile()) {
96
+ const ext = extname(entry).toLowerCase();
97
+ if (CODE_EXTENSIONS.has(ext) && stats.size < 500_000) {
98
+ files.push(fullPath);
99
+ }
100
+ }
101
+ }
102
+ return files;
103
+ }
104
+ function detectProjectProfile(rootDir) {
105
+ const profile = {
106
+ language: 'unknown',
107
+ framework: 'none',
108
+ hasTests: false,
109
+ hasCI: false,
110
+ hasReadme: false,
111
+ hasLicense: false,
112
+ hasContributing: false,
113
+ hasTypeConfig: false,
114
+ };
115
+ // Check for common files
116
+ profile.hasReadme = existsSync(join(rootDir, 'README.md')) || existsSync(join(rootDir, 'readme.md'));
117
+ profile.hasLicense = existsSync(join(rootDir, 'LICENSE')) || existsSync(join(rootDir, 'LICENSE.md'));
118
+ profile.hasContributing = existsSync(join(rootDir, 'CONTRIBUTING.md'));
119
+ profile.hasCI = existsSync(join(rootDir, '.github', 'workflows')) ||
120
+ existsSync(join(rootDir, '.circleci')) ||
121
+ existsSync(join(rootDir, '.travis.yml')) ||
122
+ existsSync(join(rootDir, 'Jenkinsfile'));
123
+ // Language detection
124
+ if (existsSync(join(rootDir, 'package.json'))) {
125
+ profile.language = 'javascript';
126
+ // Check for TypeScript
127
+ if (existsSync(join(rootDir, 'tsconfig.json'))) {
128
+ profile.language = 'typescript';
129
+ profile.hasTypeConfig = true;
130
+ }
131
+ // Framework detection from package.json
132
+ try {
133
+ const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
134
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
135
+ if (allDeps['next'])
136
+ profile.framework = 'next.js';
137
+ else if (allDeps['nuxt'] || allDeps['nuxt3'])
138
+ profile.framework = 'nuxt';
139
+ else if (allDeps['@angular/core'])
140
+ profile.framework = 'angular';
141
+ else if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
142
+ profile.framework = 'svelte';
143
+ else if (allDeps['vue'])
144
+ profile.framework = 'vue';
145
+ else if (allDeps['react'])
146
+ profile.framework = 'react';
147
+ else if (allDeps['express'])
148
+ profile.framework = 'express';
149
+ else if (allDeps['fastify'])
150
+ profile.framework = 'fastify';
151
+ else if (allDeps['hono'])
152
+ profile.framework = 'hono';
153
+ // Test detection
154
+ if (allDeps['jest'] || allDeps['vitest'] || allDeps['mocha'] || allDeps['ava'] ||
155
+ allDeps['@testing-library/react'] || allDeps['playwright'] || allDeps['cypress']) {
156
+ profile.hasTests = true;
157
+ }
158
+ }
159
+ catch { /* malformed package.json */ }
160
+ }
161
+ else if (existsSync(join(rootDir, 'Cargo.toml'))) {
162
+ profile.language = 'rust';
163
+ profile.hasTypeConfig = true; // Rust is always typed
164
+ profile.hasTests = true; // Rust has built-in tests
165
+ }
166
+ else if (existsSync(join(rootDir, 'go.mod'))) {
167
+ profile.language = 'go';
168
+ profile.hasTypeConfig = true; // Go is always typed
169
+ // Check for test files
170
+ const goTestOutput = execQuiet('find . -name "*_test.go" -maxdepth 3 | head -1', rootDir, 5000);
171
+ if (goTestOutput)
172
+ profile.hasTests = true;
173
+ }
174
+ else if (existsSync(join(rootDir, 'pyproject.toml')) || existsSync(join(rootDir, 'setup.py'))) {
175
+ profile.language = 'python';
176
+ if (existsSync(join(rootDir, 'mypy.ini')) || existsSync(join(rootDir, 'pyrightconfig.json'))) {
177
+ profile.hasTypeConfig = true;
178
+ }
179
+ // Check pyproject.toml for framework + test tools
180
+ try {
181
+ const pyproject = readFileSync(join(rootDir, 'pyproject.toml'), 'utf-8');
182
+ if (/django/i.test(pyproject))
183
+ profile.framework = 'django';
184
+ else if (/flask/i.test(pyproject))
185
+ profile.framework = 'flask';
186
+ else if (/fastapi/i.test(pyproject))
187
+ profile.framework = 'fastapi';
188
+ if (/pytest|unittest|nose/i.test(pyproject))
189
+ profile.hasTests = true;
190
+ }
191
+ catch { /* no pyproject */ }
192
+ }
193
+ else if (existsSync(join(rootDir, 'Gemfile'))) {
194
+ profile.language = 'ruby';
195
+ if (existsSync(join(rootDir, 'config', 'routes.rb')))
196
+ profile.framework = 'rails';
197
+ }
198
+ // Fallback test detection: check for test directories
199
+ if (!profile.hasTests) {
200
+ const testDirs = ['test', 'tests', '__tests__', 'spec', 'specs'];
201
+ for (const td of testDirs) {
202
+ if (existsSync(join(rootDir, td))) {
203
+ profile.hasTests = true;
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ return profile;
209
+ }
210
+ function scanTodos(files, rootDir) {
211
+ const todos = [];
212
+ const pattern = /\b(TODO|FIXME|HACK|XXX)\b[:\s]*(.*)/i;
213
+ for (const file of files) {
214
+ let source;
215
+ try {
216
+ source = readFileSync(file, 'utf-8');
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ const relPath = relative(rootDir, file);
222
+ const lines = source.split('\n');
223
+ for (let i = 0; i < lines.length; i++) {
224
+ const match = pattern.exec(lines[i]);
225
+ if (match) {
226
+ todos.push({
227
+ file: relPath,
228
+ line: i + 1,
229
+ text: match[2]?.trim() || match[0],
230
+ type: match[1].toUpperCase(),
231
+ });
232
+ }
233
+ }
234
+ }
235
+ return todos;
236
+ }
237
+ function scanComplexity(files, rootDir) {
238
+ const results = [];
239
+ const funcPattern = /(?:export\s+)?(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\s*\{)/;
240
+ for (const file of files) {
241
+ let source;
242
+ try {
243
+ source = readFileSync(file, 'utf-8');
244
+ }
245
+ catch {
246
+ continue;
247
+ }
248
+ const relPath = relative(rootDir, file);
249
+ const lines = source.split('\n');
250
+ let i = 0;
251
+ while (i < lines.length) {
252
+ const match = funcPattern.exec(lines[i]);
253
+ if (match) {
254
+ const funcName = match[1] || match[2] || match[3] || 'anonymous';
255
+ const startLine = i + 1;
256
+ let braceDepth = 0;
257
+ let funcStarted = false;
258
+ let bodyLines = 0;
259
+ let j = i;
260
+ while (j < lines.length) {
261
+ for (const ch of lines[j]) {
262
+ if (ch === '{') {
263
+ braceDepth++;
264
+ if (!funcStarted)
265
+ funcStarted = true;
266
+ }
267
+ else if (ch === '}') {
268
+ braceDepth--;
269
+ }
270
+ }
271
+ if (funcStarted)
272
+ bodyLines++;
273
+ if (funcStarted && braceDepth === 0)
274
+ break;
275
+ j++;
276
+ }
277
+ if (bodyLines > 60) {
278
+ results.push({ file: relPath, name: funcName, lineCount: bodyLines, startLine });
279
+ }
280
+ i = j + 1;
281
+ }
282
+ else {
283
+ i++;
284
+ }
285
+ }
286
+ }
287
+ results.sort((a, b) => b.lineCount - a.lineCount);
288
+ return results.slice(0, 20);
289
+ }
290
+ // ── Duplicate Pattern Scanner (simplified from codebase-guardian) ──
291
+ function normalizeLine(line) {
292
+ return line
293
+ .replace(/\/\/.*$/, '')
294
+ .replace(/\/\*[\s\S]*?\*\//g, '')
295
+ .replace(/#.*$/, '')
296
+ .replace(/\s+/g, ' ')
297
+ .trim();
298
+ }
299
+ function scanDuplicates(files, rootDir) {
300
+ const CHUNK_SIZE = 4;
301
+ const chunkMap = new Map();
302
+ const chunkOriginals = new Map();
303
+ for (const file of files) {
304
+ let source;
305
+ try {
306
+ source = readFileSync(file, 'utf-8');
307
+ }
308
+ catch {
309
+ continue;
310
+ }
311
+ const relPath = relative(rootDir, file);
312
+ const lines = source.split('\n').map(normalizeLine).filter(l => l.length > 10);
313
+ for (let i = 0; i <= lines.length - CHUNK_SIZE; i++) {
314
+ const chunk = lines.slice(i, i + CHUNK_SIZE).join('\n');
315
+ if (chunk.length > 30) {
316
+ if (!chunkMap.has(chunk)) {
317
+ chunkMap.set(chunk, new Set());
318
+ // Store original for reporting
319
+ const rawLines = source.split('\n');
320
+ for (let j = 0; j < rawLines.length - CHUNK_SIZE; j++) {
321
+ const normalized = rawLines.slice(j, j + CHUNK_SIZE)
322
+ .map(normalizeLine).filter(l => l.length > 10).join('\n');
323
+ if (normalized === chunk) {
324
+ chunkOriginals.set(chunk, rawLines.slice(j, j + CHUNK_SIZE).join('\n'));
325
+ break;
326
+ }
327
+ }
328
+ }
329
+ chunkMap.get(chunk).add(relPath);
330
+ }
331
+ }
332
+ }
333
+ const duplicates = [];
334
+ for (const [chunk, fileSet] of chunkMap) {
335
+ if (fileSet.size >= 3) {
336
+ duplicates.push({
337
+ pattern: (chunkOriginals.get(chunk) || chunk).slice(0, 200),
338
+ files: Array.from(fileSet),
339
+ occurrences: fileSet.size,
340
+ });
341
+ }
342
+ }
343
+ duplicates.sort((a, b) => b.occurrences - a.occurrences);
344
+ return duplicates.slice(0, 10);
345
+ }
346
+ // ── Typo Scanner ──
347
+ // Common programming typos in identifiers and comments
348
+ const COMMON_TYPOS = [
349
+ [/\brecieve\b/gi, 'receive'],
350
+ [/\boccured\b/gi, 'occurred'],
351
+ [/\bseperatel?y?\b/gi, 'separately'],
352
+ [/\bdefau[lt]{2,}s?\b/gi, 'default(s)'],
353
+ [/\baccidentaly\b/gi, 'accidentally'],
354
+ [/\bneccessary\b/gi, 'necessary'],
355
+ [/\boccurr?ance\b/gi, 'occurrence'],
356
+ [/\bwidht\b/gi, 'width'],
357
+ [/\bheigth\b/gi, 'height'],
358
+ [/\blegnth\b/gi, 'length'],
359
+ [/\bfunciton\b/gi, 'function'],
360
+ [/\bretrun\b/gi, 'return'],
361
+ [/\bparmeter\b/gi, 'parameter'],
362
+ [/\bargumnet\b/gi, 'argument'],
363
+ [/\benviroment\b/gi, 'environment'],
364
+ [/\bresponc?se\b/gi, 'response'],
365
+ [/\binitilaize\b/gi, 'initialize'],
366
+ [/\bsucess\b/gi, 'success'],
367
+ [/\babstarct\b/gi, 'abstract'],
368
+ [/\boveride\b/gi, 'override'],
369
+ [/\binterupts?\b/gi, 'interrupt(s)'],
370
+ [/\bwether\b/gi, 'whether'],
371
+ [/\bteh\b/gi, 'the'],
372
+ [/\badn\b/gi, 'and'],
373
+ ];
374
+ function scanTypos(files, rootDir) {
375
+ const typos = [];
376
+ for (const file of files) {
377
+ let source;
378
+ try {
379
+ source = readFileSync(file, 'utf-8');
380
+ }
381
+ catch {
382
+ continue;
383
+ }
384
+ const relPath = relative(rootDir, file);
385
+ const lines = source.split('\n');
386
+ for (let i = 0; i < lines.length; i++) {
387
+ const line = lines[i];
388
+ // Only check comments and string literals for typos
389
+ const isComment = /^\s*(\/\/|#|\*|\/\*)/.test(line);
390
+ const hasString = /['"`]/.test(line);
391
+ if (!isComment && !hasString)
392
+ continue;
393
+ for (const [pattern, fix] of COMMON_TYPOS) {
394
+ const match = pattern.exec(line);
395
+ if (match) {
396
+ typos.push({
397
+ file: relPath,
398
+ line: i + 1,
399
+ found: match[0],
400
+ suggestion: fix,
401
+ });
402
+ }
403
+ // Reset regex lastIndex for global patterns
404
+ pattern.lastIndex = 0;
405
+ }
406
+ }
407
+ }
408
+ return typos.slice(0, 30);
409
+ }
410
+ function scanMissingTypes(files, rootDir) {
411
+ const results = [];
412
+ // Only check .ts/.tsx files
413
+ const tsFiles = files.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
414
+ for (const file of tsFiles) {
415
+ let source;
416
+ try {
417
+ source = readFileSync(file, 'utf-8');
418
+ }
419
+ catch {
420
+ continue;
421
+ }
422
+ const relPath = relative(rootDir, file);
423
+ const lines = source.split('\n');
424
+ for (let i = 0; i < lines.length; i++) {
425
+ const line = lines[i];
426
+ // Exported function without return type
427
+ if (/^export\s+(async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/.test(line) &&
428
+ !/\)\s*:\s*\S/.test(line)) {
429
+ results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
430
+ }
431
+ // Exported const arrow function without return type (only top-level)
432
+ if (/^export\s+const\s+\w+\s*=\s*(async\s+)?\(/.test(line) &&
433
+ !/\)\s*:\s*\S/.test(line) &&
434
+ /=>\s*\{?\s*$/.test(line)) {
435
+ results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
436
+ }
437
+ // `any` type usage
438
+ if (/:\s*any\b/.test(line) && !/\/\/.*any/.test(line) && !/eslint-disable/.test(line)) {
439
+ results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
440
+ }
441
+ }
442
+ }
443
+ return results.slice(0, 30);
444
+ }
445
+ // ── Build Findings ──
446
+ function buildFindings(rootDir, files, profile) {
447
+ const findings = [];
448
+ // TODOs
449
+ const todos = scanTodos(files, rootDir);
450
+ for (const todo of todos) {
451
+ findings.push({
452
+ category: 'todo-removal',
453
+ severity: todo.type === 'FIXME' || todo.type === 'HACK' ? 'warn' : 'info',
454
+ title: `${todo.type}: ${todo.text.slice(0, 60)}`,
455
+ description: `${todo.type} comment at ${todo.file}:${todo.line} — "${todo.text}"`,
456
+ file: todo.file,
457
+ line: todo.line,
458
+ original: todo.text,
459
+ isSimpleFix: todo.type === 'TODO' && todo.text.length < 80,
460
+ });
461
+ }
462
+ // Typos
463
+ const typos = scanTypos(files, rootDir);
464
+ for (const typo of typos) {
465
+ findings.push({
466
+ category: 'typo',
467
+ severity: 'info',
468
+ title: `Typo: "${typo.found}" should be "${typo.suggestion}"`,
469
+ description: `Found "${typo.found}" at ${typo.file}:${typo.line}. Suggested correction: "${typo.suggestion}"`,
470
+ file: typo.file,
471
+ line: typo.line,
472
+ original: typo.found,
473
+ isSimpleFix: true,
474
+ });
475
+ }
476
+ // Missing types (TypeScript only)
477
+ if (profile.language === 'typescript') {
478
+ const missingTypes = scanMissingTypes(files, rootDir);
479
+ for (const mt of missingTypes) {
480
+ const isAny = mt.code.includes(': any');
481
+ findings.push({
482
+ category: 'missing-type',
483
+ severity: isAny ? 'warn' : 'info',
484
+ title: isAny
485
+ ? `Explicit \`any\` type at ${mt.file}:${mt.line}`
486
+ : `Missing return type at ${mt.file}:${mt.line}`,
487
+ description: isAny
488
+ ? `Replace explicit \`any\` with a proper type: ${mt.code}`
489
+ : `Exported function missing return type annotation: ${mt.code}`,
490
+ file: mt.file,
491
+ line: mt.line,
492
+ original: mt.code,
493
+ isSimpleFix: isAny,
494
+ });
495
+ }
496
+ }
497
+ // Complexity
498
+ const complex = scanComplexity(files, rootDir);
499
+ for (const fn of complex) {
500
+ findings.push({
501
+ category: 'complexity',
502
+ severity: fn.lineCount > 100 ? 'critical' : 'warn',
503
+ title: `Long function: ${fn.name} (${fn.lineCount} lines)`,
504
+ description: `${fn.file}:${fn.startLine} — function \`${fn.name}\` is ${fn.lineCount} lines long. Consider breaking into smaller functions.`,
505
+ file: fn.file,
506
+ line: fn.startLine,
507
+ isSimpleFix: false,
508
+ });
509
+ }
510
+ // Duplicates
511
+ const duplicates = scanDuplicates(files, rootDir);
512
+ for (const dup of duplicates) {
513
+ findings.push({
514
+ category: 'duplicate-pattern',
515
+ severity: dup.occurrences >= 5 ? 'critical' : 'warn',
516
+ title: `Repeated code block in ${dup.occurrences} files`,
517
+ description: `A 4-line code block appears in ${dup.occurrences} files: ${dup.files.slice(0, 3).join(', ')}${dup.files.length > 3 ? ` (+${dup.files.length - 3} more)` : ''}`,
518
+ file: dup.files[0],
519
+ original: dup.pattern,
520
+ isSimpleFix: false,
521
+ });
522
+ }
523
+ // Missing docs: exported functions without JSDoc
524
+ if (profile.language === 'typescript' || profile.language === 'javascript') {
525
+ let undocumented = 0;
526
+ for (const file of files.slice(0, 100)) {
527
+ let source;
528
+ try {
529
+ source = readFileSync(file, 'utf-8');
530
+ }
531
+ catch {
532
+ continue;
533
+ }
534
+ const relPath = relative(rootDir, file);
535
+ const lines = source.split('\n');
536
+ for (let i = 0; i < lines.length; i++) {
537
+ if (/^export\s+(async\s+)?function\s+\w+/.test(lines[i])) {
538
+ // Check if preceded by JSDoc
539
+ const prevLine = i > 0 ? lines[i - 1].trim() : '';
540
+ const prevPrevLine = i > 1 ? lines[i - 2].trim() : '';
541
+ if (!prevLine.endsWith('*/') && !prevPrevLine.endsWith('*/')) {
542
+ undocumented++;
543
+ if (undocumented <= 10) {
544
+ findings.push({
545
+ category: 'missing-docs',
546
+ severity: 'info',
547
+ title: `Exported function without JSDoc: ${relPath}:${i + 1}`,
548
+ description: `The exported function at ${relPath}:${i + 1} has no JSDoc comment. Adding documentation improves maintainability.`,
549
+ file: relPath,
550
+ line: i + 1,
551
+ original: lines[i].trim().slice(0, 80),
552
+ isSimpleFix: true,
553
+ });
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+ // Project-level findings
561
+ if (!profile.hasReadme) {
562
+ findings.push({
563
+ category: 'missing-docs',
564
+ severity: 'critical',
565
+ title: 'No README.md',
566
+ description: 'This project has no README. Every project needs a README explaining what it does and how to use it.',
567
+ file: 'README.md',
568
+ isSimpleFix: false,
569
+ });
570
+ }
571
+ if (!profile.hasLicense) {
572
+ findings.push({
573
+ category: 'other',
574
+ severity: 'warn',
575
+ title: 'No LICENSE file',
576
+ description: 'Without a license, others cannot legally use this code. Consider adding MIT, Apache 2.0, or another open-source license.',
577
+ file: 'LICENSE',
578
+ isSimpleFix: true,
579
+ });
580
+ }
581
+ if (!profile.hasContributing) {
582
+ findings.push({
583
+ category: 'missing-docs',
584
+ severity: 'info',
585
+ title: 'No CONTRIBUTING.md',
586
+ description: 'Adding a CONTRIBUTING.md helps new contributors understand how to get started.',
587
+ file: 'CONTRIBUTING.md',
588
+ isSimpleFix: true,
589
+ });
590
+ }
591
+ if (!profile.hasCI) {
592
+ findings.push({
593
+ category: 'other',
594
+ severity: 'warn',
595
+ title: 'No CI configuration detected',
596
+ description: 'No GitHub Actions, CircleCI, Travis, or Jenkins configuration found. CI helps catch regressions automatically.',
597
+ file: '.github/workflows',
598
+ isSimpleFix: false,
599
+ });
600
+ }
601
+ return findings;
602
+ }
603
+ // ── Build Proposed Fixes ──
604
+ function buildProposedFixes(findings) {
605
+ const fixes = [];
606
+ for (const finding of findings) {
607
+ if (!finding.isSimpleFix)
608
+ continue;
609
+ let changeSummary;
610
+ let estimatedReviewMinutes;
611
+ switch (finding.category) {
612
+ case 'todo-removal':
613
+ changeSummary = `Address the ${finding.original?.split(':')[0] || 'TODO'} at ${finding.file}:${finding.line ?? '?'}. Either implement the requested change or remove the stale comment.`;
614
+ estimatedReviewMinutes = 5;
615
+ break;
616
+ case 'typo':
617
+ changeSummary = `Fix typo: change "${finding.original}" to the correct spelling in ${finding.file}:${finding.line ?? '?'}.`;
618
+ estimatedReviewMinutes = 1;
619
+ break;
620
+ case 'missing-type':
621
+ changeSummary = `Add proper type annotation to replace \`any\` or add return type at ${finding.file}:${finding.line ?? '?'}.`;
622
+ estimatedReviewMinutes = 3;
623
+ break;
624
+ case 'missing-docs':
625
+ changeSummary = `Add JSDoc documentation for the exported function at ${finding.file}:${finding.line ?? '?'}.`;
626
+ estimatedReviewMinutes = 3;
627
+ break;
628
+ default:
629
+ changeSummary = `Address: ${finding.title}`;
630
+ estimatedReviewMinutes = 5;
631
+ break;
632
+ }
633
+ fixes.push({
634
+ finding,
635
+ description: finding.description,
636
+ changeSummary,
637
+ estimatedReviewMinutes,
638
+ });
639
+ }
640
+ // Sort: typos first (easiest), then missing-type, then todos, then docs
641
+ const categoryOrder = {
642
+ 'typo': 0,
643
+ 'missing-type': 1,
644
+ 'todo-removal': 2,
645
+ 'missing-docs': 3,
646
+ };
647
+ fixes.sort((a, b) => {
648
+ const aOrder = categoryOrder[a.finding.category] ?? 10;
649
+ const bOrder = categoryOrder[b.finding.category] ?? 10;
650
+ return aOrder - bOrder;
651
+ });
652
+ return fixes;
653
+ }
654
+ // ── Main: Autonomous Contributor ──
655
+ /**
656
+ * Point at any GitHub repo, clone it, analyze it, and generate a contribution
657
+ * report with findings and proposed fixes.
658
+ *
659
+ * v1 is analysis-only — no automatic PR creation. The report gives you
660
+ * everything needed to make targeted contributions.
661
+ */
662
+ export async function runAutonomousContributor(repoUrl, options = {}) {
663
+ const { maxFiles = 500, minSeverity = 'info', localPath, keepClone = false, } = options;
664
+ const repoId = parseRepoIdentifier(repoUrl);
665
+ const clonedAt = new Date().toISOString();
666
+ // ── Clone or use local path ──
667
+ let rootDir;
668
+ if (localPath) {
669
+ rootDir = localPath;
670
+ if (!existsSync(rootDir)) {
671
+ throw new Error(`Local path does not exist: ${rootDir}`);
672
+ }
673
+ console.log(`Analyzing local path: ${rootDir}`);
674
+ }
675
+ else {
676
+ rootDir = join(tmpdir(), `kbot-contributor-${repoId.replace('/', '-')}-${Date.now()}`);
677
+ console.log(`Cloning ${repoId} to ${rootDir}...`);
678
+ const cloneResult = execQuiet(`git clone --depth 1 --single-branch ${cloneUrl(repoId)} "${rootDir}"`, tmpdir(), 60000);
679
+ if (cloneResult === null && !existsSync(join(rootDir, '.git'))) {
680
+ throw new Error(`Failed to clone ${repoId}. Is the repository public and accessible?`);
681
+ }
682
+ console.log('Clone complete.');
683
+ }
684
+ try {
685
+ // ── Detect project profile ──
686
+ console.log('Detecting project profile...');
687
+ const profile = detectProjectProfile(rootDir);
688
+ console.log(` Language: ${profile.language}, Framework: ${profile.framework}`);
689
+ // ── Walk code files ──
690
+ console.log('Scanning files...');
691
+ const files = walkCodeFiles(rootDir, maxFiles);
692
+ console.log(` ${files.length} code files found`);
693
+ // ── Run analysis ──
694
+ console.log('Running analysis...');
695
+ const findings = buildFindings(rootDir, files, profile);
696
+ // Filter by minimum severity
697
+ const minSevOrder = SEVERITY_ORDER[minSeverity];
698
+ const filteredFindings = findings.filter(f => SEVERITY_ORDER[f.severity] >= minSevOrder);
699
+ // ── Build proposed fixes ──
700
+ const proposed_fixes = buildProposedFixes(filteredFindings);
701
+ // ── Build impact summary ──
702
+ const categories = {};
703
+ for (const f of filteredFindings) {
704
+ categories[f.category] = (categories[f.category] || 0) + 1;
705
+ }
706
+ const analyzedAt = new Date().toISOString();
707
+ const report = {
708
+ repo: repoId,
709
+ clonedAt,
710
+ analyzedAt,
711
+ language: profile.language,
712
+ framework: profile.framework,
713
+ filesScanned: files.length,
714
+ findings: filteredFindings,
715
+ proposed_fixes,
716
+ estimated_impact: {
717
+ totalFindings: filteredFindings.length,
718
+ simpleFixes: proposed_fixes.length,
719
+ complexFindings: filteredFindings.length - proposed_fixes.length,
720
+ categories,
721
+ },
722
+ projectHealth: {
723
+ hasReadme: profile.hasReadme,
724
+ hasLicense: profile.hasLicense,
725
+ hasTests: profile.hasTests,
726
+ hasCI: profile.hasCI,
727
+ hasContributing: profile.hasContributing,
728
+ hasTypeConfig: profile.hasTypeConfig,
729
+ },
730
+ };
731
+ // Save report
732
+ ensureDir(REPORT_DIR);
733
+ const reportFile = join(REPORT_DIR, `${repoId.replace('/', '-')}-${Date.now()}.json`);
734
+ writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf-8');
735
+ console.log(`Analysis complete: ${filteredFindings.length} findings, ` +
736
+ `${proposed_fixes.length} proposed fixes. Report saved to ${reportFile}`);
737
+ return report;
738
+ }
739
+ finally {
740
+ // Clean up clone unless keepClone is set or using localPath
741
+ if (!localPath && !keepClone && existsSync(rootDir)) {
742
+ try {
743
+ rmSync(rootDir, { recursive: true, force: true });
744
+ console.log('Cleaned up temporary clone.');
745
+ }
746
+ catch {
747
+ console.log(`Warning: could not clean up ${rootDir}`);
748
+ }
749
+ }
750
+ }
751
+ }
752
+ // ── Good First Issues ──
753
+ /**
754
+ * Fetch open issues labeled "good first issue" or "help wanted" from a GitHub repo.
755
+ * Uses the public GitHub API (no auth required for public repos).
756
+ */
757
+ export async function listGoodFirstIssues(repoUrl) {
758
+ const repoId = parseRepoIdentifier(repoUrl);
759
+ const labels = ['good first issue', 'help wanted', 'beginner-friendly', 'easy'];
760
+ const allIssues = [];
761
+ const seenNumbers = new Set();
762
+ for (const label of labels) {
763
+ const encoded = encodeURIComponent(label);
764
+ const url = `https://api.github.com/repos/${repoId}/issues?labels=${encoded}&state=open&per_page=20&sort=created&direction=desc`;
765
+ try {
766
+ const res = await fetch(url, {
767
+ headers: {
768
+ 'User-Agent': 'kbot-contributor/1.0',
769
+ 'Accept': 'application/vnd.github.v3+json',
770
+ },
771
+ signal: AbortSignal.timeout(10000),
772
+ });
773
+ if (!res.ok)
774
+ continue;
775
+ const items = await res.json();
776
+ for (const item of items) {
777
+ // Skip PRs (GitHub returns them in the issues endpoint)
778
+ if (item.pull_request)
779
+ continue;
780
+ if (seenNumbers.has(item.number))
781
+ continue;
782
+ seenNumbers.add(item.number);
783
+ allIssues.push({
784
+ number: item.number,
785
+ title: item.title,
786
+ url: item.html_url,
787
+ labels: item.labels.map(l => l.name),
788
+ created_at: item.created_at,
789
+ author: item.user.login,
790
+ comments: item.comments,
791
+ body_preview: (item.body || '').slice(0, 200),
792
+ });
793
+ }
794
+ }
795
+ catch {
796
+ // API error for this label, continue with others
797
+ }
798
+ }
799
+ // Sort by newest first
800
+ allIssues.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
801
+ return allIssues;
802
+ }
803
+ // ── Report Formatting ──
804
+ /**
805
+ * Format a ContributionReport as a readable markdown string.
806
+ */
807
+ export function formatContributionReport(report) {
808
+ const lines = [
809
+ `# Contribution Report: ${report.repo}`,
810
+ '',
811
+ `> Analyzed ${report.filesScanned} files | Language: ${report.language} | Framework: ${report.framework}`,
812
+ `> ${report.analyzedAt}`,
813
+ '',
814
+ '## Project Health',
815
+ '',
816
+ `| Check | Status |`,
817
+ `|-------|--------|`,
818
+ `| README | ${report.projectHealth.hasReadme ? 'Yes' : 'Missing'} |`,
819
+ `| LICENSE | ${report.projectHealth.hasLicense ? 'Yes' : 'Missing'} |`,
820
+ `| Tests | ${report.projectHealth.hasTests ? 'Yes' : 'None detected'} |`,
821
+ `| CI | ${report.projectHealth.hasCI ? 'Yes' : 'None detected'} |`,
822
+ `| CONTRIBUTING | ${report.projectHealth.hasContributing ? 'Yes' : 'Missing'} |`,
823
+ `| Type Config | ${report.projectHealth.hasTypeConfig ? 'Yes' : 'None'} |`,
824
+ '',
825
+ '## Impact Summary',
826
+ '',
827
+ `- **Total findings:** ${report.estimated_impact.totalFindings}`,
828
+ `- **Simple fixes (PR-ready):** ${report.estimated_impact.simpleFixes}`,
829
+ `- **Complex findings:** ${report.estimated_impact.complexFindings}`,
830
+ '',
831
+ '### By Category',
832
+ '',
833
+ ];
834
+ for (const [cat, count] of Object.entries(report.estimated_impact.categories).sort((a, b) => b[1] - a[1])) {
835
+ lines.push(`- ${cat}: ${count}`);
836
+ }
837
+ if (report.proposed_fixes.length > 0) {
838
+ lines.push('');
839
+ lines.push('## Proposed Fixes');
840
+ lines.push('');
841
+ for (const fix of report.proposed_fixes.slice(0, 20)) {
842
+ lines.push(`### ${fix.finding.title}`);
843
+ lines.push(`- **File:** ${fix.finding.file}${fix.finding.line ? `:${fix.finding.line}` : ''}`);
844
+ lines.push(`- **Change:** ${fix.changeSummary}`);
845
+ lines.push(`- **Review time:** ~${fix.estimatedReviewMinutes} min`);
846
+ lines.push('');
847
+ }
848
+ }
849
+ const criticalFindings = report.findings.filter(f => f.severity === 'critical');
850
+ if (criticalFindings.length > 0) {
851
+ lines.push('## Critical Findings');
852
+ lines.push('');
853
+ for (const f of criticalFindings) {
854
+ lines.push(`- **${f.title}** (${f.file}) — ${f.description}`);
855
+ }
856
+ lines.push('');
857
+ }
858
+ lines.push('---');
859
+ lines.push('*Generated by kbot Autonomous Contributor*');
860
+ return lines.join('\n');
861
+ }
862
+ //# sourceMappingURL=autonomous-contributor.js.map