@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,676 @@
1
+ // kbot Community Autopilot — Autonomous community management daemon
2
+ //
3
+ // Wires the community manager, FAQ system, and GitHub monitoring into one
4
+ // continuous loop. Runs as a background daemon: triages issues, reviews PRs,
5
+ // answers questions, generates digests, and welcomes new contributors.
6
+ //
7
+ // Usage:
8
+ // import { startAutopilot, stopAutopilot, runCommunityAutopilot } from './community-autopilot.js'
9
+ //
10
+ // // One-shot cycle
11
+ // const result = await runCommunityAutopilot({ github_repo: 'owner/repo', discord_webhook: '...' })
12
+ //
13
+ // // Daemon mode
14
+ // startAutopilot({ github_repo: 'owner/repo', discord_webhook: '...', check_interval_ms: 300000 })
15
+ // // ... later:
16
+ // stopAutopilot()
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { execSync } from 'node:child_process';
21
+ // ── Paths ──
22
+ const AUTOPILOT_DIR = join(homedir(), '.kbot', 'autopilot');
23
+ const STATE_PATH = join(AUTOPILOT_DIR, 'state.json');
24
+ const FAQ_PATH = join(homedir(), '.kbot', 'community-faq.json');
25
+ const LOG_PATH = join(AUTOPILOT_DIR, 'autopilot.log');
26
+ const DIGEST_DIR = join(AUTOPILOT_DIR, 'digests');
27
+ function defaultState() {
28
+ return {
29
+ triagedIssues: [],
30
+ reviewedPRs: [],
31
+ answeredIssues: [],
32
+ welcomedUsers: [],
33
+ lastDigestDate: '',
34
+ knownContributors: [],
35
+ cycleCount: 0,
36
+ };
37
+ }
38
+ // ── Helpers ──
39
+ function ensureDir(dir) {
40
+ if (!existsSync(dir))
41
+ mkdirSync(dir, { recursive: true });
42
+ }
43
+ function log(msg) {
44
+ ensureDir(AUTOPILOT_DIR);
45
+ const line = `[${new Date().toISOString().slice(0, 19)}] ${msg}\n`;
46
+ appendFileSync(LOG_PATH, line);
47
+ console.log(`[autopilot] ${msg}`);
48
+ }
49
+ function loadJsonSafe(path, fallback) {
50
+ if (!existsSync(path))
51
+ return fallback;
52
+ try {
53
+ return JSON.parse(readFileSync(path, 'utf-8'));
54
+ }
55
+ catch {
56
+ return fallback;
57
+ }
58
+ }
59
+ function saveJson(path, data) {
60
+ const dir = path.replace(/\/[^/]+$/, '');
61
+ ensureDir(dir);
62
+ writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8');
63
+ }
64
+ function loadState() {
65
+ return loadJsonSafe(STATE_PATH, defaultState());
66
+ }
67
+ function saveState(state) {
68
+ saveJson(STATE_PATH, state);
69
+ }
70
+ // ── GitHub API ──
71
+ async function ghFetch(endpoint, repo) {
72
+ const url = `https://api.github.com/repos/${repo}${endpoint}`;
73
+ try {
74
+ const res = await fetch(url, {
75
+ headers: {
76
+ 'Accept': 'application/vnd.github.v3+json',
77
+ 'User-Agent': 'kbot-autopilot/1.0',
78
+ },
79
+ signal: AbortSignal.timeout(15000),
80
+ });
81
+ if (!res.ok)
82
+ return null;
83
+ return await res.json();
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function ghCliAvailable() {
90
+ try {
91
+ execSync('gh auth status', { timeout: 5000, stdio: 'pipe' });
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function ghCliComment(repo, issueNumber, body) {
99
+ try {
100
+ execSync(`gh issue comment ${issueNumber} --repo ${repo} --body ${JSON.stringify(body)}`, { timeout: 15000, stdio: 'pipe' });
101
+ return true;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ function ghCliPRComment(repo, prNumber, body) {
108
+ try {
109
+ execSync(`gh pr comment ${prNumber} --repo ${repo} --body ${JSON.stringify(body)}`, { timeout: 15000, stdio: 'pipe' });
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ // ── Discord ──
117
+ async function postToDiscord(webhookUrl, content) {
118
+ try {
119
+ const truncated = content.length > 1900
120
+ ? content.slice(0, 1900) + '\n\n... (truncated)'
121
+ : content;
122
+ const res = await fetch(webhookUrl, {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json' },
125
+ body: JSON.stringify({ content: truncated }),
126
+ signal: AbortSignal.timeout(10000),
127
+ });
128
+ return res.ok;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ // ── 1. Issue Triage ──
135
+ function classifyIssue(issue) {
136
+ const title = issue.title.toLowerCase();
137
+ const body = (issue.body || '').toLowerCase();
138
+ const text = `${title} ${body}`;
139
+ // Already labeled — skip
140
+ if (issue.labels.length > 0) {
141
+ return { label: '', response: '' };
142
+ }
143
+ // Bug patterns
144
+ if (/\b(bug|error|crash|broken|segfault|exception|traceback|panic|undefined is not)\b/.test(text)) {
145
+ return {
146
+ label: 'bug',
147
+ response: `Thanks for reporting this, @${issue.user.login}! To help us investigate:\n\n- Steps to reproduce\n- Expected vs actual behavior\n- Environment details (OS, runtime version)\n- Error message or stack trace if available\n\nWe'll look into this.`,
148
+ };
149
+ }
150
+ // Feature request patterns
151
+ if (/\b(feature|request|add|support|would be nice|enhancement|proposal|suggestion|wish)\b/.test(text)) {
152
+ return {
153
+ label: 'enhancement',
154
+ response: `Thanks for the suggestion, @${issue.user.login}! We've tagged this as an enhancement for review. If others find this useful, please add a thumbs-up reaction to help us prioritize.`,
155
+ };
156
+ }
157
+ // Question patterns
158
+ if (/\b(how|why|question|help|docs|documentation|what does|where is|can i|is it possible)\b/.test(text) || title.includes('?')) {
159
+ return {
160
+ label: 'question',
161
+ response: `Hi @${issue.user.login}! This looks like a question. Let me check if we have an answer in our docs. If this is still unresolved after a few days, we'll follow up.`,
162
+ };
163
+ }
164
+ // Performance patterns
165
+ if (/\b(slow|performance|memory|leak|cpu|latency|timeout|hang)\b/.test(text)) {
166
+ return {
167
+ label: 'performance',
168
+ response: `Thanks for flagging this, @${issue.user.login}! Performance issues are important to us. Could you share:\n\n- What operation is slow?\n- How long does it take vs expected?\n- System specs (CPU, RAM)\n- Repro steps if possible`,
169
+ };
170
+ }
171
+ // Documentation patterns
172
+ if (/\b(typo|docs|readme|documentation|spelling|grammar|outdated)\b/.test(text)) {
173
+ return {
174
+ label: 'documentation',
175
+ response: `Thanks, @${issue.user.login}! Documentation improvements are always welcome. Feel free to open a PR if you'd like to fix this directly.`,
176
+ };
177
+ }
178
+ // Default
179
+ return {
180
+ label: 'needs-triage',
181
+ response: `Thanks for opening this, @${issue.user.login}! We'll review it shortly.`,
182
+ };
183
+ }
184
+ async function triageNewIssues(repo, state, useGhCli) {
185
+ const results = [];
186
+ const issues = await ghFetch('/issues?state=open&sort=created&direction=desc&per_page=20', repo);
187
+ if (!issues)
188
+ return results;
189
+ // Filter to pure issues (not PRs), created in the last 24h, not already triaged
190
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
191
+ const triagedSet = new Set(state.triagedIssues);
192
+ const freshIssues = issues.filter(i => !i.pull_request &&
193
+ i.created_at >= oneDayAgo &&
194
+ !triagedSet.has(i.number));
195
+ for (const issue of freshIssues) {
196
+ const { label, response } = classifyIssue(issue);
197
+ if (!label || !response)
198
+ continue;
199
+ results.push({
200
+ issue: issue.number,
201
+ title: issue.title,
202
+ label,
203
+ response,
204
+ url: issue.html_url,
205
+ });
206
+ // Post comment if gh CLI is available
207
+ if (useGhCli) {
208
+ const posted = ghCliComment(repo, issue.number, response);
209
+ log(posted
210
+ ? `Triaged #${issue.number} as ${label}`
211
+ : `Triage draft for #${issue.number} (could not post)`);
212
+ }
213
+ else {
214
+ log(`Triage draft for #${issue.number} (gh CLI not available)`);
215
+ }
216
+ state.triagedIssues.push(issue.number);
217
+ }
218
+ // Keep only last 500 triaged issue numbers
219
+ if (state.triagedIssues.length > 500) {
220
+ state.triagedIssues = state.triagedIssues.slice(-500);
221
+ }
222
+ return results;
223
+ }
224
+ // ── 2. PR Review ──
225
+ async function reviewNewPRs(repo, state, useGhCli) {
226
+ const results = [];
227
+ const prs = await ghFetch('/pulls?state=open&sort=created&direction=desc&per_page=15', repo);
228
+ if (!prs)
229
+ return results;
230
+ const reviewedSet = new Set(state.reviewedPRs);
231
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
232
+ const freshPRs = prs.filter(p => p.created_at >= oneDayAgo &&
233
+ !reviewedSet.has(p.number));
234
+ for (const pr of freshPRs) {
235
+ let status;
236
+ let comment;
237
+ if (pr.draft) {
238
+ status = 'draft';
239
+ comment = `Thanks for starting this PR, @${pr.user.login}! Let us know when it's ready for review by marking it as ready.`;
240
+ }
241
+ else {
242
+ // Check CI status
243
+ const checks = await ghFetch(`/commits/${pr.html_url.split('/').pop()}/check-runs`, repo);
244
+ if (!checks || !checks.check_runs || checks.check_runs.length === 0) {
245
+ status = 'pending-ci';
246
+ comment = `Thanks for this PR, @${pr.user.login}! We'll review it once CI results are in.`;
247
+ }
248
+ else {
249
+ const failed = checks.check_runs.some(c => c.conclusion === 'failure');
250
+ const pending = checks.check_runs.some(c => c.status !== 'completed');
251
+ if (failed) {
252
+ const failedNames = checks.check_runs
253
+ .filter(c => c.conclusion === 'failure')
254
+ .map(c => c.name)
255
+ .join(', ');
256
+ status = 'ci-failing';
257
+ comment = `Thanks for this PR, @${pr.user.login}! It looks like some CI checks are failing: ${failedNames}. Could you take a look and push a fix? We'll review once CI is green.`;
258
+ }
259
+ else if (pending) {
260
+ status = 'pending-ci';
261
+ comment = `Thanks for this PR, @${pr.user.login}! CI is still running. We'll review once all checks pass.`;
262
+ }
263
+ else {
264
+ status = 'ci-passing';
265
+ comment = `Thanks for this PR, @${pr.user.login}! CI is passing. A maintainer will review this soon.`;
266
+ }
267
+ }
268
+ }
269
+ results.push({
270
+ pr: pr.number,
271
+ title: pr.title,
272
+ author: pr.user.login,
273
+ status,
274
+ comment,
275
+ url: pr.html_url,
276
+ });
277
+ // Post comment
278
+ if (useGhCli) {
279
+ const posted = ghCliPRComment(repo, pr.number, comment);
280
+ log(posted
281
+ ? `Reviewed PR #${pr.number} (${status})`
282
+ : `Review draft for PR #${pr.number} (could not post)`);
283
+ }
284
+ else {
285
+ log(`Review draft for PR #${pr.number} (gh CLI not available)`);
286
+ }
287
+ state.reviewedPRs.push(pr.number);
288
+ }
289
+ // Keep only last 500 reviewed PR numbers
290
+ if (state.reviewedPRs.length > 500) {
291
+ state.reviewedPRs = state.reviewedPRs.slice(-500);
292
+ }
293
+ return results;
294
+ }
295
+ // ── 3. FAQ Matching ──
296
+ function loadFAQ() {
297
+ return loadJsonSafe(FAQ_PATH, []);
298
+ }
299
+ function matchFAQ(question, faq) {
300
+ if (faq.length === 0)
301
+ return null;
302
+ const questionLower = question.toLowerCase();
303
+ const questionWords = questionLower.split(/\s+/).filter(w => w.length > 2);
304
+ let bestMatch = null;
305
+ let bestScore = 0;
306
+ for (const entry of faq) {
307
+ let score = 0;
308
+ // Keyword matching (highest weight)
309
+ for (const kw of entry.keywords) {
310
+ if (questionLower.includes(kw.toLowerCase())) {
311
+ score += 3;
312
+ }
313
+ }
314
+ // Word overlap
315
+ const entryWords = entry.question.toLowerCase().split(/\s+/).filter(w => w.length > 2);
316
+ for (const word of questionWords) {
317
+ if (entryWords.includes(word))
318
+ score += 2;
319
+ if (entryWords.some(ew => ew.includes(word) || word.includes(ew)))
320
+ score += 1;
321
+ }
322
+ if (score > bestScore) {
323
+ bestScore = score;
324
+ bestMatch = entry;
325
+ }
326
+ }
327
+ if (bestMatch && bestScore >= 4) {
328
+ // Normalize confidence to 0-1 range (rough heuristic)
329
+ const confidence = Math.min(1, bestScore / 15);
330
+ return { entry: bestMatch, confidence };
331
+ }
332
+ return null;
333
+ }
334
+ async function answerPendingQuestions(repo, state, useGhCli) {
335
+ const results = [];
336
+ const faq = loadFAQ();
337
+ if (faq.length === 0)
338
+ return results;
339
+ // Fetch open issues labeled "question" or with "?" in title
340
+ const issues = await ghFetch('/issues?state=open&labels=question&sort=created&direction=desc&per_page=10', repo);
341
+ const answeredSet = new Set(state.answeredIssues);
342
+ const candidates = (issues || []).filter(i => !i.pull_request &&
343
+ !answeredSet.has(i.number) &&
344
+ i.comments === 0);
345
+ for (const issue of candidates) {
346
+ const searchText = `${issue.title} ${issue.body || ''}`;
347
+ const match = matchFAQ(searchText, faq);
348
+ if (match) {
349
+ const comment = `Hi @${issue.user.login}! This might help:\n\n${match.entry.answer}\n\n---\n*Answered by kbot FAQ system (confidence: ${(match.confidence * 100).toFixed(0)}%). If this doesn't fully address your question, a maintainer will follow up.*`;
350
+ results.push({
351
+ question: issue.title,
352
+ answer: match.entry.answer,
353
+ matchedIssue: issue.number,
354
+ confidence: match.confidence,
355
+ });
356
+ if (useGhCli) {
357
+ const posted = ghCliComment(repo, issue.number, comment);
358
+ log(posted
359
+ ? `FAQ answered #${issue.number} (confidence: ${(match.confidence * 100).toFixed(0)}%)`
360
+ : `FAQ answer draft for #${issue.number} (could not post)`);
361
+ }
362
+ else {
363
+ log(`FAQ answer draft for #${issue.number} (gh CLI not available)`);
364
+ }
365
+ state.answeredIssues.push(issue.number);
366
+ }
367
+ }
368
+ // Keep only last 500 answered issue numbers
369
+ if (state.answeredIssues.length > 500) {
370
+ state.answeredIssues = state.answeredIssues.slice(-500);
371
+ }
372
+ return results;
373
+ }
374
+ // ── 4. Daily Digest ──
375
+ function shouldGenerateDigest(state) {
376
+ if (!state.lastDigestDate)
377
+ return true;
378
+ const lastDate = state.lastDigestDate.split('T')[0];
379
+ const todayDate = new Date().toISOString().split('T')[0];
380
+ const now = new Date();
381
+ // Generate at midnight (or first cycle after midnight)
382
+ return lastDate !== todayDate && now.getHours() >= 0;
383
+ }
384
+ async function generateDailyDigest(repo, state, discordWebhook) {
385
+ if (!shouldGenerateDigest(state))
386
+ return null;
387
+ log('Generating daily digest...');
388
+ const now = new Date();
389
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
390
+ // Fetch data
391
+ const repoInfo = await ghFetch('', repo);
392
+ const issues = await ghFetch('/issues?state=all&sort=created&direction=desc&per_page=30', repo) || [];
393
+ const prs = await ghFetch('/pulls?state=all&sort=created&direction=desc&per_page=30', repo) || [];
394
+ const pureIssues = issues.filter(i => !i.pull_request);
395
+ const newIssues = pureIssues.filter(i => i.created_at >= oneDayAgo);
396
+ const newPRs = prs.filter(p => p.created_at >= oneDayAgo);
397
+ const mergedPRs = prs.filter(p => p.merged_at && p.merged_at >= oneDayAgo);
398
+ // Detect new contributors
399
+ const knownSet = new Set(state.knownContributors);
400
+ const newContributors = [];
401
+ for (const item of [...newIssues, ...newPRs]) {
402
+ const login = item.user.login;
403
+ if (!knownSet.has(login)) {
404
+ knownSet.add(login);
405
+ newContributors.push(login);
406
+ }
407
+ }
408
+ state.knownContributors = Array.from(knownSet);
409
+ // Build markdown
410
+ const stars = repoInfo?.stargazers_count ?? 0;
411
+ const lines = [
412
+ `# Daily Digest — ${repo}`,
413
+ `> ${now.toISOString().split('T')[0]}`,
414
+ '',
415
+ '## Today',
416
+ '',
417
+ `| Metric | Count |`,
418
+ `|--------|-------|`,
419
+ `| New Issues | ${newIssues.length} |`,
420
+ `| New PRs | ${newPRs.length} |`,
421
+ `| Merged PRs | ${mergedPRs.length} |`,
422
+ `| Stars | ${stars} |`,
423
+ '',
424
+ ];
425
+ if (newContributors.length > 0) {
426
+ lines.push('## New Contributors');
427
+ lines.push('');
428
+ for (const c of newContributors) {
429
+ lines.push(`- @${c}`);
430
+ }
431
+ lines.push('');
432
+ }
433
+ if (mergedPRs.length > 0) {
434
+ lines.push('## Merged Today');
435
+ lines.push('');
436
+ for (const pr of mergedPRs.slice(0, 10)) {
437
+ lines.push(`- [#${pr.number}](${pr.html_url}) ${pr.title} (@${pr.user.login})`);
438
+ }
439
+ lines.push('');
440
+ }
441
+ if (newIssues.length > 0) {
442
+ lines.push('## New Issues');
443
+ lines.push('');
444
+ for (const issue of newIssues.slice(0, 10)) {
445
+ const labels = issue.labels.map(l => l.name).join(', ');
446
+ const labelTag = labels ? ` [${labels}]` : '';
447
+ lines.push(`- [#${issue.number}](${issue.html_url}) ${issue.title}${labelTag}`);
448
+ }
449
+ lines.push('');
450
+ }
451
+ lines.push('---');
452
+ lines.push('*Generated by kbot Community Autopilot*');
453
+ const markdown = lines.join('\n');
454
+ // Save digest
455
+ ensureDir(DIGEST_DIR);
456
+ const digestFile = join(DIGEST_DIR, `${now.toISOString().split('T')[0]}.md`);
457
+ writeFileSync(digestFile, markdown, 'utf-8');
458
+ // Post to Discord
459
+ if (discordWebhook) {
460
+ const posted = await postToDiscord(discordWebhook, markdown);
461
+ log(posted ? 'Digest posted to Discord' : 'Failed to post digest to Discord');
462
+ }
463
+ state.lastDigestDate = now.toISOString();
464
+ const entry = {
465
+ generatedAt: now.toISOString(),
466
+ repo,
467
+ newIssues: newIssues.length,
468
+ newPRs: newPRs.length,
469
+ mergedPRs: mergedPRs.length,
470
+ welcomed: newContributors,
471
+ markdown,
472
+ };
473
+ log(`Digest generated: ${newIssues.length} issues, ${newPRs.length} PRs, ${mergedPRs.length} merged`);
474
+ return entry;
475
+ }
476
+ // ── 5. Welcome New Contributors ──
477
+ async function welcomeNewContributors(repo, state, useGhCli) {
478
+ const welcomed = [];
479
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
480
+ const welcomedSet = new Set(state.welcomedUsers);
481
+ // Fetch recent issues and PRs
482
+ const issues = await ghFetch('/issues?state=all&sort=created&direction=desc&per_page=15', repo) || [];
483
+ const prs = await ghFetch('/pulls?state=all&sort=created&direction=desc&per_page=10', repo) || [];
484
+ const freshIssues = issues.filter(i => !i.pull_request && i.created_at >= oneDayAgo);
485
+ const freshPRs = prs.filter(p => p.created_at >= oneDayAgo);
486
+ // Collect authors
487
+ const authors = new Map();
488
+ for (const item of [...freshIssues, ...freshPRs]) {
489
+ const login = item.user.login;
490
+ authors.set(login, item.number ?? item.number);
491
+ }
492
+ for (const [author, targetNumber] of authors) {
493
+ if (welcomedSet.has(author))
494
+ continue;
495
+ // Check if truly new: not in known contributors
496
+ const knownSet = new Set(state.knownContributors);
497
+ if (knownSet.has(author))
498
+ continue;
499
+ const welcomeMsg = [
500
+ `Welcome to the project, @${author}! We're glad to have you here.`,
501
+ '',
502
+ 'Here are some ways to get started:',
503
+ '- Check out issues labeled `good first issue` for beginner-friendly tasks',
504
+ '- Read the README for project setup and conventions',
505
+ '- Join our community discussions if you have questions',
506
+ '',
507
+ "Don't hesitate to ask for help — we value every contribution, no matter how small.",
508
+ ].join('\n');
509
+ welcomed.push(author);
510
+ welcomedSet.add(author);
511
+ if (useGhCli && targetNumber > 0) {
512
+ const posted = ghCliComment(repo, targetNumber, welcomeMsg);
513
+ log(posted
514
+ ? `Welcomed new contributor: @${author}`
515
+ : `Welcome draft for @${author} (could not post)`);
516
+ }
517
+ else {
518
+ log(`Welcome draft for @${author} (gh CLI not available)`);
519
+ }
520
+ // Also add to known contributors
521
+ if (!state.knownContributors.includes(author)) {
522
+ state.knownContributors.push(author);
523
+ }
524
+ }
525
+ state.welcomedUsers = Array.from(welcomedSet);
526
+ // Keep only last 1000 welcomed users
527
+ if (state.welcomedUsers.length > 1000) {
528
+ state.welcomedUsers = state.welcomedUsers.slice(-1000);
529
+ }
530
+ return welcomed;
531
+ }
532
+ // ── Main Cycle ──
533
+ /**
534
+ * Run a single community autopilot cycle.
535
+ *
536
+ * 1. Triage new GitHub issues (label + respond)
537
+ * 2. Review new PRs (check CI, comment)
538
+ * 3. Answer pending questions via FAQ matching
539
+ * 4. Generate daily digest at midnight
540
+ * 5. Welcome new contributors
541
+ */
542
+ export async function runCommunityAutopilot(config) {
543
+ const { github_repo, discord_webhook } = config;
544
+ const state = loadState();
545
+ const useGhCli = ghCliAvailable();
546
+ const errors = [];
547
+ state.cycleCount++;
548
+ log(`Autopilot cycle ${state.cycleCount} starting for ${github_repo}`);
549
+ // 1. Triage issues
550
+ let triaged = [];
551
+ try {
552
+ triaged = await triageNewIssues(github_repo, state, useGhCli);
553
+ }
554
+ catch (err) {
555
+ const msg = `Issue triage failed: ${err instanceof Error ? err.message : String(err)}`;
556
+ errors.push(msg);
557
+ log(msg);
558
+ }
559
+ // 2. Review PRs
560
+ let reviewed = [];
561
+ try {
562
+ reviewed = await reviewNewPRs(github_repo, state, useGhCli);
563
+ }
564
+ catch (err) {
565
+ const msg = `PR review failed: ${err instanceof Error ? err.message : String(err)}`;
566
+ errors.push(msg);
567
+ log(msg);
568
+ }
569
+ // 3. Answer FAQ questions
570
+ let faqAnswered = [];
571
+ try {
572
+ faqAnswered = await answerPendingQuestions(github_repo, state, useGhCli);
573
+ }
574
+ catch (err) {
575
+ const msg = `FAQ matching failed: ${err instanceof Error ? err.message : String(err)}`;
576
+ errors.push(msg);
577
+ log(msg);
578
+ }
579
+ // 4. Daily digest
580
+ let digest = null;
581
+ try {
582
+ digest = await generateDailyDigest(github_repo, state, discord_webhook);
583
+ }
584
+ catch (err) {
585
+ const msg = `Digest generation failed: ${err instanceof Error ? err.message : String(err)}`;
586
+ errors.push(msg);
587
+ log(msg);
588
+ }
589
+ // 5. Welcome new contributors
590
+ let welcomed = [];
591
+ try {
592
+ welcomed = await welcomeNewContributors(github_repo, state, useGhCli);
593
+ }
594
+ catch (err) {
595
+ const msg = `Welcome failed: ${err instanceof Error ? err.message : String(err)}`;
596
+ errors.push(msg);
597
+ log(msg);
598
+ }
599
+ // Persist state
600
+ saveState(state);
601
+ const result = {
602
+ timestamp: new Date().toISOString(),
603
+ repo: github_repo,
604
+ triaged,
605
+ reviewed,
606
+ faqAnswered,
607
+ welcomed,
608
+ digest,
609
+ errors,
610
+ };
611
+ log(`Cycle ${state.cycleCount} complete: ` +
612
+ `${triaged.length} triaged, ${reviewed.length} reviewed, ` +
613
+ `${faqAnswered.length} FAQ answered, ${welcomed.length} welcomed` +
614
+ (errors.length > 0 ? `, ${errors.length} error(s)` : ''));
615
+ return result;
616
+ }
617
+ // ── Daemon Control ──
618
+ let autopilotTimer = null;
619
+ let autopilotRunning = false;
620
+ /**
621
+ * Start the community autopilot as a background daemon.
622
+ * Runs a cycle immediately, then repeats at the configured interval.
623
+ */
624
+ export function startAutopilot(config) {
625
+ if (autopilotRunning) {
626
+ log('Autopilot is already running. Call stopAutopilot() first.');
627
+ return;
628
+ }
629
+ const interval = config.check_interval_ms ?? 300000; // 5 min default
630
+ log(`Starting autopilot daemon (interval: ${interval}ms)`);
631
+ autopilotRunning = true;
632
+ // Run immediately
633
+ runCommunityAutopilot(config).catch(err => {
634
+ log(`Autopilot cycle error: ${err instanceof Error ? err.message : String(err)}`);
635
+ });
636
+ // Schedule recurring cycles
637
+ autopilotTimer = setInterval(() => {
638
+ if (!autopilotRunning)
639
+ return;
640
+ runCommunityAutopilot(config).catch(err => {
641
+ log(`Autopilot cycle error: ${err instanceof Error ? err.message : String(err)}`);
642
+ });
643
+ }, interval);
644
+ // Prevent the timer from keeping Node alive if this is the only reference
645
+ if (autopilotTimer && typeof autopilotTimer === 'object' && 'unref' in autopilotTimer) {
646
+ autopilotTimer.unref();
647
+ }
648
+ }
649
+ /**
650
+ * Stop the community autopilot daemon.
651
+ */
652
+ export function stopAutopilot() {
653
+ if (!autopilotRunning) {
654
+ log('Autopilot is not running.');
655
+ return;
656
+ }
657
+ if (autopilotTimer) {
658
+ clearInterval(autopilotTimer);
659
+ autopilotTimer = null;
660
+ }
661
+ autopilotRunning = false;
662
+ log('Autopilot stopped.');
663
+ }
664
+ /**
665
+ * Check if the autopilot daemon is currently running.
666
+ */
667
+ export function isAutopilotRunning() {
668
+ return autopilotRunning;
669
+ }
670
+ /**
671
+ * Get the current autopilot state (for debugging / monitoring).
672
+ */
673
+ export function getAutopilotState() {
674
+ return loadState();
675
+ }
676
+ //# sourceMappingURL=community-autopilot.js.map