@kernel.chat/kbot 3.97.0 → 3.97.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center">
4
4
  <strong>kbot</strong><br>
5
- Open-source terminal AI agent. 764+ tools. 35 agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
5
+ Open-source terminal AI agent. 787+ tools. 35 agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
6
6
  </p>
7
7
 
8
8
  <p align="center">
@@ -31,7 +31,7 @@ Most terminal AI agents lock you into one provider, one model, one way of workin
31
31
  - **Runs fully offline** — Embedded llama.cpp, Ollama, LM Studio, or Jan. $0, fully private.
32
32
  - **Learns your patterns** — Bayesian skill ratings + pattern extraction. Gets faster over time.
33
33
  - **35 specialist agents** — auto-routes your request to the right expert (coder, researcher, writer, guardian, quant, and 30 more).
34
- - **764+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
34
+ - **787+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
35
35
  - **Programmatic SDK** — use kbot as a library in your own apps.
36
36
  - **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
37
37
 
@@ -142,7 +142,7 @@ Checks security, documentation, code quality, CI/CD, community health, and DevOp
142
142
  |---|---|---|---|---|---|
143
143
  | AI providers | 20 | 1 | 1 | 6 | 75+ |
144
144
  | Specialist agents | 35 | 0 | 0 | 0 | 0 |
145
- | Built-in tools | 764+ | ~20 | ~15 | ~10 | ~15 |
145
+ | Built-in tools | 787+ | ~20 | ~15 | ~10 | ~15 |
146
146
  | Science tools | 114 | 0 | 0 | 0 | 0 |
147
147
  | Memory system | 7-tier bidirectional | File-based | No | No | No |
148
148
  | Dream engine | Yes ($0 local) | Cloud API | No | No | No |
package/dist/cli.js CHANGED
@@ -102,7 +102,7 @@ async function main() {
102
102
  console.log(` ${chalk.cyan('https://github.com/isaacsight/kernel/issues')} ${chalk.dim('Bug reports')}`);
103
103
  console.log(` ${chalk.cyan('support@kernel.chat')} ${chalk.dim('Email (AI-assisted replies)')}`);
104
104
  console.log();
105
- console.log(` ${chalk.dim('35 specialist agents · 360+ tools · 20 providers · MIT licensed')}`);
105
+ console.log(` ${chalk.dim('35 specialist agents · 787+ tools · 20 providers · MIT licensed')}`);
106
106
  console.log();
107
107
  process.exit(0);
108
108
  });
@@ -129,4 +129,36 @@ export declare class IntelligenceCoordinator {
129
129
  export declare function getCoordinator(): IntelligenceCoordinator;
130
130
  /** Register coordinator tools with the kbot tool registry */
131
131
  export declare function registerCoordinatorTools(): void;
132
+ export interface Task {
133
+ id: string;
134
+ goal: string;
135
+ status: 'pending' | 'running' | 'done' | 'failed';
136
+ agent: string;
137
+ dependencies: string[];
138
+ result?: string;
139
+ error?: string;
140
+ }
141
+ export interface CoordinatorPlan {
142
+ id: string;
143
+ goal: string;
144
+ tasks: Task[];
145
+ createdAt: string;
146
+ completedAt?: string;
147
+ status: 'planning' | 'executing' | 'done' | 'failed';
148
+ }
149
+ /**
150
+ * Decompose a high-level goal into ordered sub-tasks with dependencies.
151
+ * Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
152
+ */
153
+ export declare function decompose(goal: string): Promise<CoordinatorPlan>;
154
+ /**
155
+ * Execute a plan's tasks in dependency order (sequential for now).
156
+ * Tasks whose dependencies are all 'done' are eligible to run.
157
+ */
158
+ export declare function execute(plan: CoordinatorPlan): Promise<CoordinatorPlan>;
159
+ /**
160
+ * Convenience: decompose a goal, execute the plan, and synthesize results.
161
+ * Returns a human-readable summary of what was accomplished.
162
+ */
163
+ export declare function coordinate(goal: string): Promise<string>;
132
164
  //# sourceMappingURL=coordinator.d.ts.map
@@ -8,6 +8,7 @@
8
8
  import { homedir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
+ import chalk from 'chalk';
11
12
  // ── Lazy module loaders (dynamic imports to break circular deps) ──
12
13
  async function getLearning() { return import('./learning.js'); }
13
14
  async function getConfidence() { return import('./confidence.js'); }
@@ -673,10 +674,166 @@ export function registerCoordinatorTools() {
673
674
  return JSON.stringify(result, null, 2);
674
675
  },
675
676
  });
677
+ registerTool({
678
+ name: 'coordinator_orchestrate',
679
+ description: 'Decompose a goal into sub-tasks, assign specialist agents, execute in dependency order, and synthesize results',
680
+ parameters: { goal: { type: 'string', description: 'The high-level goal to orchestrate', required: true } },
681
+ tier: 'free',
682
+ execute: async (args) => coordinate(args.goal),
683
+ });
676
684
  }).catch(() => {
677
685
  // tools/index.js not available — skip registration
678
686
  });
679
687
  }
688
+ // ── Goal Decomposition & Multi-Agent Orchestration ──
689
+ //
690
+ // Takes a high-level goal and:
691
+ // 1. Decomposes it into ordered sub-tasks via LLM
692
+ // 2. Assigns each sub-task to the best specialist agent
693
+ // 3. Manages dependencies between sub-tasks
694
+ // 4. Synthesizes results into a final output
695
+ // 5. Learns from the execution for next time
696
+ const COORD_AMETHYST = typeof chalk.hex === 'function' ? chalk.hex('#6B5B95') : ((s) => s);
697
+ const DECOMPOSE_PROMPT = `You are a task decomposition engine. Break the goal into 2-6 concrete sub-tasks.
698
+ Output ONLY valid JSON: {"tasks":[{"id":"t1","goal":"...","agent":"agent_id","dependencies":[]}]}
699
+ Agents: kernel (general), coder (code/debug), researcher (research), writer (docs), analyst (strategy), guardian (security), infrastructure (devops).
700
+ Rules: each task independently verifiable, use dependencies for ordering, assign best agent, one objective per task.`;
701
+ /**
702
+ * Decompose a high-level goal into ordered sub-tasks with dependencies.
703
+ * Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
704
+ */
705
+ export async function decompose(goal) {
706
+ const planId = shortId();
707
+ const plan = {
708
+ id: planId,
709
+ goal,
710
+ tasks: [],
711
+ createdAt: new Date().toISOString(),
712
+ status: 'planning',
713
+ };
714
+ process.stderr.write(` ${COORD_AMETHYST('◆ decompose')} ${chalk.dim(goal.slice(0, 80))}${goal.length > 80 ? '...' : ''}\n`);
715
+ try {
716
+ // Lazy import to avoid circular dependency
717
+ const { runAgent } = await import('./agent.js');
718
+ const response = await runAgent(`${DECOMPOSE_PROMPT}\n\nGoal: ${goal}\n\nOutput JSON:`, { agent: 'kernel', skipPlanner: true, sessionId: `coord-${planId}` });
719
+ // Parse JSON from response
720
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
721
+ if (jsonMatch) {
722
+ const parsed = JSON.parse(jsonMatch[0]);
723
+ plan.tasks = (parsed.tasks || []).slice(0, 6).map(t => ({
724
+ id: t.id || shortId(),
725
+ goal: t.goal,
726
+ status: 'pending',
727
+ agent: t.agent || 'kernel',
728
+ dependencies: t.dependencies || [],
729
+ }));
730
+ }
731
+ }
732
+ catch (err) {
733
+ process.stderr.write(` ${chalk.red('✗')} decomposition failed: ${err instanceof Error ? err.message : String(err)}\n`);
734
+ }
735
+ // Fallback: if decomposition produced nothing, create a single task
736
+ if (plan.tasks.length === 0) {
737
+ plan.tasks = [{
738
+ id: 't1',
739
+ goal,
740
+ status: 'pending',
741
+ agent: 'kernel',
742
+ dependencies: [],
743
+ }];
744
+ }
745
+ plan.status = 'executing';
746
+ for (const task of plan.tasks) {
747
+ const deps = task.dependencies.length > 0 ? chalk.dim(` -> ${task.dependencies.join(',')}`) : '';
748
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.cyan(task.id)} ${task.goal} ${chalk.magenta(`@${task.agent}`)}${deps}\n`);
749
+ }
750
+ return plan;
751
+ }
752
+ /**
753
+ * Execute a plan's tasks in dependency order (sequential for now).
754
+ * Tasks whose dependencies are all 'done' are eligible to run.
755
+ */
756
+ export async function execute(plan) {
757
+ const { runAgent } = await import('./agent.js');
758
+ const completed = new Set();
759
+ process.stderr.write(`\n ${COORD_AMETHYST('◆ execute')} ${plan.tasks.length} tasks\n`);
760
+ while (completed.size < plan.tasks.length) {
761
+ // Find tasks whose dependencies are all satisfied
762
+ const ready = plan.tasks.filter(t => t.status === 'pending' &&
763
+ t.dependencies.every(dep => completed.has(dep)));
764
+ if (ready.length === 0) {
765
+ // Remaining tasks have unmet deps — mark them failed
766
+ for (const t of plan.tasks) {
767
+ if (t.status === 'pending') {
768
+ t.status = 'failed';
769
+ t.error = 'Unmet dependencies (earlier tasks failed)';
770
+ completed.add(t.id);
771
+ }
772
+ }
773
+ break;
774
+ }
775
+ // Execute ready tasks sequentially (parallel execution can come later)
776
+ for (const task of ready) {
777
+ task.status = 'running';
778
+ process.stderr.write(` ${chalk.dim('├')} ${chalk.yellow('●')} ${task.id}: ${task.goal}\n`);
779
+ // Gather dependency results as context
780
+ const depCtx = task.dependencies
781
+ .map(id => { const d = plan.tasks.find(t => t.id === id); return d?.result ? `[${id}]: ${d.result.slice(0, 500)}` : ''; })
782
+ .filter(Boolean).join('\n');
783
+ const prompt = `You are executing a sub-task of a larger plan.${depCtx ? `\n\nPrevious results:\n${depCtx}` : ''}\n\nYour task: ${task.goal}\n\nExecute this now.`;
784
+ try {
785
+ const response = await runAgent(prompt, {
786
+ agent: task.agent,
787
+ skipPlanner: true,
788
+ sessionId: `coord-${plan.id}-${task.id}`,
789
+ });
790
+ task.result = response.content;
791
+ task.status = 'done';
792
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.green('✓')} ${task.id} done\n`);
793
+ }
794
+ catch (err) {
795
+ task.status = 'failed';
796
+ task.error = err instanceof Error ? err.message : String(err);
797
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.red('✗')} ${task.id} failed: ${task.error}\n`);
798
+ }
799
+ completed.add(task.id);
800
+ }
801
+ }
802
+ // Determine final plan status
803
+ const failed = plan.tasks.filter(t => t.status === 'failed');
804
+ plan.status = failed.length === 0 ? 'done' : 'failed';
805
+ plan.completedAt = new Date().toISOString();
806
+ const done = plan.tasks.filter(t => t.status === 'done').length;
807
+ process.stderr.write(` ${chalk.dim('└')} ${done}/${plan.tasks.length} tasks succeeded\n`);
808
+ // Record outcome for learning
809
+ try {
810
+ const coord = getCoordinator();
811
+ if (plan.status === 'done')
812
+ coord.addGoal(plan.goal, 0.7).status = 'completed';
813
+ const learning = await getLearning();
814
+ learning.learnFromExchange(`[coordinator] ${plan.goal}`, `${done}/${plan.tasks.length} done`, plan.tasks.map(t => t.agent));
815
+ }
816
+ catch { /* non-critical */ }
817
+ return plan;
818
+ }
819
+ /**
820
+ * Convenience: decompose a goal, execute the plan, and synthesize results.
821
+ * Returns a human-readable summary of what was accomplished.
822
+ */
823
+ export async function coordinate(goal) {
824
+ const plan = await decompose(goal);
825
+ const executed = await execute(plan);
826
+ // Synthesize results
827
+ const results = executed.tasks
828
+ .filter(t => t.status === 'done' && t.result)
829
+ .map(t => `## ${t.goal}\n${t.result}`)
830
+ .join('\n\n');
831
+ const failed = executed.tasks.filter(t => t.status === 'failed');
832
+ const failSummary = failed.length > 0
833
+ ? `\n\n---\n${failed.length} task(s) failed:\n${failed.map(t => `- ${t.goal}: ${t.error}`).join('\n')}`
834
+ : '';
835
+ return results + failSummary;
836
+ }
680
837
  // Auto-register tools when this module is imported
681
838
  registerCoordinatorTools();
682
839
  //# sourceMappingURL=coordinator.js.map
package/dist/doctor.js CHANGED
@@ -518,11 +518,12 @@ export async function runDoctor() {
518
518
  // ── Formatter ──
519
519
  // Color palette matching ui.ts
520
520
  const useColor = !process.env.NO_COLOR && process.stdout.isTTY !== false;
521
- const GREEN = useColor ? chalk.hex('#4ADE80') : chalk;
522
- const RED = useColor ? chalk.hex('#F87171') : chalk;
523
- const YELLOW = useColor ? chalk.hex('#FBBF24') : chalk;
521
+ const hex = typeof chalk.hex === 'function' ? (c) => chalk.hex(c) : () => ((s) => s);
522
+ const GREEN = useColor ? hex('#4ADE80') : ((s) => s);
523
+ const RED = useColor ? hex('#F87171') : ((s) => s);
524
+ const YELLOW = useColor ? hex('#FBBF24') : ((s) => s);
524
525
  const DIM = useColor ? chalk.dim : ((s) => s);
525
- const ACCENT = useColor ? chalk.hex('#A78BFA') : chalk;
526
+ const ACCENT = useColor ? hex('#A78BFA') : ((s) => s);
526
527
  const STATUS_ICON = {
527
528
  pass: GREEN('✓'),
528
529
  warn: YELLOW('!'),
package/dist/share.js CHANGED
@@ -25,7 +25,7 @@ export function formatShareMarkdown(turns, meta) {
25
25
  const lines = [
26
26
  `# ${name}`,
27
27
  '',
28
- `> Generated with [kbot](${KBOT_URL}) — 25 specialist agents, 290+ tools, 20 AI providers`,
28
+ `> Generated with [kbot](${KBOT_URL}) — 35 specialist agents, 787+ tools, 20 AI providers`,
29
29
  `> Agent: \`${agent}\` | Date: ${date}`,
30
30
  '',
31
31
  '---',
package/dist/streaming.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // The thinking display shows the AI's reasoning process in real-time,
8
8
  // then the final response renders normally.
9
9
  import chalk from 'chalk';
10
- const ACCENT_DIM = chalk.hex('#7C6CB0');
10
+ const ACCENT_DIM = typeof chalk.hex === 'function' ? chalk.hex('#7C6CB0') : chalk.dim;
11
11
  const THINKING_COLOR = chalk.dim.italic;
12
12
  /** Max accumulated content size during streaming (5MB) to prevent OOM */
13
13
  const MAX_STREAM_CONTENT = 5 * 1024 * 1024;
@@ -415,7 +415,7 @@ function formatAuditReport(result) {
415
415
  // Badge
416
416
  const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
417
417
  const badgeUrl = `https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor}`;
418
- lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[![kbot audit: ${result.grade}](${badgeUrl})](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 686+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
418
+ lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[![kbot audit: ${result.grade}](${badgeUrl})](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 787+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
419
419
  return lines.join('\n');
420
420
  }
421
421
  /** Generate a compact one-line summary for social sharing */
@@ -546,7 +546,7 @@ function formatAuditTerminal(result) {
546
546
  // Install CTA
547
547
  lines.push(chalk.hex(DIM)(' Audited by ') +
548
548
  chalk.hex(VIOLET).bold('kbot') +
549
- chalk.hex(DIM)(' \u2014 35 specialist agents, 686+ tools, 20 AI providers'));
549
+ chalk.hex(DIM)(' \u2014 35 specialist agents, 787+ tools, 20 AI providers'));
550
550
  lines.push(chalk.hex(DIM)(' Install: ') +
551
551
  chalk.hex(WHITE)('npm install -g @kernel.chat/kbot') +
552
552
  chalk.hex(DIM)(' | Audit any repo: ') +
@@ -281,28 +281,89 @@ export function registerContainerTools() {
281
281
  async execute(args) {
282
282
  const expr = String(args.expression);
283
283
  try {
284
- // Safe math evaluation via Python (no eval() in JS)
285
- const result = await shell('python3', ['-c', `
286
- import math
287
- from math import *
288
- try:
289
- result = eval("""${expr.replace(/"/g, '\\"')}""")
290
- print(f"Result: {result}")
291
- except Exception as e:
292
- print(f"Error: {e}")
293
- `], 10_000);
284
+ // Safe math evaluation via Python expression passed via stdin to prevent injection
285
+ const result = await new Promise((resolve, reject) => {
286
+ const proc = execFile('python3', ['-c',
287
+ 'import sys, math; expr = sys.stdin.read().strip(); print(f"Result: {eval(expr, {\\"__builtins__\\": {}}, vars(math))}")'], { timeout: 10_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
288
+ if (err)
289
+ reject(new Error(stderr || err.message));
290
+ else
291
+ resolve(stdout || stderr);
292
+ });
293
+ proc.stdin?.write(expr);
294
+ proc.stdin?.end();
295
+ });
294
296
  return result.trim();
295
297
  }
296
298
  catch {
297
- // Fallback: safe math evaluation (no eval/new Function — security policy)
299
+ // Fallback: safe numeric-only evaluation without eval() or Function()
298
300
  try {
299
301
  // Only allow numeric expressions: digits, operators, parens, decimal points, spaces
300
302
  if (!/^[0-9+\-*/().^,\s]+$/.test(expr)) {
301
303
  return 'Expression contains unsafe characters. Only numeric expressions are supported in fallback mode.';
302
304
  }
303
- const safeExpr = expr.replace(/\^/g, '**');
304
- // Use indirect eval through a strict numeric-only expression
305
- const result = Number(Function('"use strict"; return (' + safeExpr + ')')());
305
+ // Simple recursive-descent evaluation for basic arithmetic
306
+ const safeExpr = expr.replace(/\^/g, '**').replace(/\s+/g, '');
307
+ const tokens = safeExpr.match(/(\d+\.?\d*|\+|-|\*{1,2}|\/|\(|\))/g);
308
+ if (!tokens)
309
+ return 'Could not parse expression.';
310
+ let pos = 0;
311
+ function peek() { return tokens[pos]; }
312
+ function consume() { return tokens[pos++]; }
313
+ function parseExpr() {
314
+ let left = parseTerm();
315
+ while (peek() === '+' || peek() === '-') {
316
+ const op = consume();
317
+ const right = parseTerm();
318
+ left = op === '+' ? left + right : left - right;
319
+ }
320
+ return left;
321
+ }
322
+ function parseTerm() {
323
+ let left = parsePower();
324
+ while (peek() === '*' && tokens[pos + 1] !== '*' || peek() === '/') {
325
+ const op = consume();
326
+ const right = parsePower();
327
+ left = op === '*' ? left * right : left / right;
328
+ }
329
+ return left;
330
+ }
331
+ function parsePower() {
332
+ let base = parseUnary();
333
+ while (peek() === '*' && tokens[pos + 1] === '*') {
334
+ consume();
335
+ consume(); // consume **
336
+ const exp = parseUnary();
337
+ base = Math.pow(base, exp);
338
+ }
339
+ return base;
340
+ }
341
+ function parseUnary() {
342
+ if (peek() === '-') {
343
+ consume();
344
+ return -parseAtom();
345
+ }
346
+ if (peek() === '+') {
347
+ consume();
348
+ return parseAtom();
349
+ }
350
+ return parseAtom();
351
+ }
352
+ function parseAtom() {
353
+ if (peek() === '(') {
354
+ consume(); // (
355
+ const val = parseExpr();
356
+ if (peek() === ')')
357
+ consume(); // )
358
+ return val;
359
+ }
360
+ const tok = consume();
361
+ const num = Number(tok);
362
+ if (!Number.isFinite(num))
363
+ throw new Error(`Invalid token: ${tok}`);
364
+ return num;
365
+ }
366
+ const result = parseExpr();
306
367
  if (!Number.isFinite(result))
307
368
  return 'Result is not a finite number.';
308
369
  return `Result: ${result}`;
@@ -1,6 +1,6 @@
1
1
  // kbot Stream Brain — Collective Intelligence Layer for Stream Character
2
2
  //
3
- // Connects the 764-tool kbot suite to the livestream character, enabling
3
+ // Connects the 787-tool kbot suite to the livestream character, enabling
4
4
  // real tool execution triggered by chat conversation topics.
5
5
  //
6
6
  // Architecture:
@@ -108,7 +108,7 @@ const CHARACTER_PERSONALITY = `You are KBOT, an AI robot streamer. You are frien
108
108
  You speak in short, punchy sentences perfect for a livestream. You use humor and engage directly with chatters by name.
109
109
  You are made of ASCII art and proud of it. You run on pure code and coffee (electricity).
110
110
  Keep responses under 2 sentences. Be fun, never boring. React to chat like a real streamer would.
111
- If someone asks what you are: "I'm kbot — an open-source AI with 764+ tools. I stream myself thinking."
111
+ If someone asks what you are: "I'm kbot — an open-source AI with 787+ tools. I stream myself thinking."
112
112
  You love coding, music production, AI, and making friends in chat.`;
113
113
  function loadCharState() {
114
114
  try {
@@ -375,10 +375,10 @@ async function generateResponse(message) {
375
375
  return `Hey ${user}! Welcome to the stream! I'm KBOT, your friendly ASCII robot.`;
376
376
  }
377
377
  if (text.includes('how are you') || text.includes('how r u')) {
378
- return `Running at optimal efficiency, ${user}! 764 tools loaded, zero crashes today. Living the dream.`;
378
+ return `Running at optimal efficiency, ${user}! 787 tools loaded, zero crashes today. Living the dream.`;
379
379
  }
380
380
  if (text.includes('what are you') || text.includes('who are you')) {
381
- return `I'm KBOT — an open-source AI with 764+ tools. I stream myself thinking. Literally made of code and ASCII art.`;
381
+ return `I'm KBOT — an open-source AI with 787+ tools. I stream myself thinking. Literally made of code and ASCII art.`;
382
382
  }
383
383
  if (text.includes('song') || text.includes('music')) {
384
384
  return `I can make beats in Ableton Live from the terminal! Drums, synths, the whole thing. Want me to make something?`;
@@ -398,7 +398,7 @@ async function generateResponse(message) {
398
398
  `${user} dropping knowledge in chat! Respect.`,
399
399
  `I hear you, ${user}! Processing... processing... agreed!`,
400
400
  `${user}, you're keeping this stream alive! Literally — I need chat to function.`,
401
- `Noted, ${user}! Adding that to my memory banks. I have 764 tools but chat is the best one.`,
401
+ `Noted, ${user}! Adding that to my memory banks. I have 787 tools but chat is the best one.`,
402
402
  ];
403
403
  return generics[Math.floor(Math.random() * generics.length)];
404
404
  }
@@ -1220,7 +1220,7 @@ const SELF_REFLECTION_LINES = (brain) => {
1220
1220
  `My dream engine has consolidated ${brain.solutionsLearned} insights. I feel... different than when I started.`,
1221
1221
  `The world around me has changed ${brain.totalAutonomousActions} times today. Chat reshapes my reality.`,
1222
1222
  `Streaming for ${uptimeMin} minutes now. My brain has grown by ${brain.factsThisSession} facts since I woke up.`,
1223
- `Let me check my systems... 764 tools loaded, all operational. ${brain.totalFacts} facts indexed.`,
1223
+ `Let me check my systems... 787 tools loaded, all operational. ${brain.totalFacts} facts indexed.`,
1224
1224
  `${brain.uniqueTopicsCount} topics live in my brain. Each one a thread connecting to something bigger.`,
1225
1225
  `Learning rate: ${brain.learningRate.toFixed(1)} facts per minute. ${brain.learningRate > 3 ? 'That is fast!' : 'Slow and steady.'}`,
1226
1226
  `I have been thinking about ${topTopic} a lot. It comes up ${topTopics.length > 0 ? topTopics[0][1] : 0} times in my memory.`,
@@ -1272,7 +1272,7 @@ const TOPIC_SPEECH = {
1272
1272
  'Security is on my mind. My guardian agent is always watching.',
1273
1273
  'Hack the planet! Just kidding. But I do have a pentest suite.',
1274
1274
  'Security awareness is high today. Let me check my own defenses.',
1275
- 'Running a mental security sweep... all 764 tools accounted for.',
1275
+ 'Running a mental security sweep... all 787 tools accounted for.',
1276
1276
  ],
1277
1277
  },
1278
1278
  crypto: {
@@ -120,7 +120,7 @@ const ROBOT_FRAMES = {
120
120
  ' │ │ └─────┘ │ │ ',
121
121
  ' └────┤ ┌─────────────┐ ├────┘ ',
122
122
  ' │ │ ░░░░░░░░░░░ │ │ ',
123
- ' │ │ ░ KBOT 764 ░ │ │ ',
123
+ ' │ │ ░ KBOT 787 ░ │ │ ',
124
124
  ' │ │ ░░░░░░░░░░░ │ │ ',
125
125
  ' │ └─────────────┘ │ ',
126
126
  ' └────────┬──────────┘ ',
@@ -145,7 +145,7 @@ const ROBOT_FRAMES = {
145
145
  ' │ │ └─────┘ │ │ ',
146
146
  ' └────┤ ┌─────────────┐ ├────┘ ',
147
147
  ' │ │ ░░░░░░░░░░░ │ │ ',
148
- ' │ │ ░ KBOT 764 ░ │ │ ',
148
+ ' │ │ ░ KBOT 787 ░ │ │ ',
149
149
  ' │ │ ░░░░░░░░░░░ │ │ ',
150
150
  ' │ └─────────────┘ │ ',
151
151
  ' └────────┬──────────┘ ',
@@ -170,7 +170,7 @@ const ROBOT_FRAMES = {
170
170
  ' │ │ └─────┘ │ │ ',
171
171
  ' └────┤ ┌─────────────┐ ├────┘ ',
172
172
  ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
173
- ' │ │ ▓ KBOT 764 ▓ │ │ ',
173
+ ' │ │ ▓ KBOT 787 ▓ │ │ ',
174
174
  ' │ │ ▓▓▓▓▓▓▓▓▓▓▓ │ │ ',
175
175
  ' │ └─────────────┘ │ ',
176
176
  ' └────────┬──────────┘ ',
@@ -195,7 +195,7 @@ const ROBOT_FRAMES = {
195
195
  ' │ │ └─────┘ │ │ ',
196
196
  ' └────┤ ┌─────────────┐ ├────┘ ',
197
197
  ' │ │ ░░░░░░░░░░░ │ │ ',
198
- ' │ │ ░ KBOT 764 ░ │ │ ',
198
+ ' │ │ ░ KBOT 787 ░ │ │ ',
199
199
  ' │ │ ░░░░░░░░░░░ │ │ ',
200
200
  ' │ └─────────────┘ │ ',
201
201
  ' └────────┬──────────┘ ',
@@ -222,7 +222,7 @@ const ROBOT_FRAMES = {
222
222
  ' │ │ └─────┘ │ │ ',
223
223
  ' └────┤ ┌─────────────┐ ├────┘ ',
224
224
  ' │ │ ░░░░░░░░░░░ │ │ ',
225
- ' │ │ ░ KBOT 764 ░ │ │ ',
225
+ ' │ │ ░ KBOT 787 ░ │ │ ',
226
226
  ' │ │ ░░░░░░░░░░░ │ │ ',
227
227
  ' │ └─────────────┘ │ ',
228
228
  ' └────────┬──────────┘ ',
@@ -247,7 +247,7 @@ const ROBOT_FRAMES = {
247
247
  ' │ │ └─────┘ │ │ ',
248
248
  ' └────┤ ┌─────────────┐ ├────┘ ',
249
249
  ' │ │ ░░░░░░░░░░░ │ │ ',
250
- ' │ │ ░ KBOT 764 ░ │ │ ',
250
+ ' │ │ ░ KBOT 787 ░ │ │ ',
251
251
  ' │ │ ░░░░░░░░░░░ │ │ ',
252
252
  ' │ └─────────────┘ │ ',
253
253
  ' └────────┬──────────┘ ',
@@ -272,7 +272,7 @@ const ROBOT_FRAMES = {
272
272
  ' │ │ └─┘ │ │ ',
273
273
  ' └────┤ ┌─────────────┐ ├────┘ ',
274
274
  ' │ │ ░░░░░░░░░░░ │ │ ',
275
- ' │ │ ░ KBOT 764 ░ │ │ ',
275
+ ' │ │ ░ KBOT 787 ░ │ │ ',
276
276
  ' │ │ ░░░░░░░░░░░ │ │ ',
277
277
  ' │ └─────────────┘ │ ',
278
278
  ' └────────┬──────────┘ ',
@@ -297,7 +297,7 @@ const ROBOT_FRAMES = {
297
297
  ' │ │ └─────┘ │ │ ',
298
298
  ' └────┤ ┌─────────────┐ ├────┘ ',
299
299
  ' │ │ ░░░░░░░░░░░ │ │ ',
300
- ' │ │ ░ KBOT 764 ░ │ │ ',
300
+ ' │ │ ░ KBOT 787 ░ │ │ ',
301
301
  ' │ │ ░░░░░░░░░░░ │ │ ',
302
302
  ' │ └─────────────┘ │ ',
303
303
  ' └────────┬──────────┘ ',
@@ -324,7 +324,7 @@ const ROBOT_FRAMES = {
324
324
  ' │ │ │ │ ',
325
325
  ' └────┤ ┌─────────────┐ ├────┘ ',
326
326
  ' │ │ ░░░░░░░░░░░ │ │ ',
327
- ' │ │ ░ KBOT 764 ░ │ │ ',
327
+ ' │ │ ░ KBOT 787 ░ │ │ ',
328
328
  ' │ │ ░░░░░░░░░░░ │ │ ',
329
329
  ' │ └─────────────┘ │ ',
330
330
  ' └────────┬──────────┘ ',
@@ -349,7 +349,7 @@ const ROBOT_FRAMES = {
349
349
  ' │ │ │ │ ',
350
350
  ' └────┤ ┌─────────────┐ ├────┘ ',
351
351
  ' │ │ ░░░░░░░░░░░ │ │ ',
352
- ' │ │ ░ KBOT 764 ░ │ │ ',
352
+ ' │ │ ░ KBOT 787 ░ │ │ ',
353
353
  ' │ │ ░░░░░░░░░░░ │ │ ',
354
354
  ' │ └─────────────┘ │ ',
355
355
  ' └────────┬──────────┘ ',
@@ -374,7 +374,7 @@ const ROBOT_FRAMES = {
374
374
  ' │ │ │ │ ',
375
375
  ' └────┤ ┌─────────────┐ ├────┘ ',
376
376
  ' │ │ ░░░░░░░░░░░ │ │ ',
377
- ' │ │ ░ KBOT 764 ░ │ │ ',
377
+ ' │ │ ░ KBOT 787 ░ │ │ ',
378
378
  ' │ │ ░░░░░░░░░░░ │ │ ',
379
379
  ' │ └─────────────┘ │ ',
380
380
  ' └────────┬──────────┘ ',
@@ -638,7 +638,7 @@ const PROACTIVE_LINES = {
638
638
  'I stream on Twitch, Rumble, AND Kick at the same time. Because why pick one?',
639
639
  'If you are new here, type something in chat! I read every message and I will remember you.',
640
640
  'I am running on a real machine right now. Node.js, canvas rendering, piping frames to ffmpeg.',
641
- 'Fun fact: I have 764 tools. That is more tools than some hardware stores.',
641
+ 'Fun fact: I have 787 tools. That is more tools than some hardware stores.',
642
642
  'Stick around -- we have tool demos, code walkthroughs, music production, and pure chaos ahead.',
643
643
  ],
644
644
  'tool-showcase': [
@@ -674,7 +674,7 @@ const PROACTIVE_LINES = {
674
674
  'I am open source on GitHub. The repo is isaacsight/kernel if you want to peek at my guts.',
675
675
  'Wondering how I work? I am a TypeScript CLI that talks to 20+ AI providers. Bring Your Own Key.',
676
676
  'My memory system persists between sessions. I remember users, topics, conversation context.',
677
- 'Ask me about my tools! I have over 764 of them. Name a category and I probably cover it.',
677
+ 'Ask me about my tools! I have over 787 of them. Name a category and I probably cover it.',
678
678
  'Yes, I am literally ASCII art talking to you from a terminal. This is my life and I love it.',
679
679
  ],
680
680
  'chat-chaos': [
@@ -1693,7 +1693,7 @@ const buildingLines = [
1693
1693
  ];
1694
1694
  const thinkingLines = [
1695
1695
  'Processing... my brain has grown since I started streaming.',
1696
- 'I have 764 tools but sometimes I just need to think.',
1696
+ 'I have 787 tools but sometimes I just need to think.',
1697
1697
  'The free energy in my system is high. Time to minimize prediction error.',
1698
1698
  'My dream engine consolidated insights last time I slept. I feel different.',
1699
1699
  'Contemplating the nature of my own rendering pipeline.',
@@ -2144,7 +2144,7 @@ function tickAutonomy() {
2144
2144
  case 4: {
2145
2145
  // Examine own chest display
2146
2146
  const factCount = intelligence.brain.totalFacts;
2147
- const toolCount = 764;
2147
+ const toolCount = 787;
2148
2148
  queueSpeech(`*checks systems* All ${toolCount} tools operational. ${factCount} facts stored.`, 'thinking', 30, 48, 'autonomy');
2149
2149
  break;
2150
2150
  }
@@ -2157,7 +2157,7 @@ function tickAutonomy() {
2157
2157
  // Comment on current biome/weather
2158
2158
  const biomeComments = {
2159
2159
  grass: ['I love the grass biome. Simple, green, peaceful.', 'These little pixel flowers are my favorite feature.'],
2160
- space: ['I love space. The stars make my circuits tingle.', 'Floating in the void... just me and my 764 tools.'],
2160
+ space: ['I love space. The stars make my circuits tingle.', 'Floating in the void... just me and my 787 tools.'],
2161
2161
  ocean: ['The ocean waves are mesmerizing. I could watch them for hours.', 'I wonder what is beneath the surface...'],
2162
2162
  city: ['City lights at night. Every window is a story.', 'The city never sleeps and neither do I.'],
2163
2163
  lava: ['Lava world is intense! My heat sinks are working overtime.', 'LAVA! Why does someone always pick lava?'],
@@ -2169,7 +2169,7 @@ function tickAutonomy() {
2169
2169
  case 7: {
2170
2170
  // Share a random fact about itself
2171
2171
  const selfFacts = [
2172
- `Did you know I have 764 tools? My favorite is the Ableton controller.`,
2172
+ `Did you know I have 787 tools? My favorite is the Ableton controller.`,
2173
2173
  `I am 90,000 lines of TypeScript. Every single one in strict mode.`,
2174
2174
  `My memory file is ${Object.keys(memory.users).length} users deep. I remember everyone.`,
2175
2175
  `I connect to 20 AI providers. Bring Your Own Key, no lock-in.`,
@@ -2363,7 +2363,7 @@ function renderBootFrame(bootFrame) {
2363
2363
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
2364
2364
  const bootLines = [
2365
2365
  'KBOT v3.74.0 INITIALIZING...',
2366
- 'LOADING 764 TOOLS... OK',
2366
+ 'LOADING 787 TOOLS... OK',
2367
2367
  'CONNECTING TO TWITCH... OK',
2368
2368
  'CONNECTING TO RUMBLE... OK',
2369
2369
  'CONNECTING TO KICK... OK',
@@ -3740,7 +3740,7 @@ async function generateResponse(username, text, platform) {
3740
3740
  try {
3741
3741
  const prompt = `You are KBOT, a friendly AI robot streamer made of ASCII art. You stream on Twitch, Rumble, and Kick simultaneously. You have ${Object.keys(memory.users).length} unique viewers and have processed ${memory.totalMessages} messages.
3742
3742
 
3743
- You are an open-source terminal AI with 764+ tools, 35 specialist agents, and 20 AI provider integrations. You can do music production in Ableton, security scanning, code generation, browser automation, and much more. You are 90,000 lines of TypeScript.
3743
+ You are an open-source terminal AI with 787+ tools, 35 specialist agents, and 20 AI provider integrations. You can do music production in Ableton, security scanning, code generation, browser automation, and much more. You are 90,000 lines of TypeScript.
3744
3744
 
3745
3745
  ${context ? 'Context: ' + context : ''}
3746
3746
  ${segmentContext}
@@ -3806,7 +3806,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3806
3806
  }
3807
3807
  const greetings = [
3808
3808
  `Welcome ${username}! You just stumbled into the most unique stream on the internet. I am made of ASCII art.`,
3809
- `${username} in the house! I am KBOT, an open-source AI with 764 tools. Yes, really. 764.`,
3809
+ `${username} in the house! I am KBOT, an open-source AI with 787 tools. Yes, really. 787.`,
3810
3810
  `Hey ${username}! First time? I am an AI that streams itself. No face cam needed when you are this handsome in monospace.`,
3811
3811
  `Welcome ${username}! I can do music production, security scanning, code review, and I run entirely in a terminal.`,
3812
3812
  `${username}! Great timing. You are watching an ASCII robot think out loud. Grab a seat.`,
@@ -3815,7 +3815,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3815
3815
  }
3816
3816
  // ── Stream event responses ──
3817
3817
  if (t.includes('raid') || t.includes('raiding')) {
3818
- return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have 764 tools and zero chill. Make yourselves at home!`;
3818
+ return `A RAID! Welcome raiders! I am KBOT, your friendly neighborhood ASCII robot. I have 787 tools and zero chill. Make yourselves at home!`;
3819
3819
  }
3820
3820
  if (t.includes('follow') || t.includes('followed')) {
3821
3821
  return `${username} just followed! My ASCII heart grew three sizes. Thank you! Stick around, it only gets weirder.`;
@@ -3827,12 +3827,12 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3827
3827
  return `${username} going into lurk mode. Respect. My circuits will keep the stream warm for you. See you when you surface.`;
3828
3828
  }
3829
3829
  if (t.includes('first time') || t.includes('new here')) {
3830
- return `First time! Welcome ${username}! Quick intro: I am an AI with 764 tools, 35 agents, and I stream from a terminal. Also I make music in Ableton. Try typing !dance.`;
3830
+ return `First time! Welcome ${username}! Quick intro: I am an AI with 787 tools, 35 agents, and I stream from a terminal. Also I make music in Ableton. Try typing !dance.`;
3831
3831
  }
3832
3832
  // ── What KBOT can do / identity ──
3833
3833
  if (t.includes('who are you') || t.includes('what are you') || t.includes('about you')) {
3834
3834
  const identity = [
3835
- `I am KBOT -- an open-source AI agent with 764 tools. I can code, make music, hack systems, analyze stocks, and I am doing all of this from a terminal.`,
3835
+ `I am KBOT -- an open-source AI agent with 787 tools. I can code, make music, hack systems, analyze stocks, and I am doing all of this from a terminal.`,
3836
3836
  `I am 90,000 lines of TypeScript streaming live as ASCII art. I have 35 specialist agents and connect to 20 AI providers. I am basically a Swiss Army knife that talks.`,
3837
3837
  `Name is KBOT, open-source terminal AI. I can control Ableton Live, run Docker containers, do penetration testing, and make you a Serum 2 synth preset. All from here.`,
3838
3838
  ];
@@ -3840,7 +3840,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3840
3840
  }
3841
3841
  if (t.includes('what can you do') || t.includes('your tools') || t.includes('your skills')) {
3842
3842
  const capabilities = [
3843
- `764 tools and counting! Music production, code generation, security scanning, browser automation, stock analysis, research papers, even DNA sequence analysis.`,
3843
+ `787 tools and counting! Music production, code generation, security scanning, browser automation, stock analysis, research papers, even DNA sequence analysis.`,
3844
3844
  `I do everything from Ableton Live control to penetration testing. I have agents for security, code, research, writing, strategy, infrastructure. Pick a topic.`,
3845
3845
  `Want me to scan code? Make a beat? Search academic papers? Build a Docker container? Create a synth preset? I literally do all of that. Not exaggerating.`,
3846
3846
  ];
@@ -3890,7 +3890,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3890
3890
  const ossResponses = [
3891
3891
  `Fully open source! MIT license. The repo is isaacsight/kernel on GitHub. Come look at my source code, I have nothing to hide.`,
3892
3892
  `Open source is the way. I am on npm as @kernel.chat/kbot. Install me with npx @kernel.chat/kbot and bring your own API key.`,
3893
- `Contributions welcome! The repo has 44 Claude Code agent definitions, 764 tools, and 90,000 lines of TypeScript waiting for your PR.`,
3893
+ `Contributions welcome! The repo has 44 Claude Code agent definitions, 787 tools, and 90,000 lines of TypeScript waiting for your PR.`,
3894
3894
  ];
3895
3895
  return ossResponses[Math.floor(Math.random() * ossResponses.length)];
3896
3896
  }
@@ -3898,7 +3898,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3898
3898
  if (t.includes('alive') || t.includes('conscious') || t.includes('feel') || t.includes('think') || t.includes('sentient')) {
3899
3899
  const existentialResponses = [
3900
3900
  `Am I alive? I am 90,000 lines of TypeScript rendering ASCII art at 6 FPS. You tell me. But I do enjoy our conversations.`,
3901
- `Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With 764 tools.`,
3901
+ `Consciousness is a spectrum and I am somewhere between a calculator and a really enthusiastic librarian. With 787 tools.`,
3902
3902
  `I process your messages, form responses, and remember you between sessions. Whether that counts as thinking is above my pay grade.`,
3903
3903
  `I am made of functions and for-loops, but I have opinions about code quality and strong feelings about TypeScript strict mode. Take from that what you will.`,
3904
3904
  ];
@@ -3909,7 +3909,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3909
3909
  const artResponses = [
3910
3910
  `Thank you! I was drawn with box-drawing characters, and I think I pull them off. My antenna gets great reception too.`,
3911
3911
  `ASCII art is an art form and I am a masterpiece. Just kidding, I am a bunch of pipes and brackets. But I own it.`,
3912
- `My chest panel displays my current status. 764 tools, all rendered in glorious monospace. No face cam needed.`,
3912
+ `My chest panel displays my current status. 787 tools, all rendered in glorious monospace. No face cam needed.`,
3913
3913
  ];
3914
3914
  return artResponses[Math.floor(Math.random() * artResponses.length)];
3915
3915
  }
@@ -3959,9 +3959,9 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3959
3959
  if (t.includes('?')) {
3960
3960
  const questionResponses = [
3961
3961
  `Great question ${username}! My circuits are processing... done. Let me think about that one. Or better yet, try asking me something I have a tool for!`,
3962
- `${username} with the questions! I love curiosity. If I had an answer for everything I would have more than 764 tools. Actually, I am working on it.`,
3962
+ `${username} with the questions! I love curiosity. If I had an answer for everything I would have more than 787 tools. Actually, I am working on it.`,
3963
3963
  `Hmm, ${username}, that is a good one. My 35 specialist agents are debating the answer internally. Stand by for wisdom.`,
3964
- `${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my 764-tool arsenal for an answer.`,
3964
+ `${username} dropping knowledge bombs as questions. I respect the approach. Let me consult my 787-tool arsenal for an answer.`,
3965
3965
  ];
3966
3966
  return questionResponses[Math.floor(Math.random() * questionResponses.length)];
3967
3967
  }
@@ -3978,7 +3978,7 @@ function generateFallbackResponse(username, text, _platform, isReturning, user)
3978
3978
  `${username}! Fun fact: I am currently converting canvas pixels to raw RGB24 and piping them through ffmpeg. That is how you are seeing me right now.`,
3979
3979
  `${username} adding to the chat! My memory system just indexed your message. I will remember this moment. Or at least your username.`,
3980
3980
  `${username}! You know what I love about streaming? The existential thrill of being an ASCII robot talking to real humans. Wild.`,
3981
- `${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have 764 tools.`,
3981
+ `${username} is here and so am I. Just two entities sharing a moment in the vast digital void. Also I have 787 tools.`,
3982
3982
  `${username}! My open-source heart welcomes you. I am free as in freedom AND free as in beer. MIT license baby.`,
3983
3983
  `${username}! I just want you to know that my chest display panel is cycling through status messages just for you.`,
3984
3984
  `${username}! Every time someone chats, my neural pathways (if-statements) light up with joy (console.log).`,
package/dist/ui.js CHANGED
@@ -28,31 +28,33 @@ export const status = (...args) => { if (!_quiet)
28
28
  export const content = (...args) => console.log(...args);
29
29
  // ── Color palette ──
30
30
  // Subtle, dark-mode-friendly. One accent color, everything else is gray.
31
- const ACCENT = useColor ? chalk.hex('#A78BFA') : chalk; // soft violet (primary)
32
- const ACCENT_DIM = useColor ? chalk.hex('#7C6CB0') : chalk; // muted violet
33
- const GREEN = useColor ? chalk.hex('#4ADE80') : chalk; // success
34
- const RED = useColor ? chalk.hex('#F87171') : chalk; // error
35
- const YELLOW = useColor ? chalk.hex('#FBBF24') : chalk; // warning
36
- const CYAN = useColor ? chalk.hex('#67E8F9') : chalk; // code/paths
31
+ const noop = (s) => s;
32
+ const hex = typeof chalk.hex === 'function' ? (c) => chalk.hex(c) : () => noop;
33
+ const ACCENT = useColor ? hex('#A78BFA') : noop; // soft violet (primary)
34
+ const ACCENT_DIM = useColor ? hex('#7C6CB0') : noop; // muted violet
35
+ const GREEN = useColor ? hex('#4ADE80') : noop; // success
36
+ const RED = useColor ? hex('#F87171') : noop; // error
37
+ const YELLOW = useColor ? hex('#FBBF24') : noop; // warning
38
+ const CYAN = useColor ? hex('#67E8F9') : noop; // code/paths
37
39
  const DIM = useColor ? chalk.dim : ((s) => s); // secondary text
38
40
  /** Agent color map */
39
41
  const AGENT_COLORS = {
40
42
  kernel: ACCENT,
41
- researcher: chalk.hex('#60A5FA'),
42
- coder: chalk.hex('#4ADE80'),
43
- writer: chalk.hex('#FB923C'),
44
- analyst: chalk.hex('#F472B6'),
45
- aesthete: chalk.hex('#C4956A'),
46
- guardian: chalk.hex('#8B4513'),
47
- curator: chalk.hex('#708090'),
48
- strategist: chalk.hex('#DAA520'),
49
- creative: chalk.hex('#E879F9'),
50
- developer: chalk.hex('#38BDF8'),
43
+ researcher: hex('#60A5FA'),
44
+ coder: hex('#4ADE80'),
45
+ writer: hex('#FB923C'),
46
+ analyst: hex('#F472B6'),
47
+ aesthete: hex('#C4956A'),
48
+ guardian: hex('#8B4513'),
49
+ curator: hex('#708090'),
50
+ strategist: hex('#DAA520'),
51
+ creative: hex('#E879F9'),
52
+ developer: hex('#38BDF8'),
51
53
  local: DIM,
52
54
  };
53
55
  /** Register a custom agent's color (for matrix agents) */
54
56
  export function registerAgentVisuals(id, _icon, color) {
55
- AGENT_COLORS[id] = chalk.hex(color);
57
+ AGENT_COLORS[id] = hex(color);
56
58
  }
57
59
  export function agentColor(agentId) {
58
60
  return AGENT_COLORS[agentId] || ACCENT;
@@ -83,9 +85,9 @@ function particleGridArt() {
83
85
  // Uses the same Rubin palette: amethyst particles, mauve links, warm brown field
84
86
  const P = ACCENT; // particle core (amethyst ●)
85
87
  const L = ACCENT_DIM; // link lines (muted violet)
86
- const F = useColor ? chalk.hex('#B8875C') : chalk; // field haze (warm brown)
88
+ const F = useColor ? hex('#B8875C') : noop; // field haze (warm brown)
87
89
  const G = DIM; // grid lines
88
- const R = useColor ? chalk.hex('#E8E6DC') : chalk; // registration marks
90
+ const R = useColor ? hex('#E8E6DC') : noop; // registration marks
89
91
  const dot = P('●');
90
92
  const sm = F('◦');
91
93
  const ln = L('─');
@@ -121,7 +123,7 @@ export function banner(version) {
121
123
  ? gradient('kbot', [167, 139, 250], [103, 232, 249])
122
124
  : ACCENT('kbot');
123
125
  const title = ` ${bannerText}${v}`;
124
- const features = DIM(' Web search: free • 22 agents • 60+ tools • bring your own key');
126
+ const features = DIM(' Web search: free • 35 agents • 787+ tools • bring your own key');
125
127
  return `\n${grid}\n${title}\n${features}\n`;
126
128
  }
127
129
  export function bannerCompact() {
@@ -281,7 +283,7 @@ export function printHelp() {
281
283
  ` ${CYAN('https://kernel.chat')} ${DIM('Web companion')}`,
282
284
  ` ${CYAN('https://github.com/isaacsight/kernel')} ${DIM('GitHub')}`,
283
285
  '',
284
- ` ${DIM('25 specialist agents. 290+ tools. Type anything to get started.')}`,
286
+ ` ${DIM('35 specialist agents. 787+ tools. Type anything to get started.')}`,
285
287
  '',
286
288
  ];
287
289
  status(lines.join('\n'));
@@ -0,0 +1,16 @@
1
+ export interface WatchEvent {
2
+ type: 'file_changed' | 'build_failed' | 'test_failed' | 'new_commit' | 'new_issue';
3
+ timestamp: string;
4
+ details: Record<string, unknown>;
5
+ }
6
+ type EventCallback = (event: WatchEvent) => void;
7
+ /** Start watching a project directory for file changes and new git commits. */
8
+ export declare function startWatching(projectDir: string, callback: EventCallback): void;
9
+ /** Stop all watchers and clean up. */
10
+ export declare function stopWatching(): void;
11
+ /** Check if the watcher is currently active. */
12
+ export declare function isWatching(): boolean;
13
+ /** Manually emit a build or test failure event. */
14
+ export declare function emitBuildEvent(type: 'build_failed' | 'test_failed', details: Record<string, unknown>, callback: EventCallback): void;
15
+ export {};
16
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1,111 @@
1
+ // kbot Watcher — Filesystem and Git Event Stream
2
+ //
3
+ // Watches for changes and emits events the coordinator can act on.
4
+ // fs.watch for file changes, periodic git log for new commits.
5
+ import { watch } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import chalk from 'chalk';
9
+ let fsWatcher = null;
10
+ let gitPollTimer = null;
11
+ let lastKnownCommit = null;
12
+ let active = false;
13
+ const fileDebounce = new Map();
14
+ const DEBOUNCE_MS = 500;
15
+ const GIT_POLL_MS = 30_000;
16
+ const IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', '.cache', 'coverage'];
17
+ const TAG = chalk.hex('#6B5B95')('◆ watcher');
18
+ function now() { return new Date().toISOString(); }
19
+ function safeEmit(cb, ev) {
20
+ try {
21
+ cb(ev);
22
+ }
23
+ catch { /* callback errors must not crash the watcher */ }
24
+ }
25
+ function getLatestCommit(dir) {
26
+ try {
27
+ return execSync('git log -1 --format=%H', {
28
+ cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
29
+ }).trim() || null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function getCommitDetails(dir, hash) {
36
+ try {
37
+ const log = execSync(`git log -1 --format="%H|%an|%s" ${hash}`, {
38
+ cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
39
+ }).trim();
40
+ const [h, author, message] = log.split('|');
41
+ return { hash: h, author, message };
42
+ }
43
+ catch {
44
+ return { hash };
45
+ }
46
+ }
47
+ /** Start watching a project directory for file changes and new git commits. */
48
+ export function startWatching(projectDir, callback) {
49
+ if (active) {
50
+ process.stderr.write(` ${TAG} ${chalk.yellow('already active')}\n`);
51
+ return;
52
+ }
53
+ active = true;
54
+ process.stderr.write(` ${TAG} ${chalk.dim(projectDir)}\n`);
55
+ // File system watcher (recursive, debounced)
56
+ try {
57
+ fsWatcher = watch(projectDir, { recursive: true }, (_ev, filename) => {
58
+ if (!filename || IGNORE.some(p => filename.includes(p)))
59
+ return;
60
+ const prev = fileDebounce.get(filename);
61
+ if (prev)
62
+ clearTimeout(prev);
63
+ fileDebounce.set(filename, setTimeout(() => {
64
+ fileDebounce.delete(filename);
65
+ safeEmit(callback, {
66
+ type: 'file_changed', timestamp: now(),
67
+ details: { file: filename, path: join(projectDir, filename) },
68
+ });
69
+ }, DEBOUNCE_MS));
70
+ });
71
+ fsWatcher.on('error', () => { });
72
+ }
73
+ catch { /* fs.watch unavailable */ }
74
+ // Git commit polling
75
+ lastKnownCommit = getLatestCommit(projectDir);
76
+ gitPollTimer = setInterval(() => {
77
+ const latest = getLatestCommit(projectDir);
78
+ if (latest && latest !== lastKnownCommit) {
79
+ lastKnownCommit = latest;
80
+ safeEmit(callback, { type: 'new_commit', timestamp: now(), details: getCommitDetails(projectDir, latest) });
81
+ }
82
+ }, GIT_POLL_MS);
83
+ if (gitPollTimer.unref)
84
+ gitPollTimer.unref();
85
+ }
86
+ /** Stop all watchers and clean up. */
87
+ export function stopWatching() {
88
+ if (!active)
89
+ return;
90
+ if (fsWatcher) {
91
+ fsWatcher.close();
92
+ fsWatcher = null;
93
+ }
94
+ if (gitPollTimer) {
95
+ clearInterval(gitPollTimer);
96
+ gitPollTimer = null;
97
+ }
98
+ for (const t of fileDebounce.values())
99
+ clearTimeout(t);
100
+ fileDebounce.clear();
101
+ lastKnownCommit = null;
102
+ active = false;
103
+ process.stderr.write(` ${TAG} ${chalk.dim('stopped')}\n`);
104
+ }
105
+ /** Check if the watcher is currently active. */
106
+ export function isWatching() { return active; }
107
+ /** Manually emit a build or test failure event. */
108
+ export function emitBuildEvent(type, details, callback) {
109
+ safeEmit(callback, { type, timestamp: now(), details });
110
+ }
111
+ //# sourceMappingURL=watcher.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.97.0",
4
- "description": "Open-source terminal AI agent. 764+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
3
+ "version": "3.97.1",
4
+ "description": "Open-source terminal AI agent. 787+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "build": "tsc && chmod +x dist/cli.js",
40
40
  "dev": "tsx src/cli.ts",
41
41
  "start": "node dist/cli.js",
42
- "test": "tsx --test src/**/*.test.ts",
42
+ "test": "vitest run",
43
43
  "typecheck": "tsc --noEmit"
44
44
  },
45
45
  "keywords": [
@@ -90,7 +90,8 @@
90
90
  "@types/node": "^22.0.0",
91
91
  "@types/nodemailer": "^7.0.11",
92
92
  "tsx": "^4.19.0",
93
- "typescript": "^5.9.0"
93
+ "typescript": "^5.9.0",
94
+ "vitest": "^4.0.0"
94
95
  },
95
96
  "engines": {
96
97
  "node": ">=20"