@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.
- 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 +20 -8
- package/public/locales/da.json +8 -1
- package/public/locales/de.json +8 -1
- package/public/locales/en.json +7 -0
- package/public/locales/es.json +8 -1
- package/public/locales/ja.json +8 -1
- package/public/locales/pt-BR.json +8 -1
- package/public/locales/zh-CN.json +8 -1
- package/public/locales/zh-TW.json +8 -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) => {
|
|
@@ -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));
|
|
@@ -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,
|
|
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); };
|
package/public/locales/da.json
CHANGED
|
@@ -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
|
}
|
package/public/locales/de.json
CHANGED
|
@@ -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
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -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)",
|
package/public/locales/es.json
CHANGED
|
@@ -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
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|