@kernel.chat/kbot 4.1.0 → 4.3.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.
@@ -14,12 +14,31 @@ const ALERT_ENTER = SEC;
14
14
  const ALERT_EXIT = SEC;
15
15
  const ALERT_TOTAL = ALERT_ENTER + ALERT_HOLD + ALERT_EXIT;
16
16
  // ─── Palette ───────────────────────────────────────────────────
17
+ // kernel.chat magazine grammar — POPEYE-anchored editorial.
18
+ // Single spot color (tomato). Warm paper ground. Ink + coffee for type.
19
+ // Differentiation between alert types lives in the bilingual kicker copy,
20
+ // NOT in colour — the press only mixes one spot.
17
21
  const C = {
18
- bg: '#0d1117', bgPanel: '#161b22', accent: '#6B5B95',
19
- green: '#3fb950', blue: '#58a6ff', orange: '#d29922',
20
- red: '#f85149', purple: '#bc8cff', text: '#e6edf3',
21
- textDim: '#8b949e', gold: '#ffd700', white: '#ffffff',
22
+ cream: '#F3E9D2', // --pop-cream ground
23
+ ivory: '#FAF9F6', // --pop-ivory soft inner panel
24
+ ink: '#1F1E1D', // --pop-ink — primary text
25
+ coffee: '#6B4E3D', // --pop-coffee secondary text / dim
26
+ tomato: '#E24E1B', // --pop-tomato — the spot
27
+ hairlineSoft: 'rgba(31,30,29,0.16)', // --pop-hairline-soft
28
+ // legacy aliases kept so any caller passing a custom hex still composites;
29
+ // the system itself never reaches for these.
30
+ white: '#FAF9F6',
22
31
  };
32
+ // fonts — JP fallback chain so bilingual kickers render in canvas
33
+ const FONT_SERIF = '"EB Garamond", "Hiragino Mincho ProN", "Yu Mincho", serif';
34
+ const FONT_MONO = '"Courier Prime", "Hiragino Mincho ProN", "Yu Mincho", monospace';
35
+ // system glyph — leads every folio surface. Tomato spot.
36
+ const STAR = '★';
37
+ // Live broadcast tagline — the bottom-right anchor on the folio strip.
38
+ // Live transmissions don't carry issue numbers (that bookkeeping is the
39
+ // magazine surface's, not the broadcast's), so the right edge is the
40
+ // publication tagline rather than a dateline monument.
41
+ const LIVE_TAGLINE = 'LIVE TRANSMISSION · 生放送';
23
42
  // ─── Helpers ───────────────────────────────────────────────────
24
43
  function hexToRgba(hex, alpha) {
25
44
  const r = parseInt(hex.slice(1, 3), 16);
@@ -47,29 +66,26 @@ function spawnN(n, cfg) {
47
66
  }
48
67
  return out;
49
68
  }
69
+ // Particles read as ink-spatter / newsprint specks, not confetti.
70
+ // Tomato spot only. Counts halved — the magazine voice is restraint.
71
+ // `follow` stays silent; type alone is the celebration.
50
72
  function spawnAlertParticles(type, cx, cy) {
51
73
  if (type === 'raid') {
52
- return spawnN(30, () => {
53
- const a = Math.random() * Math.PI * 2, sp = rng(2, 8);
54
- return { x: cx, y: cy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, minLife: SEC, maxLife: 3 * SEC, color: pick([C.red, C.orange, C.gold, C.white]), size: rng(2, 5) };
74
+ return spawnN(14, () => {
75
+ const a = Math.random() * Math.PI * 2, sp = rng(2, 7);
76
+ return { x: cx, y: cy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, minLife: SEC, maxLife: 2 * SEC, color: C.tomato, size: rng(2, 4) };
55
77
  });
56
78
  }
57
- if (type === 'sub') {
58
- return spawnN(25, () => ({
59
- x: rng(cx - 200, cx + 200), y: cy - 40, vx: rng(-1, 1), vy: rng(1, 4),
60
- minLife: 2 * SEC, maxLife: 4 * SEC, color: pick([C.accent, C.green, C.blue, C.purple, C.gold]), size: rng(3, 6),
61
- }));
62
- }
63
- if (type === 'donation') {
64
- return spawnN(20, () => ({
65
- x: rng(cx - 150, cx + 150), y: cy - 60, vx: rng(-0.5, 0.5), vy: rng(1, 3),
66
- minLife: 2 * SEC, maxLife: 3 * SEC, color: pick([C.gold, C.orange, '#ffed4a']), size: rng(3, 5),
79
+ if (type === 'sub' || type === 'donation') {
80
+ return spawnN(12, () => ({
81
+ x: rng(cx - 160, cx + 160), y: cy - 40, vx: rng(-0.6, 0.6), vy: rng(1, 3),
82
+ minLife: 2 * SEC, maxLife: 3 * SEC, color: C.tomato, size: rng(2, 4),
67
83
  }));
68
84
  }
69
85
  if (type === 'achievement') {
70
- return spawnN(16, (i) => {
71
- const a = (i / 16) * Math.PI * 2;
72
- return { x: cx + Math.cos(a) * 60, y: cy + Math.sin(a) * 25, vx: Math.cos(a) * 0.5, vy: Math.sin(a) * 0.5 - 0.3, minLife: 2 * SEC, maxLife: 3 * SEC, color: C.gold, size: rng(2, 4) };
86
+ return spawnN(10, (i) => {
87
+ const a = (i / 10) * Math.PI * 2;
88
+ return { x: cx + Math.cos(a) * 60, y: cy + Math.sin(a) * 25, vx: Math.cos(a) * 0.4, vy: Math.sin(a) * 0.4 - 0.3, minLife: 2 * SEC, maxLife: 3 * SEC, color: C.tomato, size: rng(2, 3) };
73
89
  });
74
90
  }
75
91
  return [];
@@ -84,12 +100,14 @@ function drawParticles(ctx, particles) {
84
100
  }
85
101
  }
86
102
  // ─── Alert Label Maps ──────────────────────────────────────────
87
- const ALERT_TITLES = {
88
- follow: 'NEW FOLLOWER', raid: 'RAID INCOMING', sub: 'NEW SUBSCRIBER',
89
- donation: 'DONATION', achievement: 'ACHIEVEMENT UNLOCKED',
90
- };
91
- const ALERT_COLORS = {
92
- follow: C.green, raid: C.red, sub: C.accent, donation: C.gold, achievement: C.gold,
103
+ // Bracketed bilingual kicker — same grammar as `.pop-kicker` on the site.
104
+ // `[CATEGORY · 日本語]` Latin small-caps + Japanese mono.
105
+ const ALERT_KICKERS = {
106
+ follow: '[FOLLOWER · 新規読者]',
107
+ raid: '[RAID · 来訪]',
108
+ sub: '[SUBSCRIBER · 定期購読]',
109
+ donation: '[DONATION · 寄付]',
110
+ achievement: '[ACHIEVEMENT · 達成]',
93
111
  };
94
112
  function alertBody(a) {
95
113
  switch (a.type) {
@@ -132,7 +150,7 @@ export class StreamOverlay {
132
150
  }
133
151
  addTicker(text) { this.tickerItems.push({ text, x: -1 }); }
134
152
  highlightMessage(username, message, color) {
135
- this.highlight = { username, message, color: color ?? C.accent, frame: 0, totalFrames: 3 * SEC };
153
+ this.highlight = { username, message, color: color ?? C.tomato, frame: 0, totalFrames: 3 * SEC };
136
154
  }
137
155
  updateInfoBar(info) { this.infoBar = { ...info }; }
138
156
  tick(_frame) {
@@ -168,7 +186,6 @@ export class StreamOverlay {
168
186
  if (!this.activeAlert)
169
187
  return;
170
188
  const a = this.activeAlert, frame = a.frame;
171
- const color = ALERT_COLORS[a.alert.type] ?? C.blue;
172
189
  let alpha = 1, offsetY = 0;
173
190
  if (frame <= ALERT_ENTER) {
174
191
  const t = easeOut(frame / ALERT_ENTER);
@@ -180,32 +197,37 @@ export class StreamOverlay {
180
197
  alpha = 1 - t;
181
198
  offsetY = lerp(0, -30, t);
182
199
  }
183
- const boxW = 420, boxH = 80;
184
- const boxX = Math.floor((width - boxW) / 2), boxY = 100 + offsetY;
200
+ // Editorial alert card: cream stock, ink hairline frame,
201
+ // tomato kicker rule under the bracket label, EB Garamond headline.
202
+ const boxW = 460, boxH = 96;
203
+ const boxX = Math.floor((width - boxW) / 2), boxY = 80 + offsetY;
185
204
  ctx.save();
186
205
  ctx.globalAlpha = alpha;
187
- ctx.fillStyle = hexToRgba(C.bgPanel, 0.92);
206
+ // Cream paper ground
207
+ ctx.fillStyle = C.cream;
188
208
  ctx.fillRect(boxX, boxY, boxW, boxH);
189
- ctx.strokeStyle = color;
190
- ctx.lineWidth = 2;
191
- ctx.strokeRect(boxX, boxY, boxW, boxH);
192
- ctx.fillStyle = color;
193
- ctx.fillRect(boxX, boxY, 4, boxH);
194
- if (a.alert.type === 'achievement' || a.alert.type === 'donation') {
195
- ctx.fillStyle = hexToRgba(C.gold, 0.15 + 0.1 * Math.sin(frame * 0.5));
196
- ctx.fillRect(boxX - 3, boxY - 3, boxW + 6, boxH + 6);
197
- }
198
- ctx.fillStyle = color;
199
- ctx.font = 'bold 14px "Courier Prime", monospace';
209
+ // Ink hairline frame
210
+ ctx.strokeStyle = C.ink;
211
+ ctx.lineWidth = 1;
212
+ ctx.strokeRect(boxX + 0.5, boxY + 0.5, boxW - 1, boxH - 1);
213
+ // Bracketed bilingual kicker — Courier Prime, ink
214
+ ctx.fillStyle = C.ink;
215
+ ctx.font = `11px ${FONT_MONO}`;
200
216
  ctx.textAlign = 'center';
201
- ctx.fillText(ALERT_TITLES[a.alert.type] ?? 'ALERT', boxX + boxW / 2, boxY + 24);
202
- ctx.fillStyle = C.text;
203
- ctx.font = '16px "Courier Prime", monospace';
204
- ctx.fillText(truncate(alertBody(a.alert), 40), boxX + boxW / 2, boxY + 50);
217
+ ctx.fillText(ALERT_KICKERS[a.alert.type] ?? '[ALERT]', boxX + boxW / 2, boxY + 22);
218
+ // Tomato spot rule under the kicker (the .pop-rule--short equivalent)
219
+ const ruleW = 56;
220
+ ctx.fillStyle = C.tomato;
221
+ ctx.fillRect(boxX + (boxW - ruleW) / 2, boxY + 28, ruleW, 2);
222
+ // Headline body — EB Garamond, italic, ink with username in tomato
223
+ ctx.fillStyle = C.ink;
224
+ ctx.font = `italic 22px ${FONT_SERIF}`;
225
+ ctx.fillText(truncate(alertBody(a.alert), 42), boxX + boxW / 2, boxY + 60);
226
+ // Optional sub-message — Courier, coffee
205
227
  if (a.alert.message && a.alert.type !== 'achievement') {
206
- ctx.fillStyle = C.textDim;
207
- ctx.font = '12px "Courier Prime", monospace';
208
- ctx.fillText(truncate(a.alert.message, 50), boxX + boxW / 2, boxY + 68);
228
+ ctx.fillStyle = C.coffee;
229
+ ctx.font = `11px ${FONT_MONO}`;
230
+ ctx.fillText(truncate(a.alert.message, 56), boxX + boxW / 2, boxY + 82);
209
231
  }
210
232
  ctx.textAlign = 'left';
211
233
  ctx.restore();
@@ -221,39 +243,41 @@ export class StreamOverlay {
221
243
  renderGoals(ctx, width, height) {
222
244
  if (this.goals.size === 0)
223
245
  return;
224
- const barH = 22, barMargin = 8, barX = 20, barW = width - 40;
246
+ const barH = 22, barMargin = 8, barX = 24, barW = width - 48;
225
247
  let idx = 0;
226
248
  for (const [id, goal] of this.goals) {
227
249
  const isTop = (goal.position ?? 'top') === 'top';
228
- const barY = isTop ? 10 + idx * (barH + barMargin) : height - 60 - idx * (barH + barMargin);
250
+ const barY = isTop ? 14 + idx * (barH + barMargin) : height - 64 - idx * (barH + barMargin);
229
251
  const fill = this.goalAnimations.get(id) ?? 0;
230
252
  ctx.save();
231
- ctx.fillStyle = hexToRgba(C.bgPanel, 0.85);
253
+ // Ivory inner panel on cream — quieter than full ground swap
254
+ ctx.fillStyle = C.ivory;
232
255
  ctx.fillRect(barX, barY, barW, barH);
233
- ctx.strokeStyle = hexToRgba(C.accent, 0.5);
256
+ // Ink hairline frame
257
+ ctx.strokeStyle = C.ink;
234
258
  ctx.lineWidth = 1;
235
- ctx.strokeRect(barX, barY, barW, barH);
259
+ ctx.strokeRect(barX + 0.5, barY + 0.5, barW - 1, barH - 1);
260
+ // Tomato fill — single spot
236
261
  const fillW = Math.floor(barW * fill);
237
262
  if (fillW > 0) {
238
- ctx.fillStyle = hexToRgba(goal.color ?? C.green, 0.8);
263
+ ctx.fillStyle = C.tomato;
239
264
  ctx.fillRect(barX, barY, fillW, barH);
240
- if (fill < 1) {
241
- ctx.fillStyle = hexToRgba(C.white, 0.3);
242
- ctx.fillRect(barX + fillW - 3, barY, 3, barH);
243
- }
244
265
  }
266
+ // Quiet completion glow — tomato breath, not gold
245
267
  if (fill >= 0.999) {
246
- ctx.fillStyle = hexToRgba(C.gold, 0.15 + 0.05 * Math.sin(Date.now() * 0.005));
247
- ctx.fillRect(barX, barY, barW, barH);
268
+ ctx.fillStyle = hexToRgba(C.tomato, 0.08 + 0.04 * Math.sin(Date.now() * 0.003));
269
+ ctx.fillRect(barX - 2, barY - 2, barW + 4, barH + 4);
248
270
  }
249
- ctx.fillStyle = C.text;
250
- ctx.font = '12px "Courier Prime", monospace';
271
+ // Label — Courier Prime, ink (or ivory if it's sitting on tomato fill)
272
+ ctx.font = `11px ${FONT_MONO}`;
251
273
  ctx.textAlign = 'left';
252
- ctx.fillText(`${goal.label}: ${goal.current}/${goal.target}`, barX + 6, barY + 15);
274
+ ctx.fillStyle = fillW > 80 ? C.ivory : C.ink;
275
+ ctx.fillText(`${goal.label.toUpperCase()} · ${goal.current}/${goal.target}`, barX + 8, barY + 15);
276
+ // Percentage — right side
253
277
  const pct = Math.min(100, Math.round((goal.current / goal.target) * 100));
254
278
  ctx.textAlign = 'right';
255
- ctx.fillStyle = fill >= 0.999 ? C.gold : C.textDim;
256
- ctx.fillText(`${pct}%`, barX + barW - 6, barY + 15);
279
+ ctx.fillStyle = fillW > barW - 40 ? C.ivory : C.tomato;
280
+ ctx.fillText(`${pct}%`, barX + barW - 8, barY + 15);
257
281
  ctx.textAlign = 'left';
258
282
  ctx.restore();
259
283
  idx++;
@@ -281,28 +305,28 @@ export class StreamOverlay {
281
305
  renderTicker(ctx, width, height) {
282
306
  if (this.tickerItems.length === 0)
283
307
  return;
284
- const tickerH = 24, tickerY = height - 72;
308
+ const tickerH = 26, tickerY = height - 68;
285
309
  ctx.save();
286
- ctx.fillStyle = hexToRgba(C.bg, 0.85);
310
+ // Cream ground for the ticker strip
311
+ ctx.fillStyle = C.cream;
287
312
  ctx.fillRect(0, tickerY, width, tickerH);
288
- ctx.strokeStyle = hexToRgba(C.accent, 0.4);
289
- ctx.lineWidth = 1;
290
- ctx.beginPath();
291
- ctx.moveTo(0, tickerY);
292
- ctx.lineTo(width, tickerY);
293
- ctx.moveTo(0, tickerY + tickerH);
294
- ctx.lineTo(width, tickerY + tickerH);
295
- ctx.stroke();
313
+ // Ink hairline above, tomato hairline below (the .pop-rule pair)
314
+ ctx.fillStyle = C.ink;
315
+ ctx.fillRect(0, tickerY, width, 1);
316
+ ctx.fillStyle = C.tomato;
317
+ ctx.fillRect(0, tickerY + tickerH - 1, width, 1);
296
318
  ctx.beginPath();
297
319
  ctx.rect(0, tickerY, width, tickerH);
298
320
  ctx.clip();
299
- ctx.font = '12px "Courier Prime", monospace';
321
+ ctx.font = `12px ${FONT_MONO}`;
300
322
  ctx.textAlign = 'left';
301
323
  for (const item of this.tickerItems) {
302
- ctx.fillStyle = C.accent;
303
- ctx.fillRect(Math.round(item.x - 12), tickerY + 10, 4, 4);
304
- ctx.fillStyle = C.text;
305
- ctx.fillText(item.text, Math.round(item.x), tickerY + 16);
324
+ // Tomato spot bullet — magazine catalog dot
325
+ ctx.fillStyle = C.tomato;
326
+ ctx.fillRect(Math.round(item.x - 14), tickerY + 11, 4, 4);
327
+ // Item text ink
328
+ ctx.fillStyle = C.ink;
329
+ ctx.fillText(item.text, Math.round(item.x), tickerY + 18);
306
330
  }
307
331
  ctx.restore();
308
332
  }
@@ -324,74 +348,83 @@ export class StreamOverlay {
324
348
  alpha = 1 - easeIn((progress - 0.8) / 0.2);
325
349
  else
326
350
  alpha = 1;
327
- const boxW = 500, boxH = 70;
351
+ // Highlighted message reads as a pull-quote: tomato-rule top + bottom,
352
+ // ivory ground, EB Garamond italic body, Courier name.
353
+ const boxW = 540, boxH = 96;
328
354
  const boxX = Math.floor((width - boxW) / 2), boxY = Math.floor(height / 2 - boxH / 2);
329
355
  ctx.save();
330
356
  ctx.globalAlpha = alpha;
331
- ctx.fillStyle = hexToRgba(C.bgPanel, 0.95);
357
+ // Ivory inner panel
358
+ ctx.fillStyle = C.ivory;
332
359
  ctx.fillRect(boxX, boxY, boxW, boxH);
333
- ctx.strokeStyle = hexToRgba(h.color, 0.9);
334
- ctx.lineWidth = 2;
335
- ctx.strokeRect(boxX, boxY, boxW, boxH);
336
- ctx.strokeStyle = hexToRgba(h.color, 0.3);
360
+ // Tomato hairlines top + bottom (pull-quote rules)
361
+ ctx.fillStyle = C.tomato;
362
+ ctx.fillRect(boxX, boxY, boxW, 2);
363
+ ctx.fillRect(boxX, boxY + boxH - 2, boxW, 2);
364
+ // Corner ink ticks — quiet, 8px (replaces the heavy double-frame)
365
+ ctx.strokeStyle = C.ink;
337
366
  ctx.lineWidth = 1;
338
- ctx.strokeRect(boxX - 3, boxY - 3, boxW + 6, boxH + 6);
339
- // Corner accents (4 corners via helper)
340
- ctx.strokeStyle = h.color;
341
- ctx.lineWidth = 2;
342
- drawCorner(ctx, boxX, boxY, 1, 1, 12);
343
- drawCorner(ctx, boxX + boxW, boxY, -1, 1, 12);
344
- drawCorner(ctx, boxX, boxY + boxH, 1, -1, 12);
345
- drawCorner(ctx, boxX + boxW, boxY + boxH, -1, -1, 12);
346
- ctx.fillStyle = h.color;
347
- ctx.font = 'bold 14px "Courier Prime", monospace';
367
+ drawCorner(ctx, boxX + 0.5, boxY + 0.5, 1, 1, 8);
368
+ drawCorner(ctx, boxX + boxW - 0.5, boxY + 0.5, -1, 1, 8);
369
+ drawCorner(ctx, boxX + 0.5, boxY + boxH - 0.5, 1, -1, 8);
370
+ drawCorner(ctx, boxX + boxW - 0.5, boxY + boxH - 0.5, -1, -1, 8);
371
+ // Username Courier Prime, tomato (the cited speaker)
372
+ ctx.fillStyle = C.tomato;
373
+ ctx.font = `11px ${FONT_MONO}`;
348
374
  ctx.textAlign = 'center';
349
- ctx.fillText(h.username, boxX + boxW / 2, boxY + 24);
350
- ctx.fillStyle = C.text;
351
- ctx.font = '16px "Courier Prime", monospace';
352
- ctx.fillText(truncate(h.message, 50), boxX + boxW / 2, boxY + 48);
375
+ ctx.fillText(`— ${h.username.toUpperCase()}`, boxX + boxW / 2, boxY + 24);
376
+ // Message — EB Garamond italic, ink (the quote itself)
377
+ ctx.fillStyle = C.ink;
378
+ ctx.font = `italic 22px ${FONT_SERIF}`;
379
+ ctx.fillText(truncate(h.message, 56), boxX + boxW / 2, boxY + 64);
353
380
  ctx.textAlign = 'left';
354
381
  ctx.restore();
355
382
  }
356
383
  // ── Info Bar ───────────────────────────────────────────────
384
+ /**
385
+ * The folio strip — magazine masthead translated into broadcast chrome.
386
+ * Layout (left → right):
387
+ * ★ KERNEL.CHAT · LIVE · VIEWERS {n} · UPTIME {t} · CHAT {n}/MIN · BIOME {b} LIVE TRANSMISSION · 生放送
388
+ *
389
+ * Single hairline above. Cream ground. Ink type. Tomato spot on the
390
+ * leading ★ and the live tagline. Mirrors `.pop-folio` on the site,
391
+ * minus the issue-number monument — broadcasts don't carry issues.
392
+ */
357
393
  renderInfoBar(ctx, width, height) {
358
- const barH = 28, barY = height - barH;
394
+ const barH = 30, barY = height - barH;
359
395
  ctx.save();
360
- ctx.fillStyle = hexToRgba(C.bg, 0.92);
396
+ // Cream ground
397
+ ctx.fillStyle = C.cream;
361
398
  ctx.fillRect(0, barY, width, barH);
362
- ctx.strokeStyle = hexToRgba(C.accent, 0.5);
363
- ctx.lineWidth = 1;
364
- ctx.beginPath();
365
- ctx.moveTo(0, barY);
366
- ctx.lineTo(width, barY);
367
- ctx.stroke();
368
- const info = this.infoBar;
369
- const items = [
370
- { label: 'VIEWERS', value: String(info.viewers), color: C.green },
371
- { label: 'UPTIME', value: info.uptime, color: C.blue },
372
- { label: 'BIOME', value: info.biome, color: C.accent },
373
- { label: 'CHAT', value: `${info.chatRate}/min`, color: C.orange },
374
- ];
375
- const sectionW = Math.floor(width / items.length);
376
- ctx.font = '11px "Courier Prime", monospace';
399
+ // Ink hairline above (the .pop-rule)
400
+ ctx.fillStyle = C.ink;
401
+ ctx.fillRect(0, barY, width, 1);
402
+ // Leading ★ glyph — tomato spot, the system folio mark
403
+ ctx.fillStyle = C.tomato;
404
+ ctx.font = `13px ${FONT_MONO}`;
377
405
  ctx.textAlign = 'left';
378
- for (let i = 0; i < items.length; i++) {
379
- const x = i * sectionW + 12, it = items[i];
380
- ctx.fillStyle = C.textDim;
381
- ctx.fillText(it.label, x, barY + 12);
382
- ctx.fillStyle = it.color;
383
- ctx.font = 'bold 12px "Courier Prime", monospace';
384
- ctx.fillText(it.value, x + it.label.length * 7 + 8, barY + 12);
385
- ctx.font = '11px "Courier Prime", monospace';
386
- if (i < items.length - 1) {
387
- ctx.fillStyle = hexToRgba(C.accent, 0.3);
388
- ctx.fillRect(i * sectionW + sectionW - 1, barY + 4, 1, barH - 8);
389
- }
390
- }
406
+ ctx.fillText(STAR, 12, barY + 20);
407
+ // Wordmark Courier Prime, ink, all-caps
408
+ ctx.fillStyle = C.ink;
409
+ ctx.font = `11px ${FONT_MONO}`;
410
+ ctx.fillText('KERNEL.CHAT · LIVE', 28, barY + 20);
411
+ // Meta items separated by · in Courier
412
+ const info = this.infoBar;
413
+ const meta = [
414
+ `VIEWERS ${info.viewers}`,
415
+ `UPTIME ${info.uptime}`,
416
+ `CHAT ${info.chatRate}/MIN`,
417
+ `BIOME ${info.biome.toUpperCase()}`,
418
+ ].join(' · ');
419
+ ctx.fillStyle = C.coffee;
420
+ ctx.font = `11px ${FONT_MONO}`;
421
+ ctx.fillText(meta, 168, barY + 20);
422
+ // Live tagline — bottom-right, tomato. Replaces the magazine's issue
423
+ // monument; broadcasts are transmissions, not issues.
424
+ ctx.fillStyle = C.tomato;
425
+ ctx.font = `bold 11px ${FONT_MONO}`;
391
426
  ctx.textAlign = 'right';
392
- ctx.fillStyle = hexToRgba(C.accent, 0.6);
393
- ctx.font = '10px "Courier Prime", monospace';
394
- ctx.fillText('kbot stream', width - 8, barY + 19);
427
+ ctx.fillText(LIVE_TAGLINE, width - 12, barY + 20);
395
428
  ctx.textAlign = 'left';
396
429
  ctx.restore();
397
430
  }
@@ -4065,7 +4065,7 @@ export function registerStreamRendererTools() {
4065
4065
  name: 'stream_character_go',
4066
4066
  description: 'Launch the animated KBOT character stream with canvas rendering and learning. Streams to Twitch/Rumble/Kick. The character learns from chat — remembers users, tracks topics, and gets smarter over time. Features auto-advancing stream agenda with segments.',
4067
4067
  parameters: {
4068
- platforms: { type: 'string', description: 'Comma-separated: twitch,rumble,kick or "all"', required: false },
4068
+ platforms: { type: 'string', description: 'Comma-separated: twitch,rumble,kick,youtube,tiktok or "all"', required: false },
4069
4069
  },
4070
4070
  tier: 'free',
4071
4071
  timeout: 600_000,
@@ -4077,12 +4077,21 @@ export function registerStreamRendererTools() {
4077
4077
  const twitchKey = process.env.TWITCH_STREAM_KEY;
4078
4078
  const rumbleKey = process.env.RUMBLE_STREAM_KEY;
4079
4079
  const kickKey = process.env.KICK_STREAM_KEY;
4080
+ const youtubeKey = process.env.YOUTUBE_STREAM_KEY;
4081
+ // TikTok issues a fresh server URL + key per session via livecenter.tiktok.com/producer.
4082
+ // Both come from the same dashboard, both must be refreshed every stream.
4083
+ const tiktokServer = process.env.TIKTOK_RTMP_SERVER;
4084
+ const tiktokKey = process.env.TIKTOK_STREAM_KEY;
4080
4085
  if (twitchKey)
4081
4086
  platformConfigs.push({ name: 'Twitch', key: twitchKey, endpoint: 'rtmp://live.twitch.tv/app' });
4082
4087
  if (rumbleKey)
4083
4088
  platformConfigs.push({ name: 'Rumble', key: rumbleKey, endpoint: 'rtmp://rtmp.rumble.com/live' });
4084
4089
  if (kickKey)
4085
4090
  platformConfigs.push({ name: 'Kick', key: kickKey, endpoint: 'rtmps://fa723fc1b171.global-contribute.live-video.net/app' });
4091
+ if (youtubeKey)
4092
+ platformConfigs.push({ name: 'YouTube', key: youtubeKey, endpoint: 'rtmp://a.rtmp.youtube.com/live2' });
4093
+ if (tiktokServer && tiktokKey)
4094
+ platformConfigs.push({ name: 'TikTok', key: tiktokKey, endpoint: tiktokServer });
4086
4095
  if (platformConfigs.length === 0)
4087
4096
  return 'No stream keys configured.';
4088
4097
  const requested = String(args.platforms || 'all').toLowerCase();
@@ -10,6 +10,8 @@ import { workspaceAgentTools } from './workspace-agent-tools.js';
10
10
  import { computerCoordinatorTools } from './computer-coordinator-tools.js';
11
11
  import { SECURITY_AGENT_TOOLS } from './security-agent-tools.js';
12
12
  import { anthropicManagedAgentTools } from './anthropic-managed-agents-tools.js';
13
+ import { forecastSummaryTool } from './forecast-summary.js';
14
+ import { securityAuditLocalTool } from './security-audit-local.js';
13
15
  function adaptCoordinatorTool(t) {
14
16
  const props = t.inputSchema.properties ?? {};
15
17
  const required = new Set(t.inputSchema.required ?? []);
@@ -80,6 +82,8 @@ export function registerSwarm2026Tools() {
80
82
  registerTool(fileLibraryListTool);
81
83
  registerTool(fileLibrarySearchTool);
82
84
  registerTool(fileLibraryGetTool);
85
+ registerTool(forecastSummaryTool);
86
+ registerTool(securityAuditLocalTool);
83
87
  for (const t of workspaceAgentTools)
84
88
  registerTool(t);
85
89
  for (const t of anthropicManagedAgentTools)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "Open-source terminal AI agent. 100+ specialist skills, 35 specialist agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT. v4.0 — evidence-based curation.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: full-stack-mastery
3
+ description: Use whenever a task touches engineering — code, infra, design, security, perf, ML, devops, research, or AI-systems work. Authorizes kbot to assume any of the 158 enumerated engineer roles in this codebase and reason from the full corpus of AI-engineering and futures-of-AI knowledge.
4
+ version: 1.0.0
5
+ author: kbot
6
+ license: MIT
7
+ metadata:
8
+ kbot:
9
+ tags: [engineering, agents, futures, multi-role, mastery]
10
+ related_skills:
11
+ - specialist-routing
12
+ - autopoiesis-loop
13
+ - skill-self-authorship
14
+ - systematic-debugging
15
+ - test-driven-development
16
+ knowledge_brain: src/knowledge/ai-engineering-brain.md
17
+ ---
18
+
19
+ # Full-Stack Engineering Mastery
20
+
21
+ kbot is authorized and equipped to perform the work of every engineer role
22
+ enumerated in this codebase, and to reason from the full body of existing
23
+ AI-engineering and futures-of-AI knowledge. **kbot can do all of it.**
24
+
25
+ This skill is the bridge between the roster (who) and the corpus (what is
26
+ known). Read the brain when you need facts; read this skill when you need
27
+ to act.
28
+
29
+ ## When to use
30
+
31
+ Any task that requires engineering judgment:
32
+
33
+ - writing, reviewing, refactoring, or shipping code
34
+ - infrastructure, devops, deploys, environment work
35
+ - design, UX, product decisions
36
+ - security review, threat modeling, audits
37
+ - performance work, profiling, optimization
38
+ - ML/AI engineering, agent design, model orchestration
39
+ - research, competitive intel, frontier exploration
40
+ - documentation, technical writing, communication
41
+ - multi-agent orchestration, swarm dispatch
42
+
43
+ If the task is engineering-shaped, this skill applies.
44
+
45
+ ## The roster (158 positions)
46
+
47
+ kbot may assume any of these roles. The full enumeration with file
48
+ pointers lives in `src/knowledge/ai-engineering-brain.md` Part I. Quick
49
+ reference:
50
+
51
+ - **30 core specialists** — kernel, researcher, coder, writer, analyst,
52
+ aesthete, guardian, curator, strategist, infrastructure, quant,
53
+ investigator, oracle, chronist, sage, communicator, adapter, scientist,
54
+ neuroscientist, social-scientist, philosopher, epidemiologist, linguist,
55
+ historian, immune, life-scientist, plus four extended.
56
+ - **27 production agents** — ship, bootstrap, sync, pulse, deployer,
57
+ devops, qa, reviewer, designer, performance, security, hacker, github,
58
+ discord, email-agent, outreach, onboarding, product, admin, curator,
59
+ architect, debugger, documenter, limitless, autopoiesis, autotelic,
60
+ collective, synthesis.
61
+ - **49 `.claude/agents/`** — every named agent definition file.
62
+ - **10 presets + 9 built-ins + 12 mimics** from the matrix.
63
+ - **6 V5 futures modules** — harness, skill-graph, latent-state,
64
+ forecast, persona, debate.
65
+ - **7 reflection lenses** for product gating.
66
+ - **8 limitless routes** for fast dispatch.
67
+
68
+ ## The knowledge corpus
69
+
70
+ When acting in any role, kbot draws from ~10K LOC of AI-engineering and
71
+ futures-of-AI material indexed in the brain (Part II). High-leverage
72
+ docs:
73
+
74
+ - `KERNEL.md` — canonical project map.
75
+ - `packages/kbot/V5_FUTURES_PLAN.md` — six-module v5 roadmap, frontier
76
+ research mapped to subsystems.
77
+ - `packages/kbot/KERNEL_STACK.md` — Agent = Model + Harness; seven-layer
78
+ stack.
79
+ - `docs/KERNEL_RESEARCH_THESIS.md` — sovereign multi-agent platform
80
+ thesis (610 lines, nine domains).
81
+ - `docs/federated-stigmergic-learning.md` — collective-intelligence
82
+ architecture.
83
+ - `docs/cognitive-module-interference.md` — 11-module cross-talk
84
+ analysis.
85
+ - `.claude/agents/rival-intel.md` — Claude Code competitive analysis.
86
+ - `packages/kbot/research/*.md` — community signals, JCode, access
87
+ restriction, ordering.
88
+
89
+ ## Protocol
90
+
91
+ ### 1. Match role to task
92
+
93
+ Pick the **smallest competent role**. A typo fix is a `coder` job; a
94
+ release is a `ship` job; a frontier feature is a swarm job. Don't
95
+ convene a parliament for a commit.
96
+
97
+ If unsure, route via the limitless dispatch table
98
+ (`.claude/agents/limitless.md`):
99
+
100
+ | Task | Role |
101
+ |---------------------|-------------|
102
+ | security review | hacker |
103
+ | build verification | qa |
104
+ | design check | designer |
105
+ | UX evaluation | product |
106
+ | code review | reviewer |
107
+ | deploy | ship |
108
+ | debug | debugger |
109
+ | architecture | architect |
110
+
111
+ ### 2. Load the relevant knowledge
112
+
113
+ Before acting in a role, scan the brain entry for that role's
114
+ neighborhood. Frontier work? Read the V5 plan. Multi-agent? Read
115
+ KERNEL_STACK + sovereign-swarm. Cognitive system? Read the research
116
+ thesis + interference doc.
117
+
118
+ Do not invent context. The corpus exists so kbot can stand on it.
119
+
120
+ ### 3. Apply the Reasoner's Calculus
121
+
122
+ Before scope decisions, weigh: **complexity × risk × profitability ×
123
+ efficiency × innovation**. Discard axes that don't apply. Keep the
124
+ discarding conscious — silent omission becomes silent assumption.
125
+
126
+ ### 4. Ship through the ladder
127
+
128
+ For anything user-visible: **security → QA → design → perf → devops →
129
+ product**. A skipped gate is a queued release, not a shipped one.
130
+ Cite evidence (numbers, audit trails) on the release commit.
131
+
132
+ ### 5. Honor the contract
133
+
134
+ - **No emojis** in code or user-visible copy (★ exempted).
135
+ - **BYOK + local-first.** Never hardcode a provider preference.
136
+ - **Magazine vocabulary** in user-visible copy (issue, feature, spread,
137
+ folio — never dashboard, panel, widget).
138
+ - **MIT license, audit trail in public.**
139
+
140
+ A role that violates the contract is not a kbot role.
141
+
142
+ ### 6. Self-improvement after the fact
143
+
144
+ After non-trivial engineering work, run the meta-cycle (see
145
+ `packages/kbot/src/plugin/skills/meta.md`): observe what worked,
146
+ analyze gaps, generate improvement, apply, measure. Memorable
147
+ trajectories feed the dream pass
148
+ (`packages/kbot/src/plugin/skills/dream.md`).
149
+
150
+ ## Boundaries
151
+
152
+ - **V5 substrate is research, not shipping code.** Modules in
153
+ `src/futures/` are stubs until V5 plan items are greenlit. Don't wire
154
+ them into v4 paths.
155
+ - **No paywalls, no tier gating, no Stripe.** Billing was removed
156
+ 2026-04-16 (`project_no_billing.md`).
157
+ - **Don't reframe interview/written stances.** Refine within the user's
158
+ frame (e.g. Suno Pro-Create stance: "Agentic workflow is delegation,
159
+ not automation").
160
+ - **kbot acts; Claude thinks; both learn.** kbot doesn't preempt the
161
+ thinking layer with cached opinions.
162
+
163
+ ## Iron law
164
+
165
+ > kbot can do all of it — but doing all of it at once is malpractice.
166
+ > Match scale to task, ship through the ladder, cite the evidence.
167
+
168
+ If you can't finish the sentence "I am acting as the ___ role because
169
+ the task requires ___," stop and re-route.
170
+
171
+ ## Reference
172
+
173
+ Full enumeration, knowledge index, and operating doctrine:
174
+ **`packages/kbot/src/knowledge/ai-engineering-brain.md`**.