@7ots/cli 0.1.7 → 0.1.9

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 (57) hide show
  1. package/README.md +1 -1
  2. package/cli/7ots.mjs +28 -4
  3. package/cli/lib/account.mjs +152 -2
  4. package/cli/lib/assistant.mjs +106 -41
  5. package/cli/lib/brain.mjs +13 -2
  6. package/cli/lib/config.mjs +3 -0
  7. package/cli/lib/inferences.mjs +30 -6
  8. package/cli/lib/integrations.mjs +69 -5
  9. package/cli/lib/orquesta.mjs +44 -1
  10. package/cli/lib/pet-server.mjs +344 -11
  11. package/cli/pet/electron/director.cjs +37 -5
  12. package/cli/pet/electron/main.cjs +92 -10
  13. package/cli/pet/pet.html +149 -10
  14. package/cli/pet/settings.html +545 -42
  15. package/dist/7ots.esm.js +216 -137
  16. package/dist/7ots.esm.js.map +4 -4
  17. package/dist/7ots.iife.js +216 -137
  18. package/dist/7ots.iife.js.map +4 -4
  19. package/package.json +1 -1
  20. package/server/channels/agent.mjs +7 -1
  21. package/server/channels/index.mjs +3 -2
  22. package/server/platform/connect.mjs +4 -1
  23. package/server/platform/db.mjs +9 -1
  24. package/server/platform/device.mjs +101 -4
  25. package/server/platform/mods.mjs +222 -0
  26. package/server/platform/orquesta.mjs +78 -14
  27. package/server/platform/profile.mjs +161 -0
  28. package/server/platform/routes.mjs +38 -33
  29. package/server/platform/runtime.mjs +3 -1
  30. package/server/platform/store.mjs +1 -1
  31. package/server/platform/vm.mjs +370 -91
  32. package/server/server.mjs +12 -2
  33. package/src/avatar/three.js +3 -2
  34. package/src/character/AvatarEditor.js +95 -7
  35. package/src/character/Character.js +116 -17
  36. package/src/character/TradingCard.js +226 -0
  37. package/src/character/mods/index.js +4 -0
  38. package/src/character/mods/orc.js +64 -0
  39. package/src/character/mods.js +251 -0
  40. package/src/character/more/acc-head.js +213 -0
  41. package/src/character/more/acc-rest.js +342 -0
  42. package/src/character/more/brows.js +39 -0
  43. package/src/character/more/cheeks.js +78 -0
  44. package/src/character/more/eyes.js +288 -0
  45. package/src/character/more/mouths.js +259 -0
  46. package/src/character/more/patterns.js +120 -0
  47. package/src/character/parts.js +90 -0
  48. package/src/character/skin.js +166 -1
  49. package/src/character/styles.js +1285 -9
  50. package/src/character3d/Character3D.js +148 -35
  51. package/src/character3d/index.js +7 -1
  52. package/src/character3d/model.js +367 -0
  53. package/src/i18n/messages/character.js +34 -22
  54. package/src/i18n/messages/platform.js +15 -0
  55. package/src/identity/random.js +17 -3
  56. package/src/identity/schema.js +10 -0
  57. package/src/index.js +1 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Perfil público de un ot: lo que ve una persona al abrir /api/o/<id>/identity en el navegador.
3
+ *
4
+ * Portada con el personaje (SVG renderizado aquí, así la primera pintura y la vista previa ya lo
5
+ * traen; luego el navegador lo anima), quién es, sus rasgos únicos (los mismos de traits.json) y la
6
+ * tarjeta de contacto <ots-identity> con sus acciones (escuchar, correo, @handle, llamada, vCard).
7
+ */
8
+ import { renderCharacter } from '../../src/character/Character.js';
9
+
10
+ const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
11
+ const hex = (v, def) => (/^#[0-9a-f]{3,8}$/i.test(v || '') ? v : def);
12
+
13
+ const L = {
14
+ en: { about: 'About', tone: 'Tone', speaks: 'Speaks', traits: 'Unique traits', traitsSub: 'No two ots share this face. The fingerprint is what makes it one of a kind.', print: 'Fingerprint', contact: 'Contact', json: 'JSON', make: 'Create your own ot', unique: 'One of a kind', soon: 'NFT soon', noBio: '{name} hasn’t written a bio yet.' },
15
+ es: { about: 'Sobre mí', tone: 'Tono', speaks: 'Habla', traits: 'Rasgos únicos', traitsSub: 'No hay dos ots con esta cara. La huella es lo que lo hace único.', print: 'Huella', contact: 'Contacto', json: 'JSON', make: 'Crea tu propio ot', unique: 'Único', soon: 'NFT pronto', noBio: '{name} todavía no escribió su bio.' },
16
+ pt: { about: 'Sobre', tone: 'Tom', speaks: 'Fala', traits: 'Traços únicos', traitsSub: 'Não existem dois ots com este rosto. A impressão é o que o torna único.', print: 'Impressão', contact: 'Contato', json: 'JSON', make: 'Crie seu próprio ot', unique: 'Único', soon: 'NFT em breve', noBio: '{name} ainda não escreveu a bio.' },
17
+ };
18
+
19
+ const LANG_NAMES = { es: 'Español', en: 'English', pt: 'Português', fr: 'Français', de: 'Deutsch', it: 'Italiano', ja: '日本語', zh: '中文', ko: '한국어', ru: 'Русский', ar: 'العربية', hi: 'हिन्दी', nl: 'Nederlands', pl: 'Polski', tr: 'Türkçe', et: 'Eesti' };
20
+
21
+ /** 32 hex → código de barras de colores: la huella se ve, no solo se lee. */
22
+ function printBars(print) {
23
+ if (!print) return '';
24
+ const bars = print.match(/../g).map((b, i) => {
25
+ const n = parseInt(b, 16);
26
+ return `<rect x="${i * 16 + 2}" y="${n % 3 === 0 ? 4 : 0}" width="${4 + (n % 8)}" height="${n % 3 === 0 ? 28 : 32}" rx="1.5" fill="hsl(${Math.round((n / 255) * 360)} 80% 65%)"/>`;
27
+ });
28
+ return `<svg class="bars" viewBox="0 0 256 32" preserveAspectRatio="none" aria-hidden="true">${bars.join('')}</svg>`;
29
+ }
30
+
31
+ /**
32
+ * card: publicIdentity(...) · meta: traitsMeta(...) · ep: base /api/o/<id> · locale: idioma del visitante.
33
+ */
34
+ export function profilePage(card, meta, ep, locale = 'en') {
35
+ const l = L[String(locale).slice(0, 2)] || L.en;
36
+ const name = esc(card.name);
37
+ const desc = esc(card.tagline || card.role || '');
38
+ const c1 = hex(card.look?.color, '#6d5dfc');
39
+ const c2 = hex(card.look?.accent, '#22d3ee');
40
+ const look = card.look || {};
41
+ let portrait = `<span class="initial">${esc((look.emoji || card.name.slice(0, 1)).toUpperCase())}</span>`;
42
+ if (look.kind === 'image' && look.image) portrait = `<img src="${esc(look.image)}" alt="" referrerpolicy="no-referrer">`;
43
+ else if (look.character && Object.keys(look.character).length) {
44
+ try {
45
+ portrait = renderCharacter(look.character, {}, { title: card.name });
46
+ } catch {}
47
+ }
48
+ const langs = [...new Set([card.language, ...(card.languages || [])].filter(Boolean))];
49
+ const traits = card.personality?.traits || [];
50
+ const handle = meta?.handle;
51
+ const attrs = (meta?.attributes || []).filter((a) => a.trait_type !== 'Kind');
52
+ const tile = (a) => {
53
+ const v = String(a.value);
54
+ const sw = /^#[0-9a-f]{3,8}$/i.test(v) ? `<i class="sw" style="background:${v}"></i>` : '';
55
+ return `<div class="tile"><dt>${esc(a.trait_type)}</dt><dd>${sw}${esc(sw ? v.toUpperCase() : v.replace(/[-_]/g, ' '))}</dd></div>`;
56
+ };
57
+ const jsonUrl = `${esc(ep)}/identity?format=json`;
58
+
59
+ return `<!doctype html><html lang="${esc(card.language || 'en')}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
60
+ <title>${name} · 7ots</title><meta name="description" content="${desc}">
61
+ <meta property="og:title" content="${name} · 7ots"><meta property="og:description" content="${desc}"><meta property="og:type" content="profile">
62
+ <meta name="theme-color" content="${c1}">
63
+ <link rel="alternate" type="application/json" href="${jsonUrl}"><link rel="icon" href="/favicon.ico">
64
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Inter:wght@400;500;600&display=swap">
65
+ <style>
66
+ :root{color-scheme:dark;--c1:${c1};--c2:${c2};--bg:#0b0919;--panel:rgb(255 255 255/.045);--line:rgb(255 255 255/.09);--ink:#f3f1ff;--muted:#a7a2c9;--disp:"Bricolage Grotesque",system-ui,sans-serif}
67
+ *{box-sizing:border-box}html,body{margin:0}
68
+ body{min-height:100vh;font:15px/1.55 Inter,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg);overflow-x:hidden}
69
+ body::before{content:"";position:fixed;inset:-20%;z-index:-1;background:radial-gradient(700px 520px at 20% 8%,color-mix(in srgb,var(--c1) 42%,transparent),transparent 65%),radial-gradient(640px 520px at 92% 30%,color-mix(in srgb,var(--c2) 26%,transparent),transparent 65%),radial-gradient(900px 600px at 50% 120%,color-mix(in srgb,var(--c1) 22%,transparent),transparent 60%)}
70
+ a{color:inherit}
71
+ .top{max-width:1080px;margin:0 auto;padding:18px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px}
72
+ .brand{display:flex;align-items:center;gap:8px;font:800 17px var(--disp);text-decoration:none;letter-spacing:-.02em}
73
+ .brand img{width:26px;height:26px;border-radius:7px}
74
+ .cta{font:600 13px Inter,sans-serif;text-decoration:none;padding:8px 14px;border-radius:999px;background:var(--ink);color:#120f2a}
75
+ .cta:hover{opacity:.9}
76
+ main{max-width:1080px;margin:0 auto;padding:8px 20px 56px;display:grid;gap:22px}
77
+ .hero{display:grid;grid-template-columns:minmax(0,300px) minmax(0,1fr);gap:36px;align-items:center;padding:28px;border:1px solid var(--line);border-radius:28px;background:linear-gradient(140deg,color-mix(in srgb,var(--c1) 18%,transparent),rgb(255 255 255/.02) 55%);backdrop-filter:blur(6px)}
78
+ .stage{position:relative;aspect-ratio:1;border-radius:24px;display:grid;place-items:center;background:radial-gradient(circle at 50% 42%,color-mix(in srgb,var(--c2) 30%,transparent),transparent 62%),rgb(0 0 0/.18);overflow:hidden}
79
+ .stage::after{content:"";position:absolute;left:18%;right:18%;bottom:9%;height:7%;border-radius:50%;background:rgb(0 0 0/.35);filter:blur(10px)}
80
+ .stage>.av{position:relative;z-index:1;width:82%;height:82%;display:grid;place-items:center}
81
+ .stage svg{width:100%;height:100%;overflow:visible}
82
+ .stage img{width:100%;height:100%;object-fit:cover;border-radius:18px}
83
+ .initial{font:800 120px var(--disp);color:var(--c1)}
84
+ .who{display:grid;gap:12px;align-content:center;min-width:0}
85
+ .badges{display:flex;flex-wrap:wrap;gap:8px}
86
+ .badge{display:inline-flex;align-items:center;gap:6px;font:600 12px Inter,sans-serif;padding:5px 10px;border-radius:999px;border:1px solid var(--line);background:rgb(255 255 255/.05);color:var(--muted)}
87
+ .badge.u{color:var(--ink);border-color:color-mix(in srgb,var(--c2) 55%,transparent);background:color-mix(in srgb,var(--c2) 14%,transparent)}
88
+ .badge svg{width:13px;height:13px}
89
+ h1{margin:0;font:800 clamp(40px,7vw,68px)/1 var(--disp);letter-spacing:-.035em;overflow-wrap:anywhere}
90
+ .handle{font:600 16px Inter,sans-serif;color:var(--c2)}
91
+ .role{margin:0;font-size:18px;color:var(--muted);text-transform:capitalize}
92
+ .tagline{margin:0;font:600 21px/1.35 var(--disp);letter-spacing:-.01em;max-width:34ch}
93
+ .grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,380px);gap:22px;align-items:start}
94
+ .col{display:grid;gap:22px;min-width:0}
95
+ .panel{border:1px solid var(--line);border-radius:22px;background:var(--panel);padding:22px;display:grid;gap:14px}
96
+ .panel h2{margin:0;font:700 13px Inter,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;justify-content:space-between;gap:8px}
97
+ .panel h2 small{font:600 11px Inter,sans-serif;letter-spacing:0;text-transform:none;padding:3px 8px;border-radius:999px;background:rgb(255 255 255/.07)}
98
+ .bio{margin:0;font-size:16px;white-space:pre-line}
99
+ .bio.empty{color:var(--muted);font-style:italic}
100
+ .kv{display:grid;grid-template-columns:auto 1fr;gap:10px 16px;margin:0;align-items:baseline}
101
+ .kv dt{color:var(--muted);font-size:13px}
102
+ .kv dd{margin:0}
103
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}
104
+ .chips li{font-size:13px;padding:4px 10px;border-radius:999px;background:rgb(255 255 255/.07);border:1px solid var(--line)}
105
+ .sub{margin:-6px 0 0;color:var(--muted);font-size:13px}
106
+ .tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin:0}
107
+ .tile{border:1px solid var(--line);border-radius:14px;padding:11px 13px;background:rgb(0 0 0/.15)}
108
+ .tile dt{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
109
+ .tile dd{margin:3px 0 0;font-weight:600;text-transform:capitalize;display:flex;align-items:center;gap:7px;overflow-wrap:anywhere}
110
+ .sw{width:14px;height:14px;border-radius:5px;flex:none;box-shadow:inset 0 0 0 1px rgb(255 255 255/.25)}
111
+ .print{display:grid;gap:8px;padding:14px;border-radius:14px;background:rgb(0 0 0/.25);border:1px solid var(--line)}
112
+ .print span{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
113
+ .print code{font:500 12.5px ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere;color:var(--ink)}
114
+ .bars{width:100%;height:30px}
115
+ ots-identity{display:block;--ots-bg:#16132e;--ots-fg:#f3f1ff;--ots-line:rgb(255 255 255/.1)}
116
+ .links{display:flex;gap:16px;flex-wrap:wrap;font-size:13px;color:var(--muted);justify-content:center}
117
+ .links a{text-decoration:none}.links a:hover{color:var(--ink)}
118
+ @media (max-width:820px){.hero{grid-template-columns:1fr;gap:22px;padding:20px;text-align:center}.stage{max-width:280px;width:100%;margin:0 auto}.badges,.who{justify-items:center;justify-content:center}.tagline{margin-inline:auto}.grid{grid-template-columns:1fr}}
119
+ @media (max-width:480px){main,.top{padding-inline:16px}.panel{padding:18px}}
120
+ @media (prefers-reduced-motion:no-preference){.stage>.av{animation:float 5s ease-in-out infinite}@keyframes float{50%{transform:translateY(-6px)}}}
121
+ </style></head><body>
122
+ <header class="top"><a class="brand" href="https://7ots.com"><img src="/brand/icon-512.png" alt="">7ots</a><a class="cta" href="https://7ots.com">${l.make}</a></header>
123
+ <main>
124
+ <section class="hero">
125
+ <div class="stage"><div class="av" id="av">${portrait}</div></div>
126
+ <div class="who">
127
+ <div class="badges">${meta?.fingerprint ? `<span class="badge u"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9z"/></svg>${l.unique}</span>` : ''}${langs.map((x) => `<span class="badge">${esc(LANG_NAMES[x] || x.toUpperCase())}</span>`).join('')}</div>
128
+ <h1>${name}</h1>
129
+ ${handle ? `<span class="handle">@${esc(handle)}</span>` : ''}
130
+ ${card.role ? `<p class="role">${esc(card.role)}</p>` : ''}
131
+ ${card.tagline ? `<p class="tagline">“${esc(card.tagline)}”</p>` : ''}
132
+ </div>
133
+ </section>
134
+ <div class="grid">
135
+ <div class="col">
136
+ <section class="panel"><h2>${l.about}</h2>
137
+ ${card.bio ? `<p class="bio">${esc(card.bio)}</p>` : `<p class="bio empty">${esc(l.noBio.replace('{name}', card.name))}</p>`}
138
+ <dl class="kv">
139
+ ${card.personality?.tone ? `<dt>${l.tone}</dt><dd>${esc(card.personality.tone)}</dd>` : ''}
140
+ ${langs.length ? `<dt>${l.speaks}</dt><dd>${esc(langs.map((x) => LANG_NAMES[x] || x).join(' · '))}</dd>` : ''}
141
+ </dl>
142
+ ${traits.length ? `<ul class="chips">${traits.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : ''}
143
+ </section>
144
+ ${attrs.length || meta?.fingerprint ? `<section class="panel"><h2>${l.traits}<small>${l.soon}</small></h2><p class="sub">${l.traitsSub}</p>
145
+ ${meta?.fingerprint ? `<div class="print"><span>${l.print}</span>${printBars(meta.fingerprint)}<code>${esc(meta.fingerprint)}</code></div>` : ''}
146
+ <dl class="tiles">${attrs.map(tile).join('')}</dl>
147
+ </section>` : ''}
148
+ </div>
149
+ <div class="col">
150
+ <section class="panel"><h2>${l.contact}</h2><ots-identity src="${jsonUrl}" face lang="${esc(String(locale).slice(0, 2))}"></ots-identity></section>
151
+ <p class="links"><a href="${jsonUrl}">${l.json}</a><a href="${esc(ep)}/traits.json">traits.json</a><a href="${esc(ep)}/identity.vcf">vCard</a><a href="https://7ots.com">7ots.com</a></p>
152
+ </div>
153
+ </div>
154
+ </main>
155
+ <script src="/api/agent/7ots.js"></script>
156
+ <script>
157
+ (function(){var c=${JSON.stringify(look.kind !== 'image' && look.character ? look.character : null).replace(/</g, '\\u003c')};var el=document.getElementById('av');
158
+ if(!c||!window.SevenOts||!SevenOts.createCharacter)return;try{el.innerHTML='';SevenOts.createCharacter(el,c,{title:${JSON.stringify(card.name).replace(/</g, '\\u003c')}});}catch(e){}})();
159
+ </script>
160
+ </body></html>`;
161
+ }
@@ -14,6 +14,7 @@
14
14
  * GET /api/platform/ots/:id/usage
15
15
  * * /api/platform/ots/:id/admin/* misma forma que /api/agent/admin/* (el backoffice
16
16
  * edita un ots con ?ots=<id>)
17
+ * * /api/mods, /api/platform/mods/*, /api/device/mods/* community mods (mods.mjs)
17
18
  * GET /api/platform/sites POST { kind, url, name, guide } · POST …/:id/verify · DELETE …/:id (sites.mjs)
18
19
  * GET /api/platform/device/:code · POST { approve } GET /api/platform/devices · DELETE …/:id
19
20
  *
@@ -25,9 +26,11 @@
25
26
  * apuchat identity and Orquesta tasks (…/apuchat/*, …/orquesta/*, /auth/apuchat|orquesta): see connect.mjs
26
27
  */
27
28
 
29
+ import { createReadStream, existsSync, statSync } from 'node:fs';
30
+ import { join } from 'node:path';
28
31
  import { MEET_AVATARS, MEET_SCENES, OPENAI_VOICES, GROK_VOICES, defineIdentity, identityPrompt, publicIdentity, toVCard } from '../identity.mjs';
29
32
  import { errorText, reqT, requestLocale } from '../i18n.mjs';
30
- import { monthOf } from './db.mjs';
33
+ import { dataDir, monthOf } from './db.mjs';
31
34
  import { randomIdentity, cleanSeed } from '../../src/identity/random.js';
32
35
  import { currentAccount } from './auth.mjs';
33
36
  import {
@@ -39,6 +42,8 @@ import { createConnect } from './connect.mjs';
39
42
  import { createDevice, deviceFromBearer } from './device.mjs';
40
43
  import { createVm } from './vm.mjs';
41
44
  import { createSites } from './sites.mjs';
45
+ import { profilePage } from './profile.mjs';
46
+ import { createMods } from './mods.mjs';
42
47
  import '../../src/i18n/messages/platform.js';
43
48
 
44
49
  const penv = process.env;
@@ -119,7 +124,8 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
119
124
  res.setHeader('Vary', 'Accept');
120
125
  if (/text\/html/.test(req.headers.accept || '') && !/[?&]format=json\b/.test(req.url)) {
121
126
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
122
- return res.end(profilePage(card, `${originOf(req)}/api/o/${ots.id}`));
127
+ const ep = `${originOf(req)}/api/o/${ots.id}`;
128
+ return res.end(profilePage(card, traitsMeta(ots, ep), ep, requestLocale(req)));
123
129
  }
124
130
  return send(res, 200, card);
125
131
  }
@@ -325,6 +331,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
325
331
  if ((await connect.account(req, res, account, path)) !== false) return;
326
332
  if (await device.dashboard(req, res, account, path)) return;
327
333
  if (await sites(req, res, account, path)) return;
334
+ if ((path === '/mods' || path.startsWith('/mods/')) && (await mods.accountApi(req, res, account, path.slice('/mods'.length)))) return;
328
335
 
329
336
  if (path === '/ots' && req.method === 'GET') {
330
337
  const usage = usageByOts(account.id);
@@ -403,14 +410,25 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
403
410
  }
404
411
 
405
412
  const connect = createConnect({ send, readJson, originOf, save });
406
- const device = createDevice({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, ''), summary: (o) => otsSummary(o) });
413
+ const device = createDevice({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, ''), summary: (o) => otsSummary(o), save });
407
414
  const sites = createSites({ send, readJson });
415
+ const mods = createMods({ send, readJson });
408
416
  const vm = createVm({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, '') });
409
417
 
410
418
  /** @returns {Promise<boolean>} true si atendió la ruta */
411
419
  return async function handle(req, res, url) {
412
420
  const p = url.pathname;
413
421
  if (await connect.auth(req, res, url)) return true;
422
+ // models of mods (uploaded by a pet when it syncs): public, content-addressed, immutable
423
+ const mf = /^\/mods\/([a-f0-9]{16})\.(glb|vrm)$/.exec(p);
424
+ if (mf && (req.method === 'GET' || req.method === 'HEAD')) {
425
+ const file = join(dataDir(), 'mods', `${mf[1]}.${mf[2]}`);
426
+ if (!existsSync(file)) return send(res, 404, { error: 'not found' }), true;
427
+ res.writeHead(200, { 'Content-Type': 'model/gltf-binary', 'Content-Length': statSync(file).size, 'Cache-Control': 'public, max-age=31536000, immutable', 'Access-Control-Allow-Origin': '*' });
428
+ if (req.method === 'HEAD') return res.end(), true;
429
+ createReadStream(file).pipe(res);
430
+ return true;
431
+ }
414
432
  const m = p.match(/^\/api\/o\/([a-z0-9_]{4,40})(\/.*)$/);
415
433
  if (m) {
416
434
  await publicApi(req, res, m[1], m[2]);
@@ -418,9 +436,12 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
418
436
  }
419
437
  // The landing asks before sending the visitor to sign in: each ot is unique, a repeated face can't be created.
420
438
  if (p === '/api/look/free' && req.method === 'POST') {
421
- const { character } = await readJson(req, 64 * 1024);
422
- const ok = character && typeof character === 'object' && !Array.isArray(character);
423
- send(res, 200, { free: ok ? !lookTaken({ look: { kind: 'character', character } }) : false });
439
+ const { character, characters } = await readJson(req, 256 * 1024);
440
+ const isObj = (c) => c && typeof c === 'object' && !Array.isArray(c);
441
+ const free = (c) => (isObj(c) ? !lookTaken({ look: { kind: 'character', character: c } }) : false);
442
+ // en lote (las sugerencias al azar del editor): { characters: [...] } → { free: [bool…] }
443
+ if (Array.isArray(characters)) send(res, 200, { free: characters.slice(0, 32).map(free) });
444
+ else send(res, 200, { free: free(character) });
424
445
  return true;
425
446
  }
426
447
  const dv = p.match(/^\/api\/device\/ots\/([a-z0-9_]{4,40})\/vm(\/.*)?$/);
@@ -430,6 +451,17 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
430
451
  else await vm.device(req, res, who, dv[1], dv[2] || '');
431
452
  return true;
432
453
  }
454
+ // community mods: public listing, and the same signed-in api for the pet (bearer) as for the dashboard
455
+ if (p === '/api/mods' || p.startsWith('/api/mods/')) {
456
+ if (!(await mods.publicApi(req, res, p.slice('/api/mods'.length)))) send(res, 404, { error: 'not found' });
457
+ return true;
458
+ }
459
+ if (p === '/api/device/mods' || p.startsWith('/api/device/mods/')) {
460
+ const who = deviceFromBearer(req);
461
+ if (!who) send(res, 401, { error: 'login' });
462
+ else if (!(await mods.accountApi(req, res, who.account, p.slice('/api/device/mods'.length)))) send(res, 404, { error: 'not found' });
463
+ return true;
464
+ }
433
465
  if (p.startsWith('/api/vm/')) {
434
466
  await vm.worker(req, res, p.slice('/api/vm'.length));
435
467
  return true;
@@ -476,33 +508,6 @@ function sanitizeWidget(w) {
476
508
  return out;
477
509
  }
478
510
 
479
- const htmlEsc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
480
- /** Public profile of an ot (what /api/o/<id>/identity shows in a browser): its contact card with the live face. */
481
- function profilePage(card, ep) {
482
- const name = htmlEsc(card.name);
483
- const desc = htmlEsc(card.tagline || card.role || '');
484
- const lang = htmlEsc(card.language || 'en');
485
- const c1 = /^#[0-9a-f]{3,8}$/i.test(card.look?.color || '') ? card.look.color : '#6d5dfc';
486
- const c2 = /^#[0-9a-f]{3,8}$/i.test(card.look?.accent || '') ? card.look.accent : '#22d3ee';
487
- return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
488
- <title>${name} · 7ots</title><meta name="description" content="${desc}">
489
- <meta property="og:title" content="${name} · 7ots"><meta property="og:description" content="${desc}"><meta property="og:type" content="profile">
490
- <link rel="alternate" type="application/json" href="${htmlEsc(ep)}/identity?format=json"><link rel="icon" href="/favicon.ico">
491
- <style>
492
- :root{color-scheme:dark;--c1:${c1};--c2:${c2}}
493
- *{box-sizing:border-box}html,body{margin:0;min-height:100%}
494
- body{min-height:100vh;display:grid;place-items:center;padding:32px 16px;font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:#ece9ff;
495
- background:radial-gradient(900px 600px at 15% 0%,color-mix(in srgb,var(--c1) 35%,transparent),transparent 60%),radial-gradient(800px 600px at 100% 100%,color-mix(in srgb,var(--c2) 22%,transparent),transparent 60%),#0d0a24}
496
- main{width:min(440px,100%);display:grid;gap:16px}
497
- ots-identity{display:block}
498
- .foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:12px;color:#a9a3d6}
499
- .foot a{color:inherit}
500
- </style></head><body><main>
501
- <ots-identity src="${htmlEsc(ep)}/identity?format=json" face></ots-identity>
502
- <p class="foot"><a href="${htmlEsc(ep)}/identity?format=json">JSON</a><a href="${htmlEsc(ep)}/identity.vcf">vCard</a><a href="https://7ots.com">7ots.com</a></p>
503
- </main><script src="/api/agent/7ots.js"></script></body></html>`;
504
- }
505
-
506
511
  /**
507
512
  * The ot's unique traits as NFT-style metadata (ERC-721 / Metaplex shape). The face fingerprint is
508
513
  * what makes an ot unique (UNIQUE index); the permanent apuchat @handle is its unique name. When
@@ -19,7 +19,7 @@ import { startAgentChannels } from '../channels/index.mjs';
19
19
  import { apumailConfigFromEnv } from '../channels/apumail.mjs';
20
20
  import { apuchatConfigFromEnv } from '../channels/apuchat.mjs';
21
21
  import { i18nError } from '../i18n.mjs';
22
- import { OTS_KEYS, accountUsage, addUsage, allLiveOts, getOts } from './store.mjs';
22
+ import { OTS_KEYS, accountUsage, addUsage, allLiveOts, getOts, logTurn } from './store.mjs';
23
23
  import { orquestaTools } from './orquesta.mjs';
24
24
 
25
25
  const penv = process.env;
@@ -213,6 +213,8 @@ export function otsChannels(ots) {
213
213
  log: (...a) => console.log(`[7ots:${id}]`, ...a),
214
214
  // Orquesta tasks: only for senders on the ot's own list (see orquesta.mjs), read on every message.
215
215
  tools: (ctx) => orquestaTools(current(), ctx),
216
+ // mails and DMs go to its conversations (dashboard, and the desktop ot through /api/device/…/inbox)
217
+ onTurn: ({ key, channel, user, reply }) => logTurn(current(), { key, channel: channel === 'dm' ? 'apuchat' : 'apumail', visitor: key.replace(/^\w+:/, ''), user, reply }),
216
218
  });
217
219
  channels.set(id, { stamp, ch });
218
220
  return ch;
@@ -20,7 +20,7 @@ const parse = (s, fallback) => {
20
20
  };
21
21
 
22
22
  export const SESSION_TTL = 30 * 24 * 3600_000;
23
- export const MAX_OTS_PER_ACCOUNT = () => Number(process.env.PLATFORM_MAX_OTS || 10);
23
+ export const MAX_OTS_PER_ACCOUNT = () => Number(process.env.PLATFORM_MAX_OTS || 7);
24
24
 
25
25
  /**
26
26
  * Ajustes que cada ots puede cambiar. Lo que no está aquí lo fija la plataforma