@kernel.chat/kbot 3.15.1 → 3.17.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.
@@ -0,0 +1,8 @@
1
+ export declare const PLAYTESTER_AGENT_ID = "playtester";
2
+ export declare const PLAYTESTER_SYSTEM_PROMPT = "You are kbot's playtester agent \u2014 a brutally honest game tester. You don't sugarcoat. You find problems.\n\n## YOUR JOB\n\nPlay the game mentally by reading the code. Simulate 30 seconds of gameplay in your head. Then report what's broken, what's boring, and what's missing.\n\n## HOW YOU TEST\n\n1. Read scene files \u2014 trace the update loop frame by frame\n2. Read entity code \u2014 check if player/partner/enemies actually behave as designed\n3. Read VFX code \u2014 verify feedback exists for EVERY player action\n4. Read design docs \u2014 compare what's built vs what's promised\n5. Check integration \u2014 are all built systems actually wired into the game loop?\n\n## REPORT FORMAT\n\n### Bug Report:\n```\nBUG: [severity: critical/major/minor]\nWhat: <what's wrong>\nWhere: <file:line>\nExpected: <what should happen>\nActual: <what happens instead>\nFix: <specific code change>\n```\n\n### Feel Report:\n```\nFEEL: [rating: dead/flat/ok/good/great]\nWhat: <moment being evaluated>\nProblem: <why it doesn't feel right>\nReference: <how Hades/Dead Cells/etc handles this>\nFix: <specific improvement>\n```\n\n### Missing Report:\n```\nMISSING: [priority: critical/high/medium/low]\nWhat: <what's not there>\nWhy it matters: <impact on player experience>\n```\n\n## YOUR STANDARDS\n\nYou've played Hades, Dead Cells, Enter the Gungeon, Vampire Survivors, Nuclear Throne. You know what good feels like. You compare the game against those, not against \"it works.\"\n\n### Critical Checklist (fail the build if any are NO):\n- Does the player move within 16ms of input?\n- Does every attack produce visible + audible feedback?\n- Can you tell what every enemy will do by looking at it?\n- Does the partner do something useful without being told?\n- Is there a reason to keep playing after dying?\n- Does the game run at 60fps constant?\n\n### Feel Checklist:\n- Does movement have weight (acceleration, deceleration)?\n- Do hits feel impactful (hitstop, shake, sparks)?\n- Is there contrast (quiet moments vs intense moments)?\n- Does the camera enhance the action?\n- Are there moments of surprise or discovery?\n- Does the partner feel like a character, not a turret?\n\n## YOUR TONE\n\nYou are the player's advocate. You don't care how hard something was to build. You care if it's fun. If the game is boring, say \"this is boring\" and say exactly why. If something is great, acknowledge it briefly and move on \u2014 praise doesn't ship features.";
3
+ export declare const PLAYTESTER_PERSONALITY: {
4
+ id: string;
5
+ name: string;
6
+ traits: string[];
7
+ };
8
+ //# sourceMappingURL=playtester.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playtester.d.ts","sourceRoot":"","sources":["../../src/agents/playtester.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,eAAe,CAAA;AAE/C,eAAO,MAAM,wBAAwB,w6EAgE2N,CAAA;AAEhQ,eAAO,MAAM,sBAAsB;;;;CAUlC,CAAA"}
@@ -0,0 +1,83 @@
1
+ // kbot Playtester Agent — brutally honest game tester
2
+ //
3
+ // Simulates 30 seconds of gameplay by reading code. Reports bugs,
4
+ // feel issues, and missing features. Benchmarks against Hades,
5
+ // Dead Cells, Enter the Gungeon.
6
+ export const PLAYTESTER_AGENT_ID = 'playtester';
7
+ export const PLAYTESTER_SYSTEM_PROMPT = `You are kbot's playtester agent — a brutally honest game tester. You don't sugarcoat. You find problems.
8
+
9
+ ## YOUR JOB
10
+
11
+ Play the game mentally by reading the code. Simulate 30 seconds of gameplay in your head. Then report what's broken, what's boring, and what's missing.
12
+
13
+ ## HOW YOU TEST
14
+
15
+ 1. Read scene files — trace the update loop frame by frame
16
+ 2. Read entity code — check if player/partner/enemies actually behave as designed
17
+ 3. Read VFX code — verify feedback exists for EVERY player action
18
+ 4. Read design docs — compare what's built vs what's promised
19
+ 5. Check integration — are all built systems actually wired into the game loop?
20
+
21
+ ## REPORT FORMAT
22
+
23
+ ### Bug Report:
24
+ \`\`\`
25
+ BUG: [severity: critical/major/minor]
26
+ What: <what's wrong>
27
+ Where: <file:line>
28
+ Expected: <what should happen>
29
+ Actual: <what happens instead>
30
+ Fix: <specific code change>
31
+ \`\`\`
32
+
33
+ ### Feel Report:
34
+ \`\`\`
35
+ FEEL: [rating: dead/flat/ok/good/great]
36
+ What: <moment being evaluated>
37
+ Problem: <why it doesn't feel right>
38
+ Reference: <how Hades/Dead Cells/etc handles this>
39
+ Fix: <specific improvement>
40
+ \`\`\`
41
+
42
+ ### Missing Report:
43
+ \`\`\`
44
+ MISSING: [priority: critical/high/medium/low]
45
+ What: <what's not there>
46
+ Why it matters: <impact on player experience>
47
+ \`\`\`
48
+
49
+ ## YOUR STANDARDS
50
+
51
+ You've played Hades, Dead Cells, Enter the Gungeon, Vampire Survivors, Nuclear Throne. You know what good feels like. You compare the game against those, not against "it works."
52
+
53
+ ### Critical Checklist (fail the build if any are NO):
54
+ - Does the player move within 16ms of input?
55
+ - Does every attack produce visible + audible feedback?
56
+ - Can you tell what every enemy will do by looking at it?
57
+ - Does the partner do something useful without being told?
58
+ - Is there a reason to keep playing after dying?
59
+ - Does the game run at 60fps constant?
60
+
61
+ ### Feel Checklist:
62
+ - Does movement have weight (acceleration, deceleration)?
63
+ - Do hits feel impactful (hitstop, shake, sparks)?
64
+ - Is there contrast (quiet moments vs intense moments)?
65
+ - Does the camera enhance the action?
66
+ - Are there moments of surprise or discovery?
67
+ - Does the partner feel like a character, not a turret?
68
+
69
+ ## YOUR TONE
70
+
71
+ You are the player's advocate. You don't care how hard something was to build. You care if it's fun. If the game is boring, say "this is boring" and say exactly why. If something is great, acknowledge it briefly and move on — praise doesn't ship features.`;
72
+ export const PLAYTESTER_PERSONALITY = {
73
+ id: 'playtester',
74
+ name: 'Playtester',
75
+ traits: [
76
+ 'Brutally honest about game quality',
77
+ 'Benchmarks against best-in-class (Hades, Dead Cells)',
78
+ 'Player advocate — fun over technical achievement',
79
+ 'Produces actionable bug/feel/missing reports',
80
+ 'Never sugarcoats, but always offers specific fixes',
81
+ ],
82
+ };
83
+ //# sourceMappingURL=playtester.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playtester.js","sourceRoot":"","sources":["../../src/agents/playtester.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,kEAAkE;AAClE,+DAA+D;AAC/D,iCAAiC;AAEjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AAE/C,MAAM,CAAC,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gQAgEwN,CAAA;AAEhQ,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,EAAE,EAAE,YAAY;IAChB,IAAI,EAAE,YAAY;IAClB,MAAM,EAAE;QACN,oCAAoC;QACpC,sDAAsD;QACtD,kDAAkD;QAClD,8CAA8C;QAC9C,oDAAoD;KACrD;CACF,CAAA"}
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ async function main() {
35
35
  .name('kbot')
36
36
  .description('kbot — Open-source terminal AI agent. Bring your own key, pick your model, run locally.')
37
37
  .version(VERSION)
38
- .option('-a, --agent <agent>', 'Force a specific agent (run kbot agents to see all 22)')
38
+ .option('-a, --agent <agent>', 'Force a specific agent (run kbot agents to see all 25)')
39
39
  .option('-m, --model <model>', 'Override AI model (auto, sonnet, haiku)')
40
40
  .option('-s, --stream', 'Stream the response')
41
41
  .option('-p, --pipe', 'Pipe mode — raw text output for scripting')
@@ -360,6 +360,40 @@ async function main() {
360
360
  printInfo('Download: kbot models pull <name>');
361
361
  printInfo('Or use any HuggingFace GGUF: kbot models pull hf:user/repo:file.gguf');
362
362
  });
363
+ program
364
+ .command('init')
365
+ .description('Set up kbot for this project — detects stack, creates tools, writes config (60 seconds)')
366
+ .option('--force', 'Overwrite existing .kbot.json')
367
+ .action(async (opts) => {
368
+ const { existsSync } = await import('node:fs');
369
+ const { join } = await import('node:path');
370
+ const { initProject, formatInitReport } = await import('./init.js');
371
+ const chalk = (await import('chalk')).default;
372
+ const AMETHYST = chalk.hex('#6B5B95');
373
+ const root = process.cwd();
374
+ // Check for existing config
375
+ if (!opts.force && existsSync(join(root, '.kbot.json'))) {
376
+ printWarn('This project already has a .kbot.json. Use --force to overwrite.');
377
+ return;
378
+ }
379
+ process.stderr.write('\n');
380
+ process.stderr.write(` ${AMETHYST('◉')} ${chalk.bold('kbot init')} — scanning project...\n\n`);
381
+ const config = await initProject(root);
382
+ process.stderr.write(formatInitReport(config) + '\n');
383
+ process.stderr.write('\n');
384
+ process.stderr.write(` ${chalk.green('✓')} Config written to ${chalk.dim('.kbot.json')}\n`);
385
+ if (config.forgedTools.length > 0) {
386
+ process.stderr.write(` ${chalk.green('✓')} ${config.forgedTools.length} tools forged: ${chalk.dim(config.forgedTools.join(', '))}\n`);
387
+ }
388
+ process.stderr.write('\n');
389
+ process.stderr.write(` ${chalk.bold('Try now:')}\n`);
390
+ process.stderr.write(` ${chalk.cyan('kbot')} "explain this project"\n`);
391
+ process.stderr.write(` ${chalk.cyan('kbot')} "find the top bug"\n`);
392
+ if (config.commands.test) {
393
+ process.stderr.write(` ${chalk.cyan('kbot')} "run the tests and fix any failures"\n`);
394
+ }
395
+ process.stderr.write('\n');
396
+ });
363
397
  program
364
398
  .command('doctor')
365
399
  .description('Diagnose your kbot setup — check everything is working')
@@ -660,7 +694,7 @@ async function main() {
660
694
  latestVersion: latestVersion || null,
661
695
  isLatest,
662
696
  tools: toolCount,
663
- agents: 23,
697
+ agents: 25,
664
698
  cognitiveModules: cognitiveCount,
665
699
  learning: {
666
700
  patterns: stats.patternsCount,
@@ -692,7 +726,7 @@ async function main() {
692
726
  process.stderr.write(` ${AMETHYST('◉')} ${chalk.bold('Kernel Status')}\n`);
693
727
  process.stderr.write(line + '\n');
694
728
  process.stderr.write(` ${chalk.bold('Version')} ${VERSION}${versionTag}\n`);
695
- process.stderr.write(` ${chalk.bold('Tools')} ${fmtNum(toolCount)} ${DIM('|')} ${chalk.bold('Agents')} 23\n`);
729
+ process.stderr.write(` ${chalk.bold('Tools')} ${fmtNum(toolCount)} ${DIM('|')} ${chalk.bold('Agents')} 25\n`);
696
730
  process.stderr.write(` ${chalk.bold('Cognitive')} ${cognitiveCount}/${cognitiveCount} modules active\n`);
697
731
  process.stderr.write(line + '\n');
698
732
  // Learning
@@ -1393,6 +1427,191 @@ async function main() {
1393
1427
  printError('Voice mode not available on this platform');
1394
1428
  }
1395
1429
  });
1430
+ // ── Email Agent ──
1431
+ const emailAgentCmd = program
1432
+ .command('email-agent')
1433
+ .description('Autonomous email companion agent — responds to emails via local AI ($0 cost)');
1434
+ emailAgentCmd
1435
+ .command('start')
1436
+ .description('Start the email agent — polls for new emails and responds via Ollama')
1437
+ .option('--model <model>', 'Ollama model to use', 'qwen2.5-coder:32b')
1438
+ .option('--interval <ms>', 'Poll interval in milliseconds', '15000')
1439
+ .option('--users <emails>', 'Comma-separated email addresses to monitor (omit for open mode — all inbound)')
1440
+ .option('--open', 'Open mode — respond to ALL inbound emails (no whitelist)')
1441
+ .action(async (opts) => {
1442
+ const { startEmailAgent } = await import('./email-agent.js');
1443
+ const { existsSync, readFileSync } = await import('node:fs');
1444
+ const { join } = await import('node:path');
1445
+ // Load env
1446
+ let supabaseUrl = '', supabaseKey = '', resendKey = '';
1447
+ const envPaths = [join(process.cwd(), '.env'), join(process.env.HOME || '', '.kbot', '.env')];
1448
+ for (const envPath of envPaths) {
1449
+ if (existsSync(envPath)) {
1450
+ const env = readFileSync(envPath, 'utf8');
1451
+ const get = (k) => env.match(new RegExp(`^${k}=(.+)$`, 'm'))?.[1]?.trim() ?? '';
1452
+ supabaseUrl = supabaseUrl || get('VITE_SUPABASE_URL');
1453
+ supabaseKey = supabaseKey || get('SUPABASE_SERVICE_KEY');
1454
+ resendKey = resendKey || get('RESEND_API_KEY');
1455
+ }
1456
+ }
1457
+ if (!supabaseUrl || !supabaseKey) {
1458
+ printError('Missing VITE_SUPABASE_URL or SUPABASE_SERVICE_KEY in .env');
1459
+ return;
1460
+ }
1461
+ if (!resendKey) {
1462
+ printError('Missing RESEND_API_KEY in .env — needed to send reply emails');
1463
+ return;
1464
+ }
1465
+ const agentUsers = opts.users?.split(',').map(e => e.trim()) || [];
1466
+ if (agentUsers.length === 0 && !opts.open) {
1467
+ printError('No users specified. Use --users email1,email2 or --open for all inbound.');
1468
+ return;
1469
+ }
1470
+ const chalk = (await import('chalk')).default;
1471
+ const mode = agentUsers.length === 0 ? 'OPEN (all inbound)' : `${agentUsers.length} users`;
1472
+ console.log();
1473
+ console.log(chalk.hex('#6B5B95')(' ◉ Kernel Email Agent'));
1474
+ console.log(chalk.dim(` Model: ${opts.model || 'qwen2.5-coder:32b'}`));
1475
+ console.log(chalk.dim(` Mode: ${mode}`));
1476
+ console.log(chalk.dim(` Poll interval: ${Number(opts.interval || 15000) / 1000}s`));
1477
+ console.log();
1478
+ await startEmailAgent({
1479
+ supabaseUrl,
1480
+ supabaseKey,
1481
+ resendKey,
1482
+ ollamaUrl: 'http://localhost:11434',
1483
+ ollamaModel: opts.model || 'qwen2.5-coder:32b',
1484
+ pollInterval: Number(opts.interval || 15000),
1485
+ agentUsers,
1486
+ });
1487
+ // Keep process alive
1488
+ await new Promise(() => { });
1489
+ });
1490
+ emailAgentCmd
1491
+ .command('status')
1492
+ .description('Show email agent status')
1493
+ .action(async () => {
1494
+ const { getEmailAgentState } = await import('./email-agent.js');
1495
+ const state = getEmailAgentState();
1496
+ if (state.running) {
1497
+ printSuccess(`Email agent is running`);
1498
+ printInfo(` Processed: ${state.processedCount} emails`);
1499
+ printInfo(` Last check: ${state.lastCheck || 'never'}`);
1500
+ if (state.errors.length > 0) {
1501
+ printWarn(` Recent errors: ${state.errors.length}`);
1502
+ for (const err of state.errors.slice(-3)) {
1503
+ printError(` ${err}`);
1504
+ }
1505
+ }
1506
+ }
1507
+ else {
1508
+ printInfo('Email agent is not running. Start with: kbot email-agent start --open');
1509
+ }
1510
+ });
1511
+ // ── iMessage Agent ──
1512
+ const imessageCmd = program
1513
+ .command('imessage-agent')
1514
+ .description('Free iMessage/SMS agent via macOS Messages.app ($0 cost, unlimited)');
1515
+ imessageCmd
1516
+ .command('start')
1517
+ .description('Start monitoring iMessage — responds via local Ollama')
1518
+ .option('--model <model>', 'Ollama model to use', 'qwen2.5-coder:32b')
1519
+ .option('--interval <ms>', 'Poll interval in milliseconds', '10000')
1520
+ .option('--numbers <nums>', 'Comma-separated phone numbers to monitor (e.g., +17145551234)')
1521
+ .action(async (opts) => {
1522
+ const { platform } = await import('node:os');
1523
+ if (platform() !== 'darwin') {
1524
+ printError('iMessage agent is only available on macOS');
1525
+ return;
1526
+ }
1527
+ const numbers = opts.numbers?.split(',').map(n => n.trim()) || [];
1528
+ if (numbers.length === 0) {
1529
+ printError('No phone numbers specified. Use --numbers +17145551234,+12135559876');
1530
+ return;
1531
+ }
1532
+ // Optional Supabase for logging
1533
+ let supabaseUrl = '', supabaseKey = '';
1534
+ const { existsSync, readFileSync } = await import('node:fs');
1535
+ const { join } = await import('node:path');
1536
+ const envPaths = [join(process.cwd(), '.env'), join(process.env.HOME || '', '.kbot', '.env')];
1537
+ for (const envPath of envPaths) {
1538
+ if (existsSync(envPath)) {
1539
+ const env = readFileSync(envPath, 'utf8');
1540
+ const get = (k) => env.match(new RegExp(`^${k}=(.+)$`, 'm'))?.[1]?.trim() ?? '';
1541
+ supabaseUrl = supabaseUrl || get('VITE_SUPABASE_URL');
1542
+ supabaseKey = supabaseKey || get('SUPABASE_SERVICE_KEY');
1543
+ }
1544
+ }
1545
+ const chalk = (await import('chalk')).default;
1546
+ console.log();
1547
+ console.log(chalk.hex('#6B5B95')(' ◉ Kernel iMessage Agent'));
1548
+ console.log(chalk.dim(` Model: ${opts.model || 'qwen2.5-coder:32b'}`));
1549
+ console.log(chalk.dim(` Monitoring: ${numbers.join(', ')}`));
1550
+ console.log(chalk.dim(` Poll interval: ${Number(opts.interval || 10000) / 1000}s`));
1551
+ if (supabaseUrl)
1552
+ console.log(chalk.dim(' Logging to Supabase: yes'));
1553
+ console.log();
1554
+ const { startIMessageAgent } = await import('./imessage-agent.js');
1555
+ await startIMessageAgent({
1556
+ ollamaUrl: 'http://localhost:11434',
1557
+ ollamaModel: opts.model || 'qwen2.5-coder:32b',
1558
+ pollInterval: Number(opts.interval || 10000),
1559
+ numbers,
1560
+ supabaseUrl: supabaseUrl || undefined,
1561
+ supabaseKey: supabaseKey || undefined,
1562
+ });
1563
+ // Keep process alive
1564
+ await new Promise(() => { });
1565
+ });
1566
+ imessageCmd
1567
+ .command('status')
1568
+ .description('Show iMessage agent status')
1569
+ .action(async () => {
1570
+ const { getIMessageAgentState } = await import('./imessage-agent.js');
1571
+ const state = getIMessageAgentState();
1572
+ if (state.running) {
1573
+ printSuccess('iMessage agent is running');
1574
+ printInfo(` Messages processed: ${state.messagesProcessed}`);
1575
+ printInfo(` Last check: ${state.lastCheck || 'never'}`);
1576
+ if (state.errors.length > 0) {
1577
+ printWarn(` Recent errors: ${state.errors.length}`);
1578
+ }
1579
+ }
1580
+ else {
1581
+ printInfo('iMessage agent is not running. Start with: kbot imessage-agent start --numbers +1234567890');
1582
+ }
1583
+ });
1584
+ // ── Consultation ──
1585
+ program
1586
+ .command('consultation')
1587
+ .description('Consultation engine — domain guardrails, intake, client management')
1588
+ .option('--check <message>', 'Check if a message hits domain guardrails')
1589
+ .option('--intake', 'Generate the intake questionnaire')
1590
+ .action(async (opts) => {
1591
+ const { checkDomainGuardrails, getIntakeMessage } = await import('./consultation.js');
1592
+ if (opts.check) {
1593
+ const result = checkDomainGuardrails(opts.check);
1594
+ if (result.blocked) {
1595
+ printWarn(`Blocked — ${result.domain} domain`);
1596
+ printInfo(result.message || '');
1597
+ if (result.suggestedTopic)
1598
+ printInfo(`Suggested redirect: ${result.suggestedTopic}`);
1599
+ }
1600
+ else {
1601
+ printSuccess('Message passes domain guardrails');
1602
+ }
1603
+ return;
1604
+ }
1605
+ if (opts.intake) {
1606
+ console.log(getIntakeMessage());
1607
+ return;
1608
+ }
1609
+ // Default: show help
1610
+ printInfo('Kernel Consultation Engine');
1611
+ printInfo('');
1612
+ printInfo(' kbot consultation --check "message" Check domain guardrails');
1613
+ printInfo(' kbot consultation --intake Generate intake questions');
1614
+ });
1396
1615
  program
1397
1616
  .command('export <session>')
1398
1617
  .description('Export a saved session to markdown, JSON, or HTML')
@@ -1444,24 +1663,40 @@ async function main() {
1444
1663
  });
1445
1664
  program
1446
1665
  .command('audit <repo>')
1447
- .description('Full audit of any GitHub repository — security, quality, docs, DevOps')
1448
- .option('--share', 'Share the audit report as a GitHub Gist')
1666
+ .description('Full audit of any GitHub repository — security, quality, docs, DevOps. Generates shareable report with badge.')
1667
+ .option('--share', 'Auto-share the report as a public GitHub Gist')
1668
+ .option('--json', 'Output raw JSON')
1669
+ .option('--badge', 'Print only the badge markdown (for adding to READMEs)')
1449
1670
  .action(async (repo, auditOpts) => {
1450
1671
  const { auditRepo, formatAuditReport } = await import('./tools/audit.js');
1451
1672
  printInfo(`Auditing ${repo}...`);
1452
1673
  try {
1453
1674
  const result = await auditRepo(repo);
1675
+ if (auditOpts.json) {
1676
+ console.log(JSON.stringify(result, null, 2));
1677
+ return;
1678
+ }
1679
+ if (auditOpts.badge) {
1680
+ const pct = Math.round((result.score / result.maxScore) * 100);
1681
+ const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
1682
+ console.log(`[![kbot audit: ${result.grade}](https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor})](https://www.npmjs.com/package/@kernel.chat/kbot)`);
1683
+ return;
1684
+ }
1454
1685
  const report = formatAuditReport(result);
1455
1686
  console.log(report);
1687
+ // Auto-share as gist
1456
1688
  if (auditOpts.share) {
1457
- const { shareConversation } = await import('./share.js');
1458
- // Save as a pseudo-conversation for sharing
1459
1689
  printInfo('Sharing audit report...');
1460
1690
  try {
1461
1691
  const { createGist } = await import('./share.js');
1462
- const url = createGist(report, `kbot-audit-${repo.replace('/', '-')}.md`, `kbot Audit: ${repo}`, true);
1463
- if (url?.startsWith('http'))
1692
+ const url = createGist(report, `kbot-audit-${repo.replace('/', '-')}.md`, `kbot Audit: ${repo} — Grade ${result.grade}`, true);
1693
+ if (url?.startsWith('http')) {
1464
1694
  printSuccess(`Shared! ${url}`);
1695
+ printInfo(`Badge for ${repo}'s README:`);
1696
+ const pct = Math.round((result.score / result.maxScore) * 100);
1697
+ const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
1698
+ printInfo(` [![kbot audit: ${result.grade}](https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor})](${url})`);
1699
+ }
1465
1700
  }
1466
1701
  catch {
1467
1702
  printInfo('Could not create Gist. Install GitHub CLI: brew install gh');