@double_geste/agent-standup 1.25.2 → 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.
Files changed (38) hide show
  1. package/examples/cats/README.txt +7 -0
  2. package/examples/cats/biscuit-talk.png +0 -0
  3. package/examples/cats/biscuit.png +0 -0
  4. package/examples/cats/cinnamon-talk.png +0 -0
  5. package/examples/cats/cinnamon.png +0 -0
  6. package/examples/cats/earl-grey-talk.png +0 -0
  7. package/examples/cats/earl-grey.png +0 -0
  8. package/examples/cats/lead-talk.png +0 -0
  9. package/examples/cats/lead.png +0 -0
  10. package/examples/cats/miso-talk.png +0 -0
  11. package/examples/cats/miso.png +0 -0
  12. package/examples/cats/mochi-talk.png +0 -0
  13. package/examples/cats/mochi.png +0 -0
  14. package/examples/cats/oreo-talk.png +0 -0
  15. package/examples/cats/oreo.png +0 -0
  16. package/examples/cats/patches-talk.png +0 -0
  17. package/examples/cats/patches.png +0 -0
  18. package/examples/cats/salem-talk.png +0 -0
  19. package/examples/cats/salem.png +0 -0
  20. package/examples/cats/smokey-talk.png +0 -0
  21. package/examples/cats/smokey.png +0 -0
  22. package/examples/cats/you-talk.png +0 -0
  23. package/examples/cats/you.png +0 -0
  24. package/examples/characters/README.txt +5 -0
  25. package/examples/characters/buddy.png +0 -0
  26. package/examples/characters/lead-talk.png +0 -0
  27. package/examples/characters/lead.png +0 -0
  28. package/package.json +2 -1
  29. package/public/index.html +20 -8
  30. package/public/locales/da.json +8 -1
  31. package/public/locales/de.json +8 -1
  32. package/public/locales/en.json +7 -0
  33. package/public/locales/es.json +8 -1
  34. package/public/locales/ja.json +8 -1
  35. package/public/locales/pt-BR.json +8 -1
  36. package/public/locales/zh-CN.json +8 -1
  37. package/public/locales/zh-TW.json +8 -1
  38. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@double_geste/agent-standup",
3
- "version": "1.25.2",
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
- const next = { lead: null, you: null, pool: [], dir: '', n: 0 };
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('/characters/' + encodeURIComponent(c.file)), talk: c.talk ? await toData('/characters/' + encodeURIComponent(c.talk)) : null };
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) => {
@@ -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, FR() && trMem.has(baseText(e))); if (v) TTS.buf(speakable(spoken(e)), v, TTS.rate()).catch(() => {});
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
- async function speakLine(text, p, primary, token, fr = FR()) {
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));
@@ -1641,7 +1652,7 @@ async function run(i) {
1641
1652
  if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
1642
1653
  if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
1643
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 }; }
1644
- return speakLine(said, S.agents[g.agent], true, token, FR() && trMem.has(baseText(g))).then(() => stopTalk(g.agent));
1655
+ return speakLine(said, S.agents[g.agent], true, token, frVoice(said)).then(() => stopTalk(g.agent));
1645
1656
  };
1646
1657
  let plays;
1647
1658
  if (ev.kind === 'order' && group.length > 1) { // the first order in full, then "Also Remy on X, Iris on Y"
@@ -1930,6 +1941,7 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
1930
1941
 
1931
1942
  $('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
1932
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(); };
1933
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(); };
1934
1946
  $('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
1935
1947
  $('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
@@ -209,5 +209,12 @@
209
209
  "Laptop in front of your own characters": "Bærbar foran dine egne figurer",
210
210
  "Also ": "Også ",
211
211
  " on ": " på ",
212
- "To sum up: ": "Kort sagt: "
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."
213
220
  }
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "Laptop vor deinen eigenen Figuren",
211
211
  "Also ": "Außerdem ",
212
212
  " on ": " an ",
213
- "To sum up: ": "Zusammengefasst: "
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."
214
221
  }
@@ -27,7 +27,9 @@
27
27
  "cache write": "cache write",
28
28
  "Call": "Call",
29
29
  "Cartoon voices (real voice)": "Cartoon voices (real voice)",
30
+ "Cat pack": "Cat pack",
30
31
  "Characters": "Characters",
32
+ "characters from the pack": "characters from the pack",
31
33
  "Chatter between agents": "Chatter between agents",
32
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.",
33
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.",
@@ -48,6 +50,7 @@
48
50
  "Done": "Done",
49
51
  "Done. ": "Done. ",
50
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",
51
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:",
52
55
  "duration": "duration",
53
56
  "Editing ": "Editing ",
@@ -93,6 +96,7 @@
93
96
  "may be waiting for your approval": "may be waiting for your approval",
94
97
  "Music volume": "Music volume",
95
98
  "Mute / unmute": "Mute / unmute",
99
+ "My folder": "My folder",
96
100
  "Natural voices": "Natural voices",
97
101
  "need you": "need you",
98
102
  "needs you?": "needs you?",
@@ -163,9 +167,11 @@
163
167
  "stalled: a sub-agent has been silent for a long time": "stalled: a sub-agent has been silent for a long time",
164
168
  "standing by": "standing by",
165
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",
166
171
  "sub-agents active": "sub-agents active",
167
172
  "task": "task",
168
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.",
169
175
  "THE END": "THE END",
170
176
  "The engine stopped: system voices are used instead.": "The engine stopped: system voices are used instead.",
171
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? ☕",
@@ -173,6 +179,7 @@
173
179
  "Time": "Time",
174
180
  "to": "to",
175
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”.",
176
183
  "tokens": "tokens",
177
184
  "Tokens": "Tokens",
178
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)",
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "Portátil delante de tus propios personajes",
211
211
  "Also ": "También ",
212
212
  " on ": " en ",
213
- "To sum up: ": "En resumen: "
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."
214
221
  }
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "自作キャラクターの前にノートパソコン",
211
211
  "Also ": "それから ",
212
212
  " on ": " は ",
213
- "To sum up: ": "まとめると: "
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.": "アプリが描画するキャラクターです。自分の画像を使うには「自分のフォルダー」、またはパックを選んでください。"
214
221
  }
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "Notebook na frente dos seus personagens",
211
211
  "Also ": "Também ",
212
212
  " on ": " em ",
213
- "To sum up: ": "Resumindo: "
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."
214
221
  }
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "在自定义角色前显示笔记本电脑",
211
211
  "Also ": "另外 ",
212
212
  " on ": " 负责 ",
213
- "To sum up: ": "总结一下: "
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.": "由应用绘制的角色。选择“我的文件夹”使用你自己的图片,或选择一个角色包。"
214
221
  }
@@ -210,5 +210,12 @@
210
210
  "Laptop in front of your own characters": "在自訂角色前顯示筆記型電腦",
211
211
  "Also ": "另外 ",
212
212
  " on ": " 負責 ",
213
- "To sum up: ": "總結一下: "
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.": "由應用程式繪製的角色。選擇「我的資料夾」使用你自己的圖片,或選擇一個角色包。"
214
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
- function listCharacters() {
507
- const dir = charactersDir(); let names = []; try { names = fs.readdirSync(dir); } catch {}
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
- if (url.pathname.startsWith('/characters/')) { // only plain image files of the characters folder, by exact name
589
- let f = ''; try { f = decodeURIComponent(url.pathname.slice('/characters/'.length)); } catch {}
590
- const dir = charactersDir(), fp = path.join(dir, f);
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
  }