@kernel.chat/kbot 3.95.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.
@@ -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}`;
@@ -329,6 +329,12 @@ const LAZY_MODULE_IMPORTS = [
329
329
  { path: './coordination-engine.js', registerFn: 'registerCoordinationEngineTools' },
330
330
  { path: './foundation-engines.js', registerFn: 'registerFoundationEngineTools' },
331
331
  { path: './research-engine.js', registerFn: 'registerResearchEngineTools' },
332
+ { path: './stream-overlay.js', registerFn: 'registerOverlayTools' },
333
+ { path: './stream-weather.js', registerFn: 'registerStreamWeatherTools' },
334
+ { path: './stream-chat-ai.js', registerFn: 'registerStreamChatAITools' },
335
+ { path: './stream-vod.js', registerFn: 'registerStreamVODTools' },
336
+ { path: './stream-commands.js', registerFn: 'registerStreamCommandsTools' },
337
+ { path: '../coordinator.js', registerFn: 'registerCoordinatorTools' },
332
338
  ];
333
339
  /** Track whether lazy tools have been registered */
334
340
  let lazyToolsRegistered = false;
@@ -1,5 +1,23 @@
1
1
  import type { CanvasRenderingContext2D } from 'canvas';
2
2
  export declare function drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, mood: string, frame: number, moodColor?: [number, number, number], weather?: 'clear' | 'rain' | 'snow' | 'storm' | 'stars', isWalking?: boolean, walkPhase?: number): void;
3
+ /**
4
+ * Draw a stocky gorilla/monkey pixel art character (32x32 grid).
5
+ * Drop-in replacement for drawRobot() with the same signature.
6
+ *
7
+ * @param ctx - Canvas 2D rendering context
8
+ * @param x - Top-left X position in canvas pixels
9
+ * @param y - Top-left Y position in canvas pixels
10
+ * @param scale - Pixel scale multiplier (4-10 recommended)
11
+ * @param mood - Current mood: idle, talking, thinking, excited, dancing, walking
12
+ * @param frame - Animation frame counter (incrementing integer)
13
+ * @param moodColor - Optional RGB override for mood accent color
14
+ */
15
+ export declare function drawGorilla(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, mood: string, frame: number, moodColor?: [number, number, number]): void;
16
+ /**
17
+ * Draw animated mood particles around the gorilla.
18
+ * Same interface as drawMoodParticles but tuned for gorilla position/shape.
19
+ */
20
+ export declare function drawGorillaParticles(ctx: CanvasRenderingContext2D, x: number, y: number, scale: number, mood: string, frame: number): void;
3
21
  /**
4
22
  * Draw animated mood particles around the robot.
5
23
  *
@@ -151,7 +151,7 @@ function drawHead(ctx, s, ox, oy, eyeColor, mood, frame, headShiftX) {
151
151
  // Eye glow background — brighter white-green for alive look, dimmed if dreaming
152
152
  const eyeC = mood === 'dreaming'
153
153
  ? dimColor(eyeColor.startsWith('rgb') ? '#4a6670' : eyeColor, 0.5)
154
- : '#80ffb0'; // bright cyan-green that contrasts against the green head
154
+ : '#b0ffe0'; // HACK 3: even brighter cyan-white for maximum pop
155
155
  px(ctx, hx + 2, eyeY, 4, eyeH, eyeC, s, ox, oy);
156
156
  px(ctx, hx + 8, eyeY, 4, eyeH, eyeC, s, ox, oy);
157
157
  // Specular highlights on eyes — 2px L-shape catch-light for glassy/alive look (technique 8)
@@ -895,6 +895,440 @@ function drawWalkingLegs(ctx, s, ox, oy, bodyShiftY, walkPhase) {
895
895
  px(ctx, 21 + rightLegOffset, footY, 1, 2, PAL.rimLight, s, ox, oy);
896
896
  px(ctx, 17 + rightLegOffset, footY + 2, 4, 1, PAL.jetOrange, s, ox, oy);
897
897
  }
898
+ // ─── Gorilla Character ────────────────────────────────────────
899
+ const GORILLA = {
900
+ furDark: '#8B6914', // dark brown
901
+ furMain: '#C4943D', // main tan/brown
902
+ furLight: '#DEB860', // light tan highlights
903
+ furChest: '#E8D5A0', // pale chest/face
904
+ skinDark: '#7A5B2E', // darker skin (face creases)
905
+ eyeWhite: '#F0F0E0', // eye whites
906
+ eyePupil: '#1a1a1a', // dark pupils
907
+ mouth: '#3D2B1A', // dark mouth
908
+ fang: '#F0F0E0', // white fangs
909
+ capRed: '#CC2222', // red cap
910
+ capWhite: '#F0F0F0', // white cap panel
911
+ capBrim: '#999999', // brim underside
912
+ claws: '#D0D0D0', // light gray claws
913
+ outline: '#2A1F0A', // dark brown outline
914
+ nose: '#5A3D1E', // nose color
915
+ };
916
+ let _gorillaPrevMood = '';
917
+ let _gorillaSettleFrames = 0;
918
+ /**
919
+ * Draw a stocky gorilla/monkey pixel art character (32x32 grid).
920
+ * Drop-in replacement for drawRobot() with the same signature.
921
+ *
922
+ * @param ctx - Canvas 2D rendering context
923
+ * @param x - Top-left X position in canvas pixels
924
+ * @param y - Top-left Y position in canvas pixels
925
+ * @param scale - Pixel scale multiplier (4-10 recommended)
926
+ * @param mood - Current mood: idle, talking, thinking, excited, dancing, walking
927
+ * @param frame - Animation frame counter (incrementing integer)
928
+ * @param moodColor - Optional RGB override for mood accent color
929
+ */
930
+ export function drawGorilla(ctx, x, y, scale, mood, frame, moodColor) {
931
+ const s = scale;
932
+ const G = GORILLA;
933
+ // Settle animation on mood change
934
+ if (mood !== _gorillaPrevMood) {
935
+ _gorillaSettleFrames = 3;
936
+ _gorillaPrevMood = mood;
937
+ }
938
+ let settleShift = 0;
939
+ if (_gorillaSettleFrames > 0) {
940
+ settleShift = _gorillaSettleFrames === 3 ? -1 : _gorillaSettleFrames === 2 ? 1 : 0;
941
+ _gorillaSettleFrames--;
942
+ }
943
+ // ── Animation offsets ──
944
+ let bodyY = settleShift;
945
+ let bodyX = 0;
946
+ let headTilt = 0;
947
+ let mouthOpen = 0; // 0=closed, 1=half, 2=open, 3=wide
948
+ let leftArmFwd = 0; // forward offset for walking
949
+ let rightArmFwd = 0;
950
+ let eyeState = 'narrow'; // default grumpy
951
+ let capTilt = 0;
952
+ let questionMark = false;
953
+ // Breathing (idle)
954
+ const breathFrame = frame % 12;
955
+ let breathShift = 0;
956
+ if (mood === 'idle') {
957
+ if (breathFrame >= 1 && breathFrame <= 3)
958
+ breathShift = -1; // rise
959
+ if (breathFrame >= 4 && breathFrame <= 5) {
960
+ breathShift = 0;
961
+ bodyY += 1;
962
+ }
963
+ // Blink every ~24 frames
964
+ if (frame % 24 === 23)
965
+ eyeState = 'blink';
966
+ else if (frame % 24 === 22)
967
+ eyeState = 'blink';
968
+ }
969
+ if (mood === 'talking') {
970
+ // Mouth animation cycles
971
+ mouthOpen = frame % 4; // 0=open, 1=half, 2=wide, 3=closed
972
+ eyeState = 'narrow';
973
+ }
974
+ else if (mood === 'walking') {
975
+ // Walking: arms alternate, body bobs
976
+ const wf = frame % 4;
977
+ if (wf === 0) {
978
+ leftArmFwd = -2;
979
+ rightArmFwd = 2;
980
+ bodyX = -1;
981
+ }
982
+ else if (wf === 1) {
983
+ leftArmFwd = 0;
984
+ rightArmFwd = 0;
985
+ }
986
+ else if (wf === 2) {
987
+ leftArmFwd = 2;
988
+ rightArmFwd = -2;
989
+ bodyX = 1;
990
+ }
991
+ else {
992
+ leftArmFwd = 0;
993
+ rightArmFwd = 0;
994
+ }
995
+ bodyY += (wf % 2 === 0) ? -1 : 0;
996
+ }
997
+ else if (mood === 'excited') {
998
+ const ef = frame % 4;
999
+ if (ef === 0) {
1000
+ bodyY -= 2;
1001
+ }
1002
+ else if (ef === 2) {
1003
+ bodyY -= 3;
1004
+ mouthOpen = 2;
1005
+ eyeState = 'wide';
1006
+ }
1007
+ else {
1008
+ eyeState = 'narrow';
1009
+ }
1010
+ }
1011
+ else if (mood === 'thinking') {
1012
+ const tf = frame % 3;
1013
+ if (tf === 0) {
1014
+ headTilt = 1;
1015
+ }
1016
+ else if (tf === 1) {
1017
+ eyeState = 'up';
1018
+ }
1019
+ else {
1020
+ questionMark = true;
1021
+ }
1022
+ }
1023
+ else if (mood === 'dancing') {
1024
+ const df = frame % 4;
1025
+ if (df === 0) {
1026
+ bodyX = -2;
1027
+ bodyY -= 1;
1028
+ }
1029
+ else if (df === 1) {
1030
+ bodyX = 0;
1031
+ }
1032
+ else if (df === 2) {
1033
+ bodyX = 2;
1034
+ bodyY -= 1;
1035
+ }
1036
+ else {
1037
+ bodyX = 0;
1038
+ capTilt = 1;
1039
+ }
1040
+ eyeState = 'narrow';
1041
+ mouthOpen = (df % 2 === 0) ? 1 : 0;
1042
+ }
1043
+ bodyY += breathShift;
1044
+ const ox = x + bodyX * s;
1045
+ const oy = y + bodyY * s;
1046
+ // ── Drop shadow ──
1047
+ ctx.save();
1048
+ ctx.fillStyle = 'rgba(30, 20, 5, 0.3)';
1049
+ ctx.beginPath();
1050
+ ctx.ellipse(ox + 16 * s, oy + 31 * s, 12 * s, 3 * s, 0, 0, Math.PI * 2);
1051
+ ctx.fill();
1052
+ ctx.restore();
1053
+ // ── Tail (draw first, behind body) ──
1054
+ // Curled tail on the right side, rows 14-20
1055
+ px(ctx, 27, 14, 2, 1, G.furMain, s, ox, oy);
1056
+ px(ctx, 28, 15, 2, 1, G.furMain, s, ox, oy);
1057
+ px(ctx, 29, 16, 2, 1, G.furDark, s, ox, oy);
1058
+ px(ctx, 29, 17, 1, 1, G.furDark, s, ox, oy);
1059
+ px(ctx, 28, 18, 1, 1, G.furMain, s, ox, oy);
1060
+ px(ctx, 27, 19, 2, 1, G.furMain, s, ox, oy);
1061
+ // Curl tip
1062
+ px(ctx, 26, 18, 1, 1, G.furLight, s, ox, oy);
1063
+ // Outline
1064
+ px(ctx, 27, 13, 2, 1, G.outline, s, ox, oy);
1065
+ px(ctx, 29, 14, 1, 1, G.outline, s, ox, oy);
1066
+ px(ctx, 30, 15, 1, 2, G.outline, s, ox, oy);
1067
+ px(ctx, 31, 16, 1, 1, G.outline, s, ox, oy);
1068
+ px(ctx, 30, 17, 1, 1, G.outline, s, ox, oy);
1069
+ px(ctx, 30, 18, 1, 1, G.outline, s, ox, oy);
1070
+ px(ctx, 29, 19, 1, 1, G.outline, s, ox, oy);
1071
+ px(ctx, 27, 20, 2, 1, G.outline, s, ox, oy);
1072
+ px(ctx, 25, 18, 1, 1, G.outline, s, ox, oy);
1073
+ // ── Back legs (behind body) ──
1074
+ // Left back leg (rows 22-28)
1075
+ px(ctx, 17, 24, 5, 5, G.furDark, s, ox, oy);
1076
+ px(ctx, 18, 24, 3, 4, G.furMain, s, ox, oy);
1077
+ px(ctx, 17, 29, 6, 2, G.furDark, s, ox, oy); // foot
1078
+ px(ctx, 18, 29, 4, 1, G.furMain, s, ox, oy);
1079
+ // Claws on back foot
1080
+ px(ctx, 17, 31, 1, 1, G.claws, s, ox, oy);
1081
+ px(ctx, 19, 31, 1, 1, G.claws, s, ox, oy);
1082
+ px(ctx, 21, 31, 1, 1, G.claws, s, ox, oy);
1083
+ // Right back leg
1084
+ px(ctx, 22, 24, 5, 5, G.furDark, s, ox, oy);
1085
+ px(ctx, 23, 24, 3, 4, G.furMain, s, ox, oy);
1086
+ px(ctx, 22, 29, 6, 2, G.furDark, s, ox, oy); // foot
1087
+ px(ctx, 23, 29, 4, 1, G.furMain, s, ox, oy);
1088
+ // Claws
1089
+ px(ctx, 22, 31, 1, 1, G.claws, s, ox, oy);
1090
+ px(ctx, 24, 31, 1, 1, G.claws, s, ox, oy);
1091
+ px(ctx, 26, 31, 1, 1, G.claws, s, ox, oy);
1092
+ // ── Body (rows 12-24) ── Very wide, stocky torso
1093
+ // Outline
1094
+ outlineRect(ctx, 5, 12, 22, 14, G.outline, s, ox, oy);
1095
+ // Body fill — dark brown base
1096
+ px(ctx, 5, 12, 22, 14, G.furDark, s, ox, oy);
1097
+ // Main fur color on upper body
1098
+ px(ctx, 6, 13, 20, 8, G.furMain, s, ox, oy);
1099
+ // Light highlights on top (back ridge)
1100
+ px(ctx, 8, 12, 16, 2, G.furLight, s, ox, oy);
1101
+ // Lighter chest/belly underneath
1102
+ px(ctx, 9, 19, 14, 6, G.furChest, s, ox, oy);
1103
+ // Dithered transition from main fur to chest
1104
+ dither(ctx, 9, 18, 14, 1, G.furMain, G.furChest, s, ox, oy);
1105
+ // Dark underside shadow
1106
+ px(ctx, 6, 25, 20, 1, G.skinDark, s, ox, oy);
1107
+ // ── Front arms (rows 14-28): thick, reaching to ground ──
1108
+ // Left front arm
1109
+ const laOff = leftArmFwd;
1110
+ outlineRect(ctx, 2 + laOff, 14, 6, 14, G.outline, s, ox, oy);
1111
+ px(ctx, 2 + laOff, 14, 6, 14, G.furDark, s, ox, oy);
1112
+ px(ctx, 3 + laOff, 14, 4, 12, G.furMain, s, ox, oy);
1113
+ // Shoulder highlight
1114
+ px(ctx, 3 + laOff, 14, 4, 2, G.furLight, s, ox, oy);
1115
+ // Forearm darker
1116
+ px(ctx, 3 + laOff, 22, 4, 4, G.skinDark, s, ox, oy);
1117
+ // Hand/knuckles
1118
+ px(ctx, 1 + laOff, 28, 7, 3, G.furDark, s, ox, oy);
1119
+ px(ctx, 2 + laOff, 28, 5, 2, G.furMain, s, ox, oy);
1120
+ // Claws
1121
+ px(ctx, 1 + laOff, 31, 1, 1, G.claws, s, ox, oy);
1122
+ px(ctx, 3 + laOff, 31, 1, 1, G.claws, s, ox, oy);
1123
+ px(ctx, 5 + laOff, 31, 1, 1, G.claws, s, ox, oy);
1124
+ px(ctx, 7 + laOff, 31, 1, 1, G.claws, s, ox, oy);
1125
+ // Right front arm
1126
+ const raOff = rightArmFwd;
1127
+ outlineRect(ctx, 24 + raOff, 14, 6, 14, G.outline, s, ox, oy);
1128
+ px(ctx, 24 + raOff, 14, 6, 14, G.furDark, s, ox, oy);
1129
+ px(ctx, 25 + raOff, 14, 4, 12, G.furMain, s, ox, oy);
1130
+ // Shoulder highlight
1131
+ px(ctx, 25 + raOff, 14, 4, 2, G.furLight, s, ox, oy);
1132
+ // Forearm darker
1133
+ px(ctx, 25 + raOff, 22, 4, 4, G.skinDark, s, ox, oy);
1134
+ // Hand/knuckles
1135
+ px(ctx, 24 + raOff, 28, 7, 3, G.furDark, s, ox, oy);
1136
+ px(ctx, 25 + raOff, 28, 5, 2, G.furMain, s, ox, oy);
1137
+ // Claws
1138
+ px(ctx, 24 + raOff, 31, 1, 1, G.claws, s, ox, oy);
1139
+ px(ctx, 26 + raOff, 31, 1, 1, G.claws, s, ox, oy);
1140
+ px(ctx, 28 + raOff, 31, 1, 1, G.claws, s, ox, oy);
1141
+ px(ctx, 30 + raOff, 31, 1, 1, G.claws, s, ox, oy);
1142
+ // ── Head (rows 3-12): big round head ──
1143
+ const hx = 7 + headTilt;
1144
+ const hy = 3;
1145
+ // Outline
1146
+ outlineRect(ctx, hx, hy, 18, 10, G.outline, s, ox, oy);
1147
+ // Head fill — dark base
1148
+ px(ctx, hx, hy, 18, 10, G.furDark, s, ox, oy);
1149
+ // Main fur
1150
+ px(ctx, hx + 1, hy + 1, 16, 8, G.furMain, s, ox, oy);
1151
+ // Brow ridge highlight
1152
+ px(ctx, hx + 2, hy + 1, 14, 2, G.furLight, s, ox, oy);
1153
+ // Lighter face area (center)
1154
+ px(ctx, hx + 4, hy + 4, 10, 5, G.furChest, s, ox, oy);
1155
+ // Darker brow ridge above eyes (makes them look grumpy/narrowed)
1156
+ px(ctx, hx + 3, hy + 3, 12, 2, G.skinDark, s, ox, oy);
1157
+ // ── Eyes (rows 7-9 relative, hy+4 to hy+6 in head) ──
1158
+ const eyeY = hy + 5;
1159
+ if (eyeState === 'blink') {
1160
+ // Closed eyes — thin line
1161
+ px(ctx, hx + 5, eyeY, 3, 1, G.outline, s, ox, oy);
1162
+ px(ctx, hx + 11, eyeY, 3, 1, G.outline, s, ox, oy);
1163
+ }
1164
+ else if (eyeState === 'narrow') {
1165
+ // Narrowed/grumpy eyes — 3x2, squinted
1166
+ // Eye whites (narrow slit)
1167
+ px(ctx, hx + 5, eyeY, 3, 2, G.eyeWhite, s, ox, oy);
1168
+ px(ctx, hx + 11, eyeY, 3, 2, G.eyeWhite, s, ox, oy);
1169
+ // Pupils
1170
+ px(ctx, hx + 6, eyeY, 2, 2, G.eyePupil, s, ox, oy);
1171
+ px(ctx, hx + 12, eyeY, 2, 2, G.eyePupil, s, ox, oy);
1172
+ // Heavy brow line pushing down (grumpy)
1173
+ px(ctx, hx + 4, eyeY - 1, 5, 1, G.skinDark, s, ox, oy);
1174
+ px(ctx, hx + 10, eyeY - 1, 5, 1, G.skinDark, s, ox, oy);
1175
+ }
1176
+ else if (eyeState === 'up') {
1177
+ // Looking up
1178
+ px(ctx, hx + 5, eyeY - 1, 3, 2, G.eyeWhite, s, ox, oy);
1179
+ px(ctx, hx + 11, eyeY - 1, 3, 2, G.eyeWhite, s, ox, oy);
1180
+ px(ctx, hx + 6, eyeY - 1, 1, 1, G.eyePupil, s, ox, oy);
1181
+ px(ctx, hx + 12, eyeY - 1, 1, 1, G.eyePupil, s, ox, oy);
1182
+ }
1183
+ else if (eyeState === 'wide') {
1184
+ // Wide/surprised
1185
+ px(ctx, hx + 5, eyeY - 1, 3, 3, G.eyeWhite, s, ox, oy);
1186
+ px(ctx, hx + 11, eyeY - 1, 3, 3, G.eyeWhite, s, ox, oy);
1187
+ px(ctx, hx + 6, eyeY, 1, 1, G.eyePupil, s, ox, oy);
1188
+ px(ctx, hx + 12, eyeY, 1, 1, G.eyePupil, s, ox, oy);
1189
+ }
1190
+ else {
1191
+ // Normal eyes
1192
+ px(ctx, hx + 5, eyeY, 3, 2, G.eyeWhite, s, ox, oy);
1193
+ px(ctx, hx + 11, eyeY, 3, 2, G.eyeWhite, s, ox, oy);
1194
+ px(ctx, hx + 6, eyeY, 2, 2, G.eyePupil, s, ox, oy);
1195
+ px(ctx, hx + 12, eyeY, 2, 2, G.eyePupil, s, ox, oy);
1196
+ }
1197
+ // ── Nose ──
1198
+ px(ctx, hx + 8, hy + 7, 3, 2, G.nose, s, ox, oy);
1199
+ // Nostrils
1200
+ px(ctx, hx + 8, hy + 8, 1, 1, G.outline, s, ox, oy);
1201
+ px(ctx, hx + 10, hy + 8, 1, 1, G.outline, s, ox, oy);
1202
+ // ── Mouth (rows 10-11 of head) ──
1203
+ const mouthY = hy + 9;
1204
+ const mouthX = hx + 6;
1205
+ if (mouthOpen === 0) {
1206
+ // Closed grumpy mouth — wide line with downturned ends
1207
+ px(ctx, mouthX, mouthY, 7, 1, G.mouth, s, ox, oy);
1208
+ // Fangs poking down
1209
+ px(ctx, mouthX + 1, mouthY + 1, 1, 1, G.fang, s, ox, oy);
1210
+ px(ctx, mouthX + 5, mouthY + 1, 1, 1, G.fang, s, ox, oy);
1211
+ }
1212
+ else if (mouthOpen === 1) {
1213
+ // Half open
1214
+ px(ctx, mouthX, mouthY, 7, 1, G.mouth, s, ox, oy);
1215
+ px(ctx, mouthX + 1, mouthY + 1, 5, 1, G.mouth, s, ox, oy);
1216
+ // Fangs
1217
+ px(ctx, mouthX, mouthY + 1, 1, 1, G.fang, s, ox, oy);
1218
+ px(ctx, mouthX + 6, mouthY + 1, 1, 1, G.fang, s, ox, oy);
1219
+ }
1220
+ else if (mouthOpen === 2) {
1221
+ // Wide open — show red inside
1222
+ px(ctx, mouthX - 1, mouthY, 9, 2, G.mouth, s, ox, oy);
1223
+ px(ctx, mouthX, mouthY, 7, 2, '#8B2020', s, ox, oy); // red inner
1224
+ // Big fangs
1225
+ px(ctx, mouthX - 1, mouthY, 1, 2, G.fang, s, ox, oy);
1226
+ px(ctx, mouthX + 7, mouthY, 1, 2, G.fang, s, ox, oy);
1227
+ }
1228
+ else {
1229
+ // Closed tight line
1230
+ px(ctx, mouthX + 1, mouthY, 5, 1, G.mouth, s, ox, oy);
1231
+ }
1232
+ // ── Baseball cap (rows 0-5): red with white front panel ──
1233
+ const capX = hx - 1 + capTilt;
1234
+ const capY = hy - 3;
1235
+ // Cap crown — red
1236
+ px(ctx, capX + 2, capY, 16, 2, G.capRed, s, ox, oy);
1237
+ px(ctx, capX + 1, capY + 2, 18, 2, G.capRed, s, ox, oy);
1238
+ // White front panel (left portion — cap is backwards/sideways)
1239
+ px(ctx, capX + 2, capY, 5, 2, G.capWhite, s, ox, oy);
1240
+ px(ctx, capX + 1, capY + 2, 6, 2, G.capWhite, s, ox, oy);
1241
+ // Brim extending to the right (cap worn sideways)
1242
+ px(ctx, capX + 18, capY + 3, 4, 2, G.capRed, s, ox, oy);
1243
+ px(ctx, capX + 18, capY + 4, 4, 1, G.capBrim, s, ox, oy); // brim underside
1244
+ // Cap outline
1245
+ px(ctx, capX + 2, capY - 1, 16, 1, G.outline, s, ox, oy); // top
1246
+ px(ctx, capX, capY + 2, 1, 2, G.outline, s, ox, oy); // left side
1247
+ px(ctx, capX + 1, capY + 4, 18, 1, G.outline, s, ox, oy); // bottom band
1248
+ px(ctx, capX + 22, capY + 3, 1, 2, G.outline, s, ox, oy); // brim end
1249
+ // Cap button on top
1250
+ px(ctx, capX + 9, capY - 1, 2, 1, G.capRed, s, ox, oy);
1251
+ // ── Question mark particle (thinking frame 2) ──
1252
+ if (questionMark) {
1253
+ const qColor = getMoodColor('thinking', frame, moodColor);
1254
+ px(ctx, hx + 6, hy - 6, 3, 1, qColor, s, ox, oy);
1255
+ px(ctx, hx + 8, hy - 5, 1, 1, qColor, s, ox, oy);
1256
+ px(ctx, hx + 7, hy - 4, 1, 1, qColor, s, ox, oy);
1257
+ px(ctx, hx + 7, hy - 2, 1, 1, qColor, s, ox, oy);
1258
+ }
1259
+ }
1260
+ /**
1261
+ * Draw animated mood particles around the gorilla.
1262
+ * Same interface as drawMoodParticles but tuned for gorilla position/shape.
1263
+ */
1264
+ export function drawGorillaParticles(ctx, x, y, scale, mood, frame) {
1265
+ const s = scale;
1266
+ const color = getMoodColor(mood, frame);
1267
+ if (mood === 'dancing') {
1268
+ // Music notes floating up
1269
+ const notes = [
1270
+ { baseX: -2, baseY: 2, phase: 0 },
1271
+ { baseX: 30, baseY: 0, phase: 2 },
1272
+ { baseX: 14, baseY: -2, phase: 4 },
1273
+ ];
1274
+ for (const note of notes) {
1275
+ const floatY = ((frame + note.phase) % 8) * -2;
1276
+ const ny = note.baseY + floatY;
1277
+ if (ny > -8) {
1278
+ const c = RAINBOW[(frame + note.phase) % RAINBOW.length];
1279
+ px(ctx, note.baseX, ny + 3, 2, 2, c, s, x, y);
1280
+ px(ctx, note.baseX + 1, ny, 1, 3, c, s, x, y);
1281
+ px(ctx, note.baseX + 1, ny, 2, 1, c, s, x, y);
1282
+ }
1283
+ }
1284
+ }
1285
+ else if (mood === 'excited') {
1286
+ // Sparkle + shapes
1287
+ const positions = [
1288
+ { x: -2, y: 4 }, { x: 30, y: 2 },
1289
+ { x: 2, y: -4 }, { x: 28, y: -2 },
1290
+ ];
1291
+ for (let i = 0; i < positions.length; i++) {
1292
+ const visible = ((frame + i * 2) % 4) < 2;
1293
+ if (!visible)
1294
+ continue;
1295
+ const p = positions[i];
1296
+ px(ctx, p.x + 1, p.y, 1, 3, color, s, x, y);
1297
+ px(ctx, p.x, p.y + 1, 3, 1, color, s, x, y);
1298
+ }
1299
+ }
1300
+ else if (mood === 'thinking') {
1301
+ // Thought bubbles
1302
+ const f = frame % 3;
1303
+ if (f === 0) {
1304
+ px(ctx, 20, -6, 3, 1, color, s, x, y);
1305
+ px(ctx, 22, -5, 1, 1, color, s, x, y);
1306
+ px(ctx, 21, -4, 1, 1, color, s, x, y);
1307
+ px(ctx, 21, -2, 1, 1, color, s, x, y);
1308
+ }
1309
+ else if (f === 1) {
1310
+ px(ctx, 19, -4, 1, 1, color, s, x, y);
1311
+ px(ctx, 21, -5, 1, 1, color, s, x, y);
1312
+ px(ctx, 23, -4, 1, 1, color, s, x, y);
1313
+ }
1314
+ }
1315
+ else if (mood === 'talking') {
1316
+ // Sound waves from mouth area
1317
+ const baseX = 26;
1318
+ const baseY = 14;
1319
+ for (let i = 0; i < 3; i++) {
1320
+ const visible = ((frame + i) % 4) < 3;
1321
+ if (!visible)
1322
+ continue;
1323
+ const dist = i * 2 + ((frame % 2) * 1);
1324
+ const alpha = 1 - i * 0.3;
1325
+ const c = dimColor(color.startsWith('rgb') ? '#58a6ff' : color, alpha);
1326
+ px(ctx, baseX + dist, baseY - 1, 1, 1, c, s, x, y);
1327
+ px(ctx, baseX + dist + 1, baseY, 1, 1, c, s, x, y);
1328
+ px(ctx, baseX + dist, baseY + 1, 1, 1, c, s, x, y);
1329
+ }
1330
+ }
1331
+ }
898
1332
  // ─── Mood Particles ────────────────────────────────────────────
899
1333
  /**
900
1334
  * Draw animated mood particles around the robot.
@@ -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
  }