@double_geste/agent-standup 1.25.1 → 1.25.3
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/examples/cats/README.txt +7 -0
- package/examples/cats/biscuit-talk.png +0 -0
- package/examples/cats/biscuit.png +0 -0
- package/examples/cats/cinnamon-talk.png +0 -0
- package/examples/cats/cinnamon.png +0 -0
- package/examples/cats/earl-grey-talk.png +0 -0
- package/examples/cats/earl-grey.png +0 -0
- package/examples/cats/lead-talk.png +0 -0
- package/examples/cats/lead.png +0 -0
- package/examples/cats/miso-talk.png +0 -0
- package/examples/cats/miso.png +0 -0
- package/examples/cats/mochi-talk.png +0 -0
- package/examples/cats/mochi.png +0 -0
- package/examples/cats/oreo-talk.png +0 -0
- package/examples/cats/oreo.png +0 -0
- package/examples/cats/patches-talk.png +0 -0
- package/examples/cats/patches.png +0 -0
- package/examples/cats/salem-talk.png +0 -0
- package/examples/cats/salem.png +0 -0
- package/examples/cats/smokey-talk.png +0 -0
- package/examples/cats/smokey.png +0 -0
- package/examples/cats/you-talk.png +0 -0
- package/examples/cats/you.png +0 -0
- package/examples/characters/README.txt +5 -0
- package/examples/characters/buddy.png +0 -0
- package/examples/characters/lead-talk.png +0 -0
- package/examples/characters/lead.png +0 -0
- package/package.json +2 -1
- package/public/index.html +54 -19
- package/public/locales/da.json +11 -1
- package/public/locales/de.json +11 -1
- package/public/locales/en.json +10 -0
- package/public/locales/es.json +11 -1
- package/public/locales/ja.json +11 -1
- package/public/locales/pt-BR.json +11 -1
- package/public/locales/zh-CN.json +11 -1
- package/public/locales/zh-TW.json +11 -1
- package/server.js +10 -6
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Cat pack for Agent Standup (public domain / CC0, like the starter pack).
|
|
2
|
+
Copy the PNG files into your characters folder, then press Reload:
|
|
3
|
+
- lead.png = Boss, a grumpy cream cat (the lead)
|
|
4
|
+
- you.png = Kibbles, a bag of cat food (you)
|
|
5
|
+
- salem, smokey, mochi, patches, miso, biscuit, cinnamon, earl-grey, oreo = the sub-agents
|
|
6
|
+
- name-talk.png = open-mouth frame for each of them
|
|
7
|
+
64x64 px, transparent background. Drawn procedurally in Python with the help of an AI assistant.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Example pixel characters (public domain, made for Agent Standup).
|
|
2
|
+
Copy them into your characters folder to try the feature, then draw your own:
|
|
3
|
+
- 32x32 or 64x64 px PNG with a transparent background, facing front, feet at the bottom
|
|
4
|
+
- lead.png = the lead (Claude), you.png = you, any other name = a sub-agent
|
|
5
|
+
- name-talk.png = optional open-mouth frame
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.3",
|
|
4
4
|
"description": "Your Claude Code agents as a live video call, from the terminal: a local page in your browser, plus one-line status for status bars. Free, open source, zero token cost.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-standup": "bin/agent-standup.js"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"kokoro-worker.js",
|
|
14
14
|
"demo.js",
|
|
15
15
|
"public",
|
|
16
|
+
"examples",
|
|
16
17
|
"media",
|
|
17
18
|
"LICENSE",
|
|
18
19
|
"README.md"
|
package/public/index.html
CHANGED
|
@@ -279,6 +279,7 @@
|
|
|
279
279
|
<select id="liveProj" style="display:none;margin-top:6px" title="Project to follow"></select>
|
|
280
280
|
<select id="sessions" style="display:none"></select></div>
|
|
281
281
|
<h4 data-fr="Personnages" data-en="Characters">Personnages</h4>
|
|
282
|
+
<div class="box"><select id="charPack" aria-label="Characters"><option value="none" data-fr="Personnages dessinés" data-en="Drawn characters">Personnages dessinés</option><option value="folder" data-fr="Mon dossier" data-en="My folder">Mon dossier</option><option value="starter" data-fr="Pack de départ" data-en="Starter pack">Pack de départ</option><option value="cats" data-fr="Pack de chats" data-en="Cat pack">Pack de chats</option></select></div>
|
|
282
283
|
<div class="box"><div id="charInfo" class="dim" style="font-size:11.5px;line-height:1.5"></div><div style="display:flex;gap:6px;margin-top:6px"><button class="btn" id="charReload" data-fr="Recharger" data-en="Reload">Recharger</button><button class="btn" id="charOpen" style="display:none" data-fr="Ouvrir le dossier" data-en="Open folder">Ouvrir le dossier</button></div></div>
|
|
283
284
|
<button class="mi" data-t="customLaptop"><span data-fr="Ordinateur devant vos personnages" data-en="Laptop in front of your own characters">Ordinateur devant vos personnages</span><i class="sw"></i></button>
|
|
284
285
|
<h4 data-fr="Outils" data-en="Tools">Outils</h4>
|
|
@@ -342,6 +343,11 @@ const savePrefs = () => { prefs._t = Date.now(); try { localStorage.setItem('cc.
|
|
|
342
343
|
const LANGS = [['en', 'English', 'en'], ['fr', 'Français', 'fr'], ['zh-TW', '繁體中文', 'zh-Hant-TW'], ['zh-CN', '简体中文', 'zh-Hans-CN'], ['es', 'Español', 'es'], ['de', 'Deutsch', 'de'], ['da', 'Dansk', 'da'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja']];
|
|
343
344
|
let LOC = {};
|
|
344
345
|
const FR = () => prefs.lang === 'fr';
|
|
346
|
+
const FR_W = /\b(le|la|les|des|une|un|et|est|pour|dans|que|qui|pas|sur|avec|je|tu|nous|vous|ce|cette|ça|du|au|aux|mais|sont|été|fait|plus|ne|ont|il|elle|ils|elles|très|aussi|donc|bien|tout|voici|voilà|merci|oui|non|comme|où|déjà|encore)\b/gi, EN_W = /\b(the|and|is|are|to|of|in|that|with|for|this|it|on|you|we|not|have|has|was|be|will|can|from|your|at|as|but|all|now|also|done|here|there|which|what|then|just|so)\b/gi;
|
|
347
|
+
const langOf = (t) => { // 'fr' or 'en' when a line clearly reads as one of them, else null: the voice then follows the interface language
|
|
348
|
+
t = String(t || ''); const fr = (t.match(FR_W) || []).length + (t.match(/[éèêàçùâîôûœ]/g) || []).length, en = (t.match(EN_W) || []).length;
|
|
349
|
+
return fr >= 2 && fr > en * 1.5 ? 'fr' : en >= 2 && en > fr * 1.5 ? 'en' : null;
|
|
350
|
+
};
|
|
345
351
|
const tr = (en) => (prefs.lang === 'en' || prefs.lang === 'fr' ? en : LOC[en] || en);
|
|
346
352
|
const L = (fr, en) => (FR() ? fr : tr(en));
|
|
347
353
|
async function loadLocale(code) { LOC = {}; if (code === 'en' || code === 'fr') return; try { const r = await fetch('/locales/' + encodeURIComponent(code) + '.json'); if (r.ok) LOC = await r.json(); } catch {} }
|
|
@@ -367,10 +373,14 @@ const mix = (a, b, t) => { const A = hexRGB(a), B = hexRGB(b); return '#' + A.ma
|
|
|
367
373
|
let CHARS = { lead: null, you: null, pool: [], dir: '', n: 0 };
|
|
368
374
|
const toData = (url) => fetch(url).then((r) => r.blob()).then((b) => new Promise((res) => { const f = new FileReader(); f.onload = () => res(f.result); f.readAsDataURL(b); }));
|
|
369
375
|
async function loadChars() {
|
|
370
|
-
|
|
376
|
+
if (!['none', 'folder', 'starter', 'cats'].includes(prefs.charPack)) { // drawn characters by default; someone who already had images in their folder keeps them
|
|
377
|
+
let n = 0; try { n = ((await (await fetch('/api/characters')).json()).chars || []).length; } catch {} prefs.charPack = n ? 'folder' : 'none'; savePrefs(); const sel = $('#charPack'); if (sel) sel.value = prefs.charPack;
|
|
378
|
+
}
|
|
379
|
+
const pack = prefs.charPack, next = { lead: null, you: null, pool: [], dir: '', n: 0, pack };
|
|
380
|
+
if (pack === 'none') { try { next.dir = (await (await fetch('/api/characters')).json()).dir || ''; } catch {} CHARS = next; return; } // the drawn characters, whatever is in the folder
|
|
371
381
|
try {
|
|
372
|
-
const d = await (await fetch('/api/characters')).json(); next.dir = d.dir || '';
|
|
373
|
-
for (const c of d.chars || []) { try { const e = { name: c.stem, idle: await toData(
|
|
382
|
+
const base = pack === 'folder' ? '/characters/' : '/packs/' + pack + '/', d = await (await fetch('/api/characters?pack=' + (pack === 'folder' ? '' : pack))).json(); next.dir = d.dir || '';
|
|
383
|
+
for (const c of d.chars || []) { try { const e = { name: c.stem, idle: await toData(base + encodeURIComponent(c.file)), talk: c.talk ? await toData(base + encodeURIComponent(c.talk)) : null };
|
|
374
384
|
if (c.role === 'lead') next.lead = e; else if (c.role === 'you') next.you = e; else next.pool.push(e); next.n++; } catch {} }
|
|
375
385
|
} catch {}
|
|
376
386
|
CHARS = next;
|
|
@@ -383,7 +393,7 @@ const customFor = (id) => { // sub-agents: the best-hashed image no other agen
|
|
|
383
393
|
};
|
|
384
394
|
function renderCharInfo() {
|
|
385
395
|
const el = $('#charInfo'); if (!el) return;
|
|
386
|
-
el.innerHTML = CHARS.n ? `✓ ${CHARS.n} ${esc(L('personnage(s) personnalisé(s) chargé(s)', 'custom character(s) loaded'))}` : `${esc(L('Déposez vos images (PNG) dans ce dossier pour remplacer les personnages :', 'Drop your pixel art (PNG) in this folder to replace the characters:'))}<br><code style="word-break:break-all">${esc(CHARS.dir)}</code><br>${esc(L('lead.png = le chef · you.png = vous · les autres = les sous-agents · nom-talk.png = bouche ouverte', 'lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth'))}`;
|
|
396
|
+
el.innerHTML = CHARS.pack === 'none' ? esc(L('Les personnages dessinés par l’application. Choisissez « Mon dossier » pour vos propres images, ou un pack.', 'The characters drawn by the app. Pick “My folder” for your own images, or a pack.')) : CHARS.pack !== 'folder' ? `✓ ${CHARS.n} ${esc(L('personnages du pack', 'characters from the pack'))} · ${esc(L('Pour les modifier, copiez-les dans votre dossier et choisissez « Mon dossier ».', 'To tweak them, copy them into your folder and pick “My folder”.'))}` : CHARS.n ? `✓ ${CHARS.n} ${esc(L('personnage(s) personnalisé(s) chargé(s)', 'custom character(s) loaded'))}` : `${esc(L('Déposez vos images (PNG) dans ce dossier pour remplacer les personnages :', 'Drop your pixel art (PNG) in this folder to replace the characters:'))}<br><code style="word-break:break-all">${esc(CHARS.dir)}</code><br>${esc(L('lead.png = le chef · you.png = vous · les autres = les sous-agents · nom-talk.png = bouche ouverte', 'lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth'))}`;
|
|
387
397
|
$('#charOpen').style.display = EMBED ? '' : 'none';
|
|
388
398
|
}
|
|
389
399
|
function persona(a) { const p = persona0(a); p.custom = customFor(a.id); return p; }
|
|
@@ -527,7 +537,7 @@ const speakable = (t) => t.replace(/(?:[\w.-]+\/)+([\w.-]+)/g, (m, f) => f.repla
|
|
|
527
537
|
const trMem = new Map(), trPending = new Map();
|
|
528
538
|
const baseText = (ev, max) => short(ev.text, max || (S.live ? (ev.kind === 'order' ? 220 : 180) : (ev.kind === 'order' ? 200 : 230)));
|
|
529
539
|
function ensureTr(texts) {
|
|
530
|
-
texts = [...new Set(texts)].filter((t) => t && !trMem.has(t));
|
|
540
|
+
texts = [...new Set(texts)].filter((t) => t && !trMem.has(t)).filter((t) => langOf(t) !== 'fr'); // already French: nothing to translate
|
|
531
541
|
const waits = texts.filter((t) => trPending.has(t)).map((t) => trPending.get(t)), fresh = texts.filter((t) => !trPending.has(t));
|
|
532
542
|
if (fresh.length) {
|
|
533
543
|
const pr = fetch('/api/translate', { method: 'POST', body: JSON.stringify({ texts: fresh }) }).then((r) => r.json()).then((d) => {
|
|
@@ -713,7 +723,7 @@ const TTS = {
|
|
|
713
723
|
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
714
724
|
},
|
|
715
725
|
voiceFor(p, fr = FR()) { const pool = this.pool[fr ? 'fr' : 'en']; return pool.length ? (p.id === 'main' ? pool[0] : pool[(p.vi >>> 2) % pool.length]) : null; },
|
|
716
|
-
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() : +$('#speed').value))) / 10) * 10; },
|
|
726
|
+
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() * (S.boost > 1 ? 1.25 : 1) : +$('#speed').value))) / 10) * 10; },
|
|
717
727
|
buf(text, voice, rate) {
|
|
718
728
|
const key = this.engine + '|' + voice + '|' + rate + '|' + text;
|
|
719
729
|
if (!this.cache.has(key)) {
|
|
@@ -728,7 +738,7 @@ function warmTTS(i) { // synthesize the next few lines ahead of time
|
|
|
728
738
|
if (!TTS.server || !AUD.ctx || !prefs.voice || prefs.vstyle === 'animal' || prefs.vstyle === 'sims') return;
|
|
729
739
|
let n = 0; for (let k = i; k < S.events.length && n < 4; k++) {
|
|
730
740
|
const e = S.events[k]; if (quiet(e)) continue; n++;
|
|
731
|
-
const p = S.agents[e.agent], v = p && TTS.voiceFor(p,
|
|
741
|
+
const p = S.agents[e.agent], txt = speakable(spoken(e)), v = p && TTS.voiceFor(p, frVoice(txt)); if (v) TTS.buf(txt, v, TTS.rate()).catch(() => {});
|
|
732
742
|
}
|
|
733
743
|
}
|
|
734
744
|
|
|
@@ -849,7 +859,8 @@ function captionRun(id, text, totalMs) {
|
|
|
849
859
|
for (let i = 1; i < ps.length; i++) { const x = ps[i], when = at; timers.push(setTimeout(() => setCaption(id, x), when)); at += totalMs * x.length / n; }
|
|
850
860
|
capTimers.set(id, timers);
|
|
851
861
|
}
|
|
852
|
-
|
|
862
|
+
const frVoice = (t) => (langOf(t) ?? (FR() ? 'fr' : 'en')) === 'fr'; // a French line gets a French voice whatever the interface language, and the other way round
|
|
863
|
+
async function speakLine(text, p, primary, token, fr = frVoice(text)) {
|
|
853
864
|
captionRun(p.id, text, estMs(text));
|
|
854
865
|
if (AUD.off() || !prefs.voice) { if (!AUD.off() && prefs.office) AUD.murmur(0.5 + (p.vi % 5) / 10, estMs(text) / 1000); return sleep(estMs(text)); }
|
|
855
866
|
if (S.hurry) return sleep(estMs(text));
|
|
@@ -1250,6 +1261,7 @@ setInterval(() => { // now and then a busy agent has a little thought; never w
|
|
|
1250
1261
|
}, 7000);
|
|
1251
1262
|
function oops(id) { if (!prefs.banter || !S.playing || S.recomputing) return; const t = $('#t-' + id); if (!t || t.style.display === 'none' || (t._oops && Date.now() - t._oops < 4000)) return; t._oops = Date.now(); t.classList.remove('oops'); void t.offsetWidth; t.classList.add('oops'); thought(id, '😬'); }
|
|
1252
1263
|
function confetti() {
|
|
1264
|
+
S.wrap = true; // the lead is done: if the end of its last message was never read, it is read now (see wrapUp)
|
|
1253
1265
|
if (!prefs.banter || document.hidden) return; const box = $('#center'), cols = ['#f5c04a', '#7c9cff', '#ff7a6b', '#5ad19a', '#c58bff'];
|
|
1254
1266
|
for (let i = 0; i < 26; i++) { const c = document.createElement('i'); c.className = 'confetti'; c.style.left = Math.random() * 100 + '%'; c.style.background = cols[i % cols.length]; c.style.animationDelay = Math.random() * 0.6 + 's'; c.style.animationDuration = 1.8 + Math.random() * 1.4 + 's'; box.appendChild(c); setTimeout(() => c.remove(), 4200); }
|
|
1255
1267
|
}
|
|
@@ -1531,7 +1543,7 @@ async function react(ev, token) {
|
|
|
1531
1543
|
if ((!prefs.epi && !prefs.banter) || S.boost > 1 || S.hurry || !caughtUp()) return; const seed = hash(String(ev.ts) + ev.agent), R = REACT[FR() ? 'fr' : 'en']; let who, line, thanks = false;
|
|
1532
1544
|
if (ev.kind === 'order') { if (!prefs.banter && seed % 100 > 55) return; who = ev.to; line = prefs.banter ? ackLine(ev) : R.ack[seed % R.ack.length]; }
|
|
1533
1545
|
else if (ev.kind === 'report') {
|
|
1534
|
-
const r = seed % 100, joke = prefs.epi ? 60 : 35; // a remark from someone else, or (a little less often) a word of thanks from whoever receives the report
|
|
1546
|
+
const r = seed % 100, joke = prefs.epi ? 60 : prefs.chat === 'quiet' ? -1 : 35; // a remark from someone else, or (a little less often) a word of thanks from whoever receives the report
|
|
1535
1547
|
if (r <= joke) { const others = Object.keys(S.status).concat(['main']).filter((x) => x !== ev.agent && S.agents[x] && !S.gone.has(x)); who = others[(seed >>> 3) % others.length]; line = quipMem.get(baseText(ev, 160)) || R.done[seed % R.done.length]; }
|
|
1536
1548
|
else if (prefs.banter && r <= joke + 25 && ev.to !== 'user' && S.agents[ev.to] && !S.gone.has(ev.to)) { who = ev.to; line = varied('thanks', seed >>> 3); thanks = true; }
|
|
1537
1549
|
else return;
|
|
@@ -1560,10 +1572,18 @@ function varied(kind, seed) { const P = TALK[FR() ? 'fr' : 'en'][kind]; let line
|
|
|
1560
1572
|
const thinkerOf = (e) => (e.kind === 'results' ? e.agent : e.kind === 'user' ? 'main' : e.kind === 'report' || e.kind === 'finish' || e.kind === 'order' ? e.to : null);
|
|
1561
1573
|
const before = (i) => { let k = i - 1; while (k >= 0 && (S.events[k].kind === 'usage' || S.events[k].kind === 'compact')) k--; return S.events[k]; };
|
|
1562
1574
|
async function ponder(i, token) { // replay: an agent that really took a long time (40 s or more) before this line or call says so first, about every other time
|
|
1563
|
-
const ev = S.events[i], p = before(i); if (!prefs.banter || S.boost > 1 || S.hurry || !p || !S.agents[ev.agent] || thinkerOf(p) !== ev.agent || ev.ts - p.ts < 40000 || hash('t' + ev.ts) % 100 > 55) return;
|
|
1575
|
+
const ev = S.events[i], p = before(i); if (!prefs.banter || prefs.chat === 'quiet' || S.boost > 1 || S.hurry || !p || !S.agents[ev.agent] || thinkerOf(p) !== ev.agent || ev.ts - p.ts < 40000 || hash('t' + ev.ts) % 100 > 55) return;
|
|
1564
1576
|
const line = varied('think', hash(String(ev.ts))); startTalk(ev.agent, line, '💭'); await speakLine(line, S.agents[ev.agent], true, token); stopTalk(ev.agent);
|
|
1565
1577
|
}
|
|
1578
|
+
async function wrapUp(token) { // live, once the lead is done: the part of its last message that the clipped reading left out, so you know what was done
|
|
1579
|
+
if (!S.wrap || S.speakers.size || !prefs.voice && !prefs.banter) return false; S.wrap = false;
|
|
1580
|
+
let k = S.events.length - 1; while (k >= 0 && !(S.events[k].agent === 'main' && S.events[k].kind === 'say')) k--; if (k < 0) return false; const e = S.events[k], m = S.mainSaid;
|
|
1581
|
+
const full = plain(e.text); let rest = m && m.ev === e ? full.slice(m.said).trim() : full; if (m && m.ev === e && m.full) return false; if (!m || m.ev !== e) rest = full;
|
|
1582
|
+
rest = rest.replace(/^[^\w\p{L}]+/u, ''); if (rest.length < 25) return false; const line = (m && m.ev === e ? L('Pour résumer : ', 'To sum up: ') : '') + short(rest, 520);
|
|
1583
|
+
startTalk('main', line, '🎉'); await speakLine(line, S.agents.main, true, token); stopTalk('main'); return true;
|
|
1584
|
+
}
|
|
1566
1585
|
async function smallTalk(token) {
|
|
1586
|
+
if (prefs.chat === 'quiet') return; // quiet: nothing to fill the silences, only the real exchanges
|
|
1567
1587
|
const n = S.fill || 0, last = S.events[S.events.length - 1]; if (!prefs.banter || n > 2 || !last || S.speakers.size || S.hurry || S.boost > 1 || (document.hidden && !S.exporting)) return;
|
|
1568
1588
|
const quiet = performance.now() - S.spokeAt, here = Object.keys(S.agents).filter((id) => id !== 'user' && $('#t-' + id) && $('#t-' + id).style.display !== 'none'), pick = (a) => a[Math.floor(Math.random() * a.length)];
|
|
1569
1589
|
const p = before(S.events.length), muser = S.live && !n && !S.mused && p ? thinkerOf(p) : null, think = !!muser && here.includes(muser) && quiet > 6000 && Date.now() - p.ts > 20000 && Date.now() - p.ts < 180000; // live: 20 s without a word from whoever is thinking, once per silence
|
|
@@ -1585,11 +1605,11 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1585
1605
|
function aheadSpeakable(i, limit) { let n = 0; for (let k = i, m = Math.min(S.events.length, i + 800); k < m; k++) if (SPK.has(S.events[k].kind) && ++n > limit) break; return n; }
|
|
1586
1606
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1587
1607
|
async function run(i) {
|
|
1588
|
-
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; S.exQ = []; S.errRun = {}; S.syncTs = (S.events[i] && S.events[i].ts) || Date.now(); syncUI(); AUD.init();
|
|
1608
|
+
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; S.exQ = []; S.errRun = {}; S.wrap = false; S.mainSaid = null; S.syncTs = (S.events[i] && S.events[i].ts) || Date.now(); syncUI(); AUD.init();
|
|
1589
1609
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1590
1610
|
while (token === S.token) {
|
|
1591
1611
|
const lim = S.stopAt || S.events.length;
|
|
1592
|
-
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } await smallTalk(token); await sleep(350); continue; } break; }
|
|
1612
|
+
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } if (await wrapUp(token)) continue; await smallTalk(token); await sleep(350); continue; } break; }
|
|
1593
1613
|
if (S.idle) { S.idle = false; syncUI(); }
|
|
1594
1614
|
if (S.live) { // way behind: skip ahead to stay current, but only count what is actually said: sub-agents produce thousands of silent tool events
|
|
1595
1615
|
const rem = S.events.length - i;
|
|
@@ -1605,10 +1625,15 @@ async function run(i) {
|
|
|
1605
1625
|
if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
|
|
1606
1626
|
if (ev.kind === 'usage' || ev.kind === 'compact') { if (ev.kind === 'compact' && S.live && prefs.banter) toast(`${nameOf(ev.agent)}: ${L('contexte compacté 🗜', 'context compacted 🗜')}`); i++; continue; }
|
|
1607
1627
|
if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
|
|
1608
|
-
|
|
1609
|
-
if (ev.kind === '
|
|
1628
|
+
const backlog = S.live ? S.events.length - i : 0, quick = S.live ? Math.min(1, 40 / Math.max(1, backlog)) : 1; // live: hundreds of tool events from parallel sub-agents must not take minutes to scroll past
|
|
1629
|
+
if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.boost === 1 && S.present === ev.agent && ev.items.some((it) => it.img) ? (S.live ? 900 : 1800) : S.demo ? 900 : S.live ? 30 * quick : 80) / spd()); i++; continue; } // a picture gets a moment on screen
|
|
1630
|
+
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); paintStatus(); if (quick === 1) await smallTalk(token); await sleep((S.demo ? 1100 : S.live ? 110 * quick : 250) / spd()); i++; continue; }
|
|
1610
1631
|
const group = [ev]; let j = i + 1; // sub-agents talking back-to-back were running in parallel: they talk over each other
|
|
1611
|
-
if (
|
|
1632
|
+
if (ev.kind === 'order') { // several orders in a row (a parallel launch): the new agents all appear now, the first order is read, the others are named in one line
|
|
1633
|
+
while (j < Math.min(S.events.length, i + 40) && group.length < 8) { const e2 = S.events[j]; if (e2.kind === 'usage' || e2.kind === 'compact') { tally(e2); j++; continue; } if (e2.kind === 'order' && e2.agent === ev.agent) { group.push(e2); j++; } else break; }
|
|
1634
|
+
if (group.length > 1) { for (const g of group) applyStatus(g); paintStatus(); }
|
|
1635
|
+
}
|
|
1636
|
+
else if (isSub(ev.agent) && ev.kind !== 'order') {
|
|
1612
1637
|
while (j < Math.min(S.events.length, i + 28) && group.length < 3) {
|
|
1613
1638
|
const e2 = S.events[j];
|
|
1614
1639
|
if (e2.kind === 'usage' || e2.kind === 'compact') { tally(e2); j++; continue; }
|
|
@@ -1626,11 +1651,18 @@ async function run(i) {
|
|
|
1626
1651
|
startTalk(g.agent, line, label);
|
|
1627
1652
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1628
1653
|
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1629
|
-
|
|
1654
|
+
const said = clipSpeech(line, S.pace); if (g.agent === 'main' && g.kind === 'say') { const full = plain(g.text), key = said.replace(/…$/, '').trim().slice(-30), pos = key ? full.indexOf(key) : -1, n = pos >= 0 ? pos + key.length : said.length; S.mainSaid = { ev: g, said: n, full: n >= full.length - 5 }; }
|
|
1655
|
+
return speakLine(said, S.agents[g.agent], true, token, frVoice(said)).then(() => stopTalk(g.agent));
|
|
1630
1656
|
};
|
|
1631
|
-
let plays;
|
|
1657
|
+
let plays;
|
|
1658
|
+
if (ev.kind === 'order' && group.length > 1) { // the first order in full, then "Also Remy on X, Iris on Y"
|
|
1659
|
+
const rest = group.slice(1), line = `${L('Et aussi ', 'Also ')}${rest.map((o) => `${nameOf(o.to)}${L(' sur ', ' on ')}${(o.title || short(o.text, 60)).replace(/[.。]\s*$/, '')}`).join(', ')}.`;
|
|
1660
|
+
setTimeout(() => rest.forEach((o, k) => setTimeout(() => { fly(o.agent, o.to, '📨'); popTile(o.to); }, k * 120)), 30);
|
|
1661
|
+
plays = [playOne(ev).then(() => { if (token !== S.token) return; startTalk(ev.agent, line, ''); return speakLine(clipSpeech(line, S.pace), S.agents[ev.agent], true, token).then(() => stopTalk(ev.agent)); })];
|
|
1662
|
+
}
|
|
1663
|
+
else if (S.live) { plays = []; let chain = Promise.resolve(); for (const g of group) { chain = chain.then(() => (token === S.token ? playOne(g) : null)); plays.push(chain); } } else plays = group.map(playOne); // live: one voice at a time so each can be heard; replay keeps the overlapping chatter
|
|
1632
1664
|
S.idx = j - 1; paintStatus(); await Promise.all(plays); if (token !== S.token) return;
|
|
1633
|
-
for (const g of group) { await converse(g, token); if (token !== S.token) return; }
|
|
1665
|
+
for (const g of (ev.kind === 'order' && group.length > 1 ? [ev] : group)) { await converse(g, token); if (token !== S.token) return; }
|
|
1634
1666
|
i = j;
|
|
1635
1667
|
}
|
|
1636
1668
|
if (token === S.token) {
|
|
@@ -1701,7 +1733,9 @@ setInterval(async () => {
|
|
|
1701
1733
|
if (d.error) return; if (d.total < S.events.length) { S.cur = null; return; } // transcript was rewritten: reload next tick
|
|
1702
1734
|
if (!d.events || !d.events.length) return;
|
|
1703
1735
|
for (const a of d.agents) if (!S.agents[a.id]) { S.agents[a.id] = persona(a); addTile(S.agents[a.id]); }
|
|
1704
|
-
const from = S.events.length; S.events = S.events.concat(d.events); $('#seek').max = S.events.length - 1; trNew(from); warmTTS(from); renderLog(false);
|
|
1736
|
+
const from = S.events.length; S.events = S.events.concat(d.events); $('#seek').max = S.events.length - 1; trNew(from); warmTTS(from); renderLog(false);
|
|
1737
|
+
if (S.playing && !S.recomputing) for (const e of d.events) if (e.kind === 'order' && !S.status[e.to]) { S.status[e.to] = 'working'; S.orderTs[e.to] = e.ts || 0; S.gone.delete(e.to); } // a new sub-agent shows up now, while the previous line is still being read
|
|
1738
|
+
paintStatus();
|
|
1705
1739
|
} catch { if (!S.offline) { S.offline = true; syncUI(); } } finally { polling = false; }
|
|
1706
1740
|
}, 400);
|
|
1707
1741
|
|
|
@@ -1907,6 +1941,7 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
|
|
|
1907
1941
|
|
|
1908
1942
|
$('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
|
|
1909
1943
|
$('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
1944
|
+
$('#charPack').value = ['none', 'folder', 'starter', 'cats'].includes(prefs.charPack) ? prefs.charPack : 'none'; $('#charPack').onchange = () => { prefs.charPack = $('#charPack').value; savePrefs(); $('#charReload').onclick(); };
|
|
1910
1945
|
$('#charReload').onclick = async () => { await loadChars(); for (const k in imgCache) delete imgCache[k]; for (const id in S.agents) S.agents[id].custom = null; for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
|
|
1911
1946
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1912
1947
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
package/public/locales/da.json
CHANGED
|
@@ -206,5 +206,15 @@
|
|
|
206
206
|
"Normal": "Normal",
|
|
207
207
|
"nothing to report": "intet at rapportere",
|
|
208
208
|
"Quiet": "Stille",
|
|
209
|
-
"Laptop in front of your own characters": "Bærbar foran dine egne figurer"
|
|
209
|
+
"Laptop in front of your own characters": "Bærbar foran dine egne figurer",
|
|
210
|
+
"Also ": "Også ",
|
|
211
|
+
" on ": " på ",
|
|
212
|
+
"To sum up: ": "Kort sagt: ",
|
|
213
|
+
"Cat pack": "Kattepakke",
|
|
214
|
+
"characters from the pack": "figurer fra pakken",
|
|
215
|
+
"Drawn characters": "Tegnede figurer",
|
|
216
|
+
"Starter pack": "Startpakke",
|
|
217
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "For at tilpasse dem: kopiér dem til din mappe og vælg “Min mappe”.",
|
|
218
|
+
"My folder": "Min mappe",
|
|
219
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Figurerne tegnet af appen. Vælg “Min mappe” for dine egne billeder, eller en pakke."
|
|
210
220
|
}
|
package/public/locales/de.json
CHANGED
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "Normal",
|
|
208
208
|
"nothing to report": "nichts zu melden",
|
|
209
209
|
"Quiet": "Ruhig",
|
|
210
|
-
"Laptop in front of your own characters": "Laptop vor deinen eigenen Figuren"
|
|
210
|
+
"Laptop in front of your own characters": "Laptop vor deinen eigenen Figuren",
|
|
211
|
+
"Also ": "Außerdem ",
|
|
212
|
+
" on ": " an ",
|
|
213
|
+
"To sum up: ": "Zusammengefasst: ",
|
|
214
|
+
"Cat pack": "Katzen-Pack",
|
|
215
|
+
"characters from the pack": "Figuren aus dem Pack",
|
|
216
|
+
"Drawn characters": "Gezeichnete Figuren",
|
|
217
|
+
"Starter pack": "Starter-Pack",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "Zum Anpassen in deinen Ordner kopieren und „Mein Ordner“ wählen.",
|
|
219
|
+
"My folder": "Mein Ordner",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Die von der App gezeichneten Figuren. Wähle „Mein Ordner“ für eigene Bilder oder ein Pack."
|
|
211
221
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
" full — compaction soon": " full — compaction soon",
|
|
3
|
+
" on ": " on ",
|
|
3
4
|
"”": "”",
|
|
4
5
|
"↗ Open in browser": "↗ Open in browser",
|
|
5
6
|
"⌨ Keyboard shortcuts (?)": "⌨ Keyboard shortcuts (?)",
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"Agents": "Agents",
|
|
15
16
|
"Agents list (left)": "Agents list (left)",
|
|
16
17
|
"All projects (most recent)": "All projects (most recent)",
|
|
18
|
+
"Also ": "Also ",
|
|
17
19
|
"Ambience volume": "Ambience volume",
|
|
18
20
|
"An agent may be waiting for you ✋": "An agent may be waiting for you ✋",
|
|
19
21
|
"Animal Crossing (babble)": "Animal Crossing (babble)",
|
|
@@ -25,7 +27,9 @@
|
|
|
25
27
|
"cache write": "cache write",
|
|
26
28
|
"Call": "Call",
|
|
27
29
|
"Cartoon voices (real voice)": "Cartoon voices (real voice)",
|
|
30
|
+
"Cat pack": "Cat pack",
|
|
28
31
|
"Characters": "Characters",
|
|
32
|
+
"characters from the pack": "characters from the pack",
|
|
29
33
|
"Chatter between agents": "Chatter between agents",
|
|
30
34
|
"Claude Code is not logged in: run “claude” once in a terminal to sign in.": "Claude Code is not logged in: run “claude” once in a terminal to sign in.",
|
|
31
35
|
"Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.": "Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.",
|
|
@@ -46,6 +50,7 @@
|
|
|
46
50
|
"Done": "Done",
|
|
47
51
|
"Done. ": "Done. ",
|
|
48
52
|
"Downloading, you can carry on: system voices are used meanwhile.": "Downloading, you can carry on: system voices are used meanwhile.",
|
|
53
|
+
"Drawn characters": "Drawn characters",
|
|
49
54
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Drop your pixel art (PNG) in this folder to replace the characters:",
|
|
50
55
|
"duration": "duration",
|
|
51
56
|
"Editing ": "Editing ",
|
|
@@ -91,6 +96,7 @@
|
|
|
91
96
|
"may be waiting for your approval": "may be waiting for your approval",
|
|
92
97
|
"Music volume": "Music volume",
|
|
93
98
|
"Mute / unmute": "Mute / unmute",
|
|
99
|
+
"My folder": "My folder",
|
|
94
100
|
"Natural voices": "Natural voices",
|
|
95
101
|
"need you": "need you",
|
|
96
102
|
"needs you?": "needs you?",
|
|
@@ -161,15 +167,19 @@
|
|
|
161
167
|
"stalled: a sub-agent has been silent for a long time": "stalled: a sub-agent has been silent for a long time",
|
|
162
168
|
"standing by": "standing by",
|
|
163
169
|
"Start Claude Code in this folder — the call will start by itself.": "Start Claude Code in this folder — the call will start by itself.",
|
|
170
|
+
"Starter pack": "Starter pack",
|
|
164
171
|
"sub-agents active": "sub-agents active",
|
|
165
172
|
"task": "task",
|
|
166
173
|
"Tasks": "Tasks",
|
|
174
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "The characters drawn by the app. Pick “My folder” for your own images, or a pack.",
|
|
167
175
|
"THE END": "THE END",
|
|
168
176
|
"The engine stopped: system voices are used instead.": "The engine stopped: system voices are used instead.",
|
|
169
177
|
"This dev was replaced by his own agents. Spare a coffee for his career change? ☕": "This dev was replaced by his own agents. Spare a coffee for his career change? ☕",
|
|
170
178
|
"This help": "This help",
|
|
171
179
|
"Time": "Time",
|
|
172
180
|
"to": "to",
|
|
181
|
+
"To sum up: ": "To sum up: ",
|
|
182
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "To tweak them, copy them into your folder and pick “My folder”.",
|
|
173
183
|
"tokens": "tokens",
|
|
174
184
|
"Tokens": "Tokens",
|
|
175
185
|
"Tokens recorded by Claude Code (counted once per message, not a bill)": "Tokens recorded by Claude Code (counted once per message, not a bill)",
|
package/public/locales/es.json
CHANGED
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "Normal",
|
|
208
208
|
"nothing to report": "nada que informar",
|
|
209
209
|
"Quiet": "Tranquilo",
|
|
210
|
-
"Laptop in front of your own characters": "Portátil delante de tus propios personajes"
|
|
210
|
+
"Laptop in front of your own characters": "Portátil delante de tus propios personajes",
|
|
211
|
+
"Also ": "También ",
|
|
212
|
+
" on ": " en ",
|
|
213
|
+
"To sum up: ": "En resumen: ",
|
|
214
|
+
"Cat pack": "Pack de gatos",
|
|
215
|
+
"characters from the pack": "personajes del pack",
|
|
216
|
+
"Drawn characters": "Personajes dibujados",
|
|
217
|
+
"Starter pack": "Pack inicial",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "Para retocarlos, cópialos en tu carpeta y elige «Mi carpeta».",
|
|
219
|
+
"My folder": "Mi carpeta",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Los personajes dibujados por la aplicación. Elige «Mi carpeta» para tus propias imágenes, o un pack."
|
|
211
221
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "ふつう",
|
|
208
208
|
"nothing to report": "報告事項なし",
|
|
209
209
|
"Quiet": "静か",
|
|
210
|
-
"Laptop in front of your own characters": "自作キャラクターの前にノートパソコン"
|
|
210
|
+
"Laptop in front of your own characters": "自作キャラクターの前にノートパソコン",
|
|
211
|
+
"Also ": "それから ",
|
|
212
|
+
" on ": " は ",
|
|
213
|
+
"To sum up: ": "まとめると: ",
|
|
214
|
+
"Cat pack": "ネコパック",
|
|
215
|
+
"characters from the pack": "パックのキャラクター",
|
|
216
|
+
"Drawn characters": "描画キャラクター",
|
|
217
|
+
"Starter pack": "スターターパック",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "調整するには、自分のフォルダーにコピーして「自分のフォルダー」を選んでください。",
|
|
219
|
+
"My folder": "自分のフォルダー",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "アプリが描画するキャラクターです。自分の画像を使うには「自分のフォルダー」、またはパックを選んでください。"
|
|
211
221
|
}
|
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "Normal",
|
|
208
208
|
"nothing to report": "nada a reportar",
|
|
209
209
|
"Quiet": "Quieto",
|
|
210
|
-
"Laptop in front of your own characters": "Notebook na frente dos seus personagens"
|
|
210
|
+
"Laptop in front of your own characters": "Notebook na frente dos seus personagens",
|
|
211
|
+
"Also ": "Também ",
|
|
212
|
+
" on ": " em ",
|
|
213
|
+
"To sum up: ": "Resumindo: ",
|
|
214
|
+
"Cat pack": "Pacote de gatos",
|
|
215
|
+
"characters from the pack": "personagens do pacote",
|
|
216
|
+
"Drawn characters": "Personagens desenhados",
|
|
217
|
+
"Starter pack": "Pacote inicial",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "Para ajustá-los, copie-os para sua pasta e escolha “Minha pasta”.",
|
|
219
|
+
"My folder": "Minha pasta",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Os personagens desenhados pelo aplicativo. Escolha “Minha pasta” para suas próprias imagens, ou um pacote."
|
|
211
221
|
}
|
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "正常",
|
|
208
208
|
"nothing to report": "没什么可报告的",
|
|
209
209
|
"Quiet": "安静",
|
|
210
|
-
"Laptop in front of your own characters": "在自定义角色前显示笔记本电脑"
|
|
210
|
+
"Laptop in front of your own characters": "在自定义角色前显示笔记本电脑",
|
|
211
|
+
"Also ": "另外 ",
|
|
212
|
+
" on ": " 负责 ",
|
|
213
|
+
"To sum up: ": "总结一下: ",
|
|
214
|
+
"Cat pack": "猫咪包",
|
|
215
|
+
"characters from the pack": "来自角色包的角色",
|
|
216
|
+
"Drawn characters": "绘制的角色",
|
|
217
|
+
"Starter pack": "入门包",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "若要修改,请将它们复制到你的文件夹并选择“我的文件夹”。",
|
|
219
|
+
"My folder": "我的文件夹",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "由应用绘制的角色。选择“我的文件夹”使用你自己的图片,或选择一个角色包。"
|
|
211
221
|
}
|
|
@@ -207,5 +207,15 @@
|
|
|
207
207
|
"Normal": "正常",
|
|
208
208
|
"nothing to report": "沒什麼好回報的",
|
|
209
209
|
"Quiet": "安靜",
|
|
210
|
-
"Laptop in front of your own characters": "在自訂角色前顯示筆記型電腦"
|
|
210
|
+
"Laptop in front of your own characters": "在自訂角色前顯示筆記型電腦",
|
|
211
|
+
"Also ": "另外 ",
|
|
212
|
+
" on ": " 負責 ",
|
|
213
|
+
"To sum up: ": "總結一下: ",
|
|
214
|
+
"Cat pack": "貓咪包",
|
|
215
|
+
"characters from the pack": "來自角色包的角色",
|
|
216
|
+
"Drawn characters": "繪製的角色",
|
|
217
|
+
"Starter pack": "入門包",
|
|
218
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "若要修改,請將它們複製到你的資料夾並選擇「我的資料夾」。",
|
|
219
|
+
"My folder": "我的資料夾",
|
|
220
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "由應用程式繪製的角色。選擇「我的資料夾」使用你自己的圖片,或選擇一個角色包。"
|
|
211
221
|
}
|
package/server.js
CHANGED
|
@@ -503,8 +503,10 @@ const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css
|
|
|
503
503
|
const CHAR_NAME = /^[\w][\w .()\-]{0,78}\.(png|gif|webp|jpe?g)$/i, CHAR_MAX = 300 * 1024, CHAR_TYPES = { png: 'image/png', gif: 'image/gif', webp: 'image/webp', jpg: 'image/jpeg', jpeg: 'image/jpeg' };
|
|
504
504
|
const charactersDir = () => process.env.AGENT_STANDUP_CHARACTERS || path.join(os.homedir(), '.agent-standup', 'characters');
|
|
505
505
|
// lstat, not stat: a symlink is not a regular file, so it is skipped and never served
|
|
506
|
-
|
|
507
|
-
|
|
506
|
+
// Built-in character packs, shipped with the app (examples/<folder>): picked in ⚙ → Characters, or copied into the person's own folder as a start
|
|
507
|
+
const PACKS = { starter: 'characters', cats: 'cats' }, packDir = (name) => (PACKS[name] ? path.join(__dirname, 'examples', PACKS[name]) : null);
|
|
508
|
+
function listCharacters(pack) {
|
|
509
|
+
const dir = pack ? packDir(pack) : charactersDir(); let names = []; if (!dir) return { dir: '', max: CHAR_MAX, chars: [] }; try { names = fs.readdirSync(dir); } catch {}
|
|
508
510
|
const ok = names.filter((f) => CHAR_NAME.test(f)).filter((f) => { try { const st = fs.lstatSync(path.join(dir, f)); return st.isFile() && st.size <= CHAR_MAX; } catch { return false; } }).slice(0, 80);
|
|
509
511
|
const stem = (f) => f.replace(/\.[^.]+$/, ''), by = new Map(ok.map((f) => [stem(f).toLowerCase(), f]));
|
|
510
512
|
const chars = ok.filter((f) => !/-talk$/i.test(stem(f))).map((f) => { const st = stem(f), l = st.toLowerCase();
|
|
@@ -563,7 +565,8 @@ function start(port = PORT) {
|
|
|
563
565
|
if (url.pathname === '/api/prefs') return json(readPrefs() || {});
|
|
564
566
|
if (url.pathname === '/api/current') return json(currentSession(url.searchParams.get('proj') || '', url.searchParams.get('id') || '', { proj: url.searchParams.get('sproj') || '', id: url.searchParams.get('sid') || '' }));
|
|
565
567
|
if (url.pathname === '/api/board') return json(boardRows());
|
|
566
|
-
if (url.pathname === '/api/characters') return json(listCharacters());
|
|
568
|
+
if (url.pathname === '/api/characters') return json(listCharacters(url.searchParams.get('pack') || ''));
|
|
569
|
+
if (url.pathname === '/api/packs') return json(Object.keys(PACKS).map((name) => ({ name, n: listCharacters(name).chars.length })));
|
|
567
570
|
if (url.pathname === '/api/media') {
|
|
568
571
|
const q = (k) => url.searchParams.get(k) || '', SAFE = /^[\w.\-]+$/, n = +q('n') || 0, bad = [q('proj'), q('id'), q('agent'), q('tool')].some((v) => !SAFE.test(v) || v === '..' || v.length > 200);
|
|
569
572
|
const file = bad || !Number.isInteger(n) || n < 0 || n > 9 ? null : mediaFile(q('proj'), q('id'), q('agent')), im = file && toolImage(file, q('tool'), n);
|
|
@@ -585,9 +588,10 @@ function start(port = PORT) {
|
|
|
585
588
|
}
|
|
586
589
|
return json({ id: tl.id, agents: tl.agents, total: tl.events.length, events: tl.events.slice(+after) });
|
|
587
590
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
+
const pk = url.pathname.match(/^\/packs\/([a-z]+)\/(.*)$/);
|
|
592
|
+
if (url.pathname.startsWith('/characters/') || (pk && packDir(pk[1]))) { // only plain image files of the characters folder (or of a built-in pack), by exact name
|
|
593
|
+
let f = ''; try { f = decodeURIComponent(pk ? pk[2] : url.pathname.slice('/characters/'.length)); } catch {}
|
|
594
|
+
const dir = pk ? packDir(pk[1]) : charactersDir(), fp = path.join(dir, f);
|
|
591
595
|
try { const ls = fs.lstatSync(fp); if (!CHAR_NAME.test(f) || !fp.startsWith(dir + path.sep) || !ls.isFile() || ls.size > CHAR_MAX) throw 0; } catch { res.writeHead(404); return res.end('not found'); }
|
|
592
596
|
res.writeHead(200, { 'content-type': CHAR_TYPES[f.split('.').pop().toLowerCase()] || 'application/octet-stream', 'cache-control': 'no-cache' }); return fs.createReadStream(fp).pipe(res);
|
|
593
597
|
}
|