@7ots/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.env.example +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +655 -0
  4. package/brand/apple-touch-icon.png +0 -0
  5. package/brand/favicon.svg +4 -0
  6. package/brand/icon-512.png +0 -0
  7. package/brand/mark-mono.svg +3 -0
  8. package/brand/mark.svg +4 -0
  9. package/brand/og-es.png +0 -0
  10. package/brand/og-pt.png +0 -0
  11. package/brand/og.png +0 -0
  12. package/brand/ots.json +11 -0
  13. package/brand/tokens.css +59 -0
  14. package/brand/wordmark.svg +5 -0
  15. package/cli/7ots.mjs +400 -0
  16. package/cli/lib/brain.mjs +143 -0
  17. package/cli/lib/config.mjs +70 -0
  18. package/cli/lib/hooks.mjs +151 -0
  19. package/cli/lib/i18n.mjs +480 -0
  20. package/cli/lib/identity.mjs +51 -0
  21. package/cli/lib/install.mjs +79 -0
  22. package/cli/lib/lines.mjs +73 -0
  23. package/cli/lib/meet.mjs +166 -0
  24. package/cli/lib/open.mjs +19 -0
  25. package/cli/lib/orquesta.mjs +84 -0
  26. package/cli/lib/paths.mjs +49 -0
  27. package/cli/lib/pet-core.mjs +184 -0
  28. package/cli/lib/pet-server.mjs +296 -0
  29. package/cli/lib/prompts.mjs +75 -0
  30. package/cli/lib/terminal.mjs +145 -0
  31. package/cli/lib/ui.mjs +234 -0
  32. package/cli/lib/wizard.mjs +279 -0
  33. package/cli/pet/electron/main.cjs +86 -0
  34. package/cli/pet/pet.html +404 -0
  35. package/dist/7ots.esm.js +627 -0
  36. package/dist/7ots.esm.js.map +7 -0
  37. package/dist/7ots.iife.js +627 -0
  38. package/dist/7ots.iife.js.map +7 -0
  39. package/llms.txt +57 -0
  40. package/package.json +73 -0
  41. package/server/admin.mjs +213 -0
  42. package/server/apuchat-relay.mjs +170 -0
  43. package/server/channels/agent.mjs +136 -0
  44. package/server/channels/apuchat.mjs +364 -0
  45. package/server/channels/apumail.mjs +211 -0
  46. package/server/channels/index.mjs +69 -0
  47. package/server/contact.mjs +151 -0
  48. package/server/demo.mjs +154 -0
  49. package/server/i18n.mjs +54 -0
  50. package/server/identity.mjs +108 -0
  51. package/server/llm.mjs +413 -0
  52. package/server/platform/auth.mjs +290 -0
  53. package/server/platform/crypto.mjs +80 -0
  54. package/server/platform/db.mjs +130 -0
  55. package/server/platform/routes.mjs +388 -0
  56. package/server/platform/runtime.mjs +180 -0
  57. package/server/platform/store.mjs +331 -0
  58. package/server/sdk.mjs +125 -0
  59. package/server/server.mjs +456 -0
  60. package/server/settings.mjs +177 -0
  61. package/server/tts.mjs +186 -0
  62. package/skills/7ots/SKILL.md +59 -0
  63. package/src/actions/ActionRegistry.js +254 -0
  64. package/src/actions/PageTools.js +270 -0
  65. package/src/actions/builtins.js +565 -0
  66. package/src/auth/AuthManager.js +128 -0
  67. package/src/avatar/AvatarStage.js +473 -0
  68. package/src/character/AvatarEditor.js +829 -0
  69. package/src/character/Character.js +1191 -0
  70. package/src/character/motion.js +794 -0
  71. package/src/character/parts.js +559 -0
  72. package/src/context/ContextManager.js +472 -0
  73. package/src/core/AgentBrain.js +341 -0
  74. package/src/core/AgentWidget.js +1059 -0
  75. package/src/core/EventBus.js +61 -0
  76. package/src/core/Proactivity.js +363 -0
  77. package/src/core/storage.js +35 -0
  78. package/src/i18n/index.js +193 -0
  79. package/src/i18n/messages/character.js +543 -0
  80. package/src/i18n/messages/identity.js +141 -0
  81. package/src/i18n/messages/platform.js +114 -0
  82. package/src/i18n/messages/server.js +630 -0
  83. package/src/i18n/messages/widget.js +362 -0
  84. package/src/identity/ContactCard.js +351 -0
  85. package/src/identity/Face.js +148 -0
  86. package/src/identity/random.js +112 -0
  87. package/src/identity/schema.js +262 -0
  88. package/src/index.js +117 -0
  89. package/src/integrations/apuchat.js +212 -0
  90. package/src/integrations/apumail.js +88 -0
  91. package/src/llm/ProxyLLM.js +87 -0
  92. package/src/mcp/McpClient.js +153 -0
  93. package/src/ui/Companion.js +498 -0
  94. package/src/ui/UIManager.js +425 -0
  95. package/src/ui/VirtualPointer.js +746 -0
  96. package/src/ui/markdown.js +94 -0
  97. package/src/voice/VoiceEngine.js +227 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Datos de la plataforma: cuentas, sesiones, ots, conversaciones y uso.
3
+ * Todo pasa por aquí; las rutas nunca escriben SQL.
4
+ */
5
+
6
+ import { defineIdentity } from '../../src/identity/schema.js';
7
+ import { getDb, monthOf, tx } from './db.mjs';
8
+ import { newId, open, seal, sha256, token } from './crypto.mjs';
9
+
10
+ const now = () => Date.now();
11
+ const parse = (s, fallback) => {
12
+ try {
13
+ return JSON.parse(s);
14
+ } catch {
15
+ return fallback;
16
+ }
17
+ };
18
+
19
+ export const SESSION_TTL = 30 * 24 * 3600_000;
20
+ export const MAX_OTS_PER_ACCOUNT = () => Number(process.env.PLATFORM_MAX_OTS || 10);
21
+
22
+ /**
23
+ * Ajustes que cada ots puede cambiar. Lo que no está aquí lo fija la plataforma
24
+ * (hub de apuchat, API de apumail, URL de la voz de apuchat, puertos…): así nadie puede
25
+ * apuntar el servidor a direcciones internas.
26
+ * secret: se guarda cifrado y nunca se devuelve.
27
+ * max: tope numérico.
28
+ * channels: al cambiar, se reinician los canales de ese ots.
29
+ */
30
+ export const OTS_KEYS = {
31
+ LLM_PROVIDER: {}, LLM_MODEL: {}, LLM_EFFORT: {}, LLM_MAX_TOKENS: { max: 16000 },
32
+ ANTHROPIC_API_KEY: { secret: true }, OPENAI_API_KEY: { secret: true },
33
+ SERVER_INSTRUCTIONS: {},
34
+ TTS_PROVIDER: {}, OPENAI_TTS_MODEL: {}, OPENAI_TTS_VOICE: {},
35
+ ELEVENLABS_API_KEY: { secret: true }, ELEVENLABS_VOICE_ID: {}, ELEVENLABS_MODEL: {},
36
+ APUCHAT_VOICE_TOKEN: { secret: true }, APUCHAT_VOICE_ID: {}, APUCHAT_VOICE_PROVIDER: {},
37
+ XAI_API_KEY: { secret: true }, XAI_TTS_VOICE: {},
38
+ FISH_API_KEY: { secret: true }, FISH_VOICE_ID: {}, FISH_MODEL: {},
39
+ APUMAIL_INBOX: {}, APUMAIL_INBOX_TOKEN: { secret: true }, APUMAIL_TO: {},
40
+ APUCHAT_NOTIFIER_IDENTITY_KEY: { secret: true }, APUCHAT_OPERATOR_HANDLE: {}, APUCHAT_TRANSCRIBE: {},
41
+ APUMAIL_AGENT_INBOX: { channels: true }, APUMAIL_AGENT_TOKEN: { secret: true, channels: true },
42
+ APUMAIL_AGENT_DAILY: { channels: true, max: 100 },
43
+ APUCHAT_AGENT_IDENTITY_KEY: { secret: true, channels: true }, APUCHAT_AGENT_ALLOW: { channels: true },
44
+ AGENT_MAX_CALLS: { channels: true, max: 3 }, AGENT_CALL_MAX_MINUTES: { channels: true, max: 30 }, AGENT_CALL_MAX_TURNS: { channels: true, max: 60 },
45
+ };
46
+
47
+ // ───────────────────────────── cuentas ─────────────────────────────
48
+
49
+ export function accountById(id) {
50
+ return getDb().prepare('SELECT * FROM accounts WHERE id = ?').get(id) || null;
51
+ }
52
+
53
+ /**
54
+ * Cuenta para un login. Con notlogin se busca por `sub`; si no existe y trae un email
55
+ * verificado que ya tiene cuenta, se vincula a esa. Si no, se crea.
56
+ */
57
+ export function upsertAccount({ email = null, notloginSub = null, name = '' }) {
58
+ email = email ? String(email).trim().toLowerCase() : null;
59
+ return tx((d) => {
60
+ let acc = notloginSub ? d.prepare('SELECT * FROM accounts WHERE notlogin_sub = ?').get(notloginSub) : null;
61
+ if (!acc && email) acc = d.prepare('SELECT * FROM accounts WHERE email = ?').get(email);
62
+ if (acc) {
63
+ if (notloginSub && !acc.notlogin_sub) d.prepare('UPDATE accounts SET notlogin_sub = ? WHERE id = ?').run(notloginSub, acc.id);
64
+ if (email && !acc.email && !d.prepare('SELECT 1 FROM accounts WHERE email = ?').get(email)) d.prepare('UPDATE accounts SET email = ? WHERE id = ?').run(email, acc.id);
65
+ d.prepare('UPDATE accounts SET last_login_at = ? WHERE id = ?').run(now(), acc.id);
66
+ return d.prepare('SELECT * FROM accounts WHERE id = ?').get(acc.id);
67
+ }
68
+ const id = newId('acc');
69
+ d.prepare('INSERT INTO accounts (id, email, notlogin_sub, name, created_at, last_login_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, email, notloginSub, String(name || '').slice(0, 80), now(), now());
70
+ return d.prepare('SELECT * FROM accounts WHERE id = ?').get(id);
71
+ });
72
+ }
73
+
74
+ export function publicAccount(acc) {
75
+ return { id: acc.id, email: acc.email, name: acc.name, plan: acc.plan, notlogin: !!acc.notlogin_sub, createdAt: acc.created_at };
76
+ }
77
+
78
+ // ───────────────────────────── sesiones ─────────────────────────────
79
+
80
+ /** Crea una sesión y devuelve el token en claro (solo va en la cookie). */
81
+ export function createSession(accountId, via) {
82
+ const raw = token();
83
+ getDb().prepare('INSERT INTO sessions (id, account_id, created_at, expires_at, via) VALUES (?, ?, ?, ?, ?)').run(sha256(raw), accountId, now(), now() + SESSION_TTL, via);
84
+ return raw;
85
+ }
86
+
87
+ export function sessionAccount(raw) {
88
+ if (!raw) return null;
89
+ const row = getDb().prepare('SELECT a.* FROM sessions s JOIN accounts a ON a.id = s.account_id WHERE s.id = ? AND s.expires_at > ?').get(sha256(raw), now());
90
+ return row || null;
91
+ }
92
+
93
+ export function deleteSession(raw) {
94
+ if (raw) getDb().prepare('DELETE FROM sessions WHERE id = ?').run(sha256(raw));
95
+ }
96
+
97
+ // ───────────────────────────── enlaces por email ─────────────────────────────
98
+
99
+ export const LOGIN_TOKEN_TTL = 15 * 60_000;
100
+
101
+ export function createLoginToken(email) {
102
+ const raw = token();
103
+ const d = getDb();
104
+ d.prepare('DELETE FROM login_tokens WHERE expires_at < ?').run(now() - 24 * 3600_000);
105
+ d.prepare('INSERT INTO login_tokens (hash, email, expires_at) VALUES (?, ?, ?)').run(sha256(raw), email, now() + LOGIN_TOKEN_TTL);
106
+ return raw;
107
+ }
108
+
109
+ /** Enlaces pedidos para ese email en la última hora (límite anti-abuso). */
110
+ export function recentLoginTokens(email) {
111
+ return getDb().prepare('SELECT COUNT(*) n FROM login_tokens WHERE email = ? AND expires_at > ?').get(email, now() + LOGIN_TOKEN_TTL - 3600_000).n;
112
+ }
113
+
114
+ /** Gasta un enlace: devuelve el email si era válido (una sola vez). */
115
+ export function consumeLoginToken(raw) {
116
+ if (!raw) return null;
117
+ return tx((d) => {
118
+ const row = d.prepare('SELECT * FROM login_tokens WHERE hash = ?').get(sha256(raw));
119
+ if (!row || row.used_at || row.expires_at < now()) return null;
120
+ d.prepare('UPDATE login_tokens SET used_at = ? WHERE hash = ?').run(now(), row.hash);
121
+ return row.email;
122
+ });
123
+ }
124
+
125
+ // ───────────────────────────── ots ─────────────────────────────
126
+
127
+ function hydrate(row) {
128
+ if (!row) return null;
129
+ return {
130
+ id: row.id,
131
+ accountId: row.account_id,
132
+ name: row.name,
133
+ status: row.status,
134
+ domains: parse(row.domains, []),
135
+ identity: defineIdentity(parse(row.identity, {})),
136
+ widget: parse(row.widget, {}),
137
+ settings: parse(row.settings, {}),
138
+ secrets: open(row.secrets),
139
+ createdAt: row.created_at,
140
+ updatedAt: row.updated_at,
141
+ };
142
+ }
143
+
144
+ export function getOts(id) {
145
+ return hydrate(getDb().prepare('SELECT * FROM ots WHERE id = ?').get(String(id || '')));
146
+ }
147
+
148
+ /** El ots solo si es de esa cuenta (si no, null: la ruta responde 404, no 403). */
149
+ export function getOwnOts(accountId, id) {
150
+ const o = getOts(id);
151
+ return o && o.accountId === accountId ? o : null;
152
+ }
153
+
154
+ export function listOts(accountId) {
155
+ return getDb().prepare('SELECT * FROM ots WHERE account_id = ? ORDER BY created_at').all(accountId).map(hydrate);
156
+ }
157
+
158
+ export function countOts(accountId) {
159
+ return getDb().prepare('SELECT COUNT(*) n FROM ots WHERE account_id = ?').get(accountId).n;
160
+ }
161
+
162
+ export function createOts(accountId, { name, identity = {}, widget = {}, settings = {}, secrets = {}, domains = [] } = {}) {
163
+ const id = newId('ots');
164
+ const nm = cleanName(name) || 'Ots';
165
+ const ident = defineIdentity({ ...identity, name: identity.name || nm });
166
+ getDb()
167
+ .prepare('INSERT INTO ots (id, account_id, name, status, domains, identity, widget, settings, secrets, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
168
+ .run(id, accountId, nm, 'on', JSON.stringify(cleanDomains(domains)), JSON.stringify(ident), JSON.stringify(widget), JSON.stringify(settings), seal(secrets), now(), now());
169
+ return getOts(id);
170
+ }
171
+
172
+ /** Copia un ots (sin sus claves ni canales propios: un @handle no puede estar en dos ots). */
173
+ export function duplicateOts(accountId, src) {
174
+ const settings = { ...src.settings };
175
+ for (const k of Object.keys(settings)) if (OTS_KEYS[k]?.channels) delete settings[k];
176
+ return createOts(accountId, { name: `${src.name} (copia)`, identity: src.identity, widget: src.widget, settings, domains: src.domains });
177
+ }
178
+
179
+ export function updateOts(id, patch) {
180
+ const cur = getOts(id);
181
+ if (!cur) return null;
182
+ const next = { ...cur, ...patch };
183
+ getDb()
184
+ .prepare('UPDATE ots SET name = ?, status = ?, domains = ?, identity = ?, widget = ?, settings = ?, secrets = ?, updated_at = ? WHERE id = ?')
185
+ .run(cleanName(next.name) || cur.name, next.status === 'off' ? 'off' : 'on', JSON.stringify(cleanDomains(next.domains)), JSON.stringify(next.identity), JSON.stringify(next.widget), JSON.stringify(next.settings), seal(next.secrets), now(), id);
186
+ return getOts(id);
187
+ }
188
+
189
+ export function deleteOts(id) {
190
+ getDb().prepare('DELETE FROM ots WHERE id = ?').run(id);
191
+ }
192
+
193
+ /** Todos los ots encendidos (para arrancar sus canales). */
194
+ export function allLiveOts() {
195
+ return getDb().prepare("SELECT * FROM ots WHERE status = 'on'").all().map(hydrate);
196
+ }
197
+
198
+ function cleanName(s) {
199
+ return String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 60);
200
+ }
201
+
202
+ /**
203
+ * Dominios donde puede ir el widget: "tienda.com", "*.tienda.com" o "http://localhost:3000".
204
+ * Se guardan como orígenes o comodines normalizados.
205
+ */
206
+ export function cleanDomains(list) {
207
+ const out = new Set();
208
+ for (const raw of Array.isArray(list) ? list : String(list || '').split(/[\s,]+/)) {
209
+ let s = String(raw || '').trim().toLowerCase().replace(/\/+$/, '');
210
+ if (!s) continue;
211
+ const wildcard = s.startsWith('*.');
212
+ if (wildcard) s = s.slice(2);
213
+ if (!/^https?:\/\//.test(s)) s = `https://${s}`;
214
+ let u;
215
+ try {
216
+ u = new URL(s);
217
+ } catch {
218
+ continue;
219
+ }
220
+ if (!/^[a-z0-9.-]+$/.test(u.hostname) || u.pathname !== '/' || u.search || u.username) continue;
221
+ out.add(wildcard ? `*.${u.hostname}` : u.origin);
222
+ if (out.size >= 20) break;
223
+ }
224
+ return [...out];
225
+ }
226
+
227
+ /** ¿Puede este origen usar el ots? */
228
+ export function originAllowed(ots, origin) {
229
+ let u;
230
+ try {
231
+ u = new URL(origin);
232
+ } catch {
233
+ return false;
234
+ }
235
+ return ots.domains.some((d) => {
236
+ if (d.startsWith('*.')) {
237
+ const base = d.slice(2);
238
+ return u.protocol === 'https:' && (u.hostname === base || u.hostname.endsWith(`.${base}`));
239
+ }
240
+ // "https://tienda.com" también admite www.tienda.com
241
+ const a = new URL(d);
242
+ return u.protocol === a.protocol && u.port === a.port && (u.hostname === a.hostname || u.hostname === `www.${a.hostname}`);
243
+ });
244
+ }
245
+
246
+ // ───────────────────────────── conversaciones ─────────────────────────────
247
+
248
+ /**
249
+ * Apunta un turno en la conversación (se crea la primera vez).
250
+ * `key` la elige quien llama (sesión del widget, contacto del canal); el id se deriva de ella.
251
+ * @returns {{ created: boolean }}
252
+ */
253
+ /** `page`: URL concreta (actualiza la conversación); `origin`: solo si aún no hay página. */
254
+ export function logTurn(ots, { key, channel = 'web', visitor = '', page = '', origin = '', user = '', reply = '' }) {
255
+ if (!key || (!user && !reply)) return { created: false };
256
+ const id = `c_${sha256(`${ots.id}:${channel}:${key}`).slice(0, 22)}`;
257
+ const t = now();
258
+ return tx((d) => {
259
+ const cur = d.prepare('SELECT id FROM conversations WHERE id = ?').get(id);
260
+ if (!cur) {
261
+ d.prepare('INSERT INTO conversations (id, ots_id, channel, visitor, page, title, messages, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)').run(id, ots.id, channel, String(visitor).slice(0, 120), String(page || origin).slice(0, 500), String(user || reply).replace(/\s+/g, ' ').slice(0, 80), t, t);
262
+ }
263
+ let n = 0;
264
+ if (user) {
265
+ d.prepare('INSERT INTO messages (conversation_id, role, content, at) VALUES (?, ?, ?, ?)').run(id, 'user', String(user).slice(0, 8000), t);
266
+ n++;
267
+ }
268
+ if (reply) {
269
+ d.prepare('INSERT INTO messages (conversation_id, role, content, at) VALUES (?, ?, ?, ?)').run(id, 'assistant', String(reply).slice(0, 8000), t);
270
+ n++;
271
+ }
272
+ d.prepare('UPDATE conversations SET messages = messages + ?, updated_at = ?, page = CASE WHEN ? != \'\' THEN ? ELSE page END WHERE id = ?').run(n, t, page, String(page).slice(0, 500), id);
273
+ return { created: !cur };
274
+ });
275
+ }
276
+
277
+ export function listConversations(otsId, { limit = 50, before = null } = {}) {
278
+ const lim = Math.min(200, Math.max(1, Number(limit) || 50));
279
+ const rows = before
280
+ ? getDb().prepare('SELECT * FROM conversations WHERE ots_id = ? AND updated_at < ? ORDER BY updated_at DESC LIMIT ?').all(otsId, Number(before), lim)
281
+ : getDb().prepare('SELECT * FROM conversations WHERE ots_id = ? ORDER BY updated_at DESC LIMIT ?').all(otsId, lim);
282
+ return rows.map((r) => ({ id: r.id, channel: r.channel, visitor: r.visitor, page: r.page, title: r.title, messages: r.messages, startedAt: r.started_at, updatedAt: r.updated_at }));
283
+ }
284
+
285
+ export function getConversation(otsId, id) {
286
+ const c = getDb().prepare('SELECT * FROM conversations WHERE id = ? AND ots_id = ?').get(id, otsId);
287
+ if (!c) return null;
288
+ const messages = getDb().prepare('SELECT role, content, at FROM messages WHERE conversation_id = ? ORDER BY id LIMIT 2000').all(id);
289
+ return { id: c.id, channel: c.channel, visitor: c.visitor, page: c.page, title: c.title, startedAt: c.started_at, updatedAt: c.updated_at, messages };
290
+ }
291
+
292
+ export function deleteConversation(otsId, id) {
293
+ return getDb().prepare('DELETE FROM conversations WHERE id = ? AND ots_id = ?').run(id, otsId).changes > 0;
294
+ }
295
+
296
+ // ───────────────────────────── uso ─────────────────────────────
297
+
298
+ /**
299
+ * Tipos: llm (respuestas de IA), llm_platform (las pagadas por la plataforma),
300
+ * tts / tts_platform (audios), call (videollamadas), conversation (conversaciones nuevas).
301
+ */
302
+ export function addUsage(ots, kind, n = 1) {
303
+ getDb()
304
+ .prepare('INSERT INTO usage (account_id, ots_id, month, kind, count) VALUES (?, ?, ?, ?, ?) ON CONFLICT(ots_id, month, kind) DO UPDATE SET count = count + excluded.count')
305
+ .run(ots.accountId, ots.id, monthOf(), kind, n);
306
+ }
307
+
308
+ /** Uso del mes de toda la cuenta de un tipo (para el cupo gratis). */
309
+ export function accountUsage(accountId, kind, month = monthOf()) {
310
+ return getDb().prepare('SELECT COALESCE(SUM(count), 0) n FROM usage WHERE account_id = ? AND month = ? AND kind = ?').get(accountId, month, kind).n;
311
+ }
312
+
313
+ /** Uso del mes por ots: { [otsId]: { llm, llm_platform, … } } */
314
+ export function usageByOts(accountId, month = monthOf()) {
315
+ const out = {};
316
+ for (const r of getDb().prepare('SELECT ots_id, kind, count FROM usage WHERE account_id = ? AND month = ?').all(accountId, month)) {
317
+ (out[r.ots_id] ||= {})[r.kind] = r.count;
318
+ }
319
+ return out;
320
+ }
321
+
322
+ /** Últimos meses de un ots: [{ month, llm, … }] */
323
+ export function usageHistory(otsId, months = 6) {
324
+ const rows = getDb().prepare('SELECT month, kind, count FROM usage WHERE ots_id = ? ORDER BY month DESC').all(otsId);
325
+ const by = new Map();
326
+ for (const r of rows) {
327
+ if (!by.has(r.month)) by.set(r.month, { month: r.month });
328
+ by.get(r.month)[r.kind] = r.count;
329
+ }
330
+ return [...by.values()].slice(0, months);
331
+ }
package/server/sdk.mjs ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * 7ots/server — identidad y contactabilidad para CUALQUIER agente, sin el widget.
3
+ *
4
+ * Tienes tu propio agente (LangChain, tu API, un bot…) y quieres darle una cara, una voz y
5
+ * formas de contactarlo: un correo propio (apumail), un @handle que contesta mensajes y
6
+ * llamadas (apuchat), videollamadas con avatar (meet.apuchat.com) y una tarjeta pública.
7
+ *
8
+ * import { createAgentIdentity } from '7ots/server';
9
+ *
10
+ * const nube = createAgentIdentity({
11
+ * identity: {
12
+ * name: 'Nube', role: 'asistente de Nimbus', bio: 'Te ayudo con tu almacenamiento.',
13
+ * look: { color: '#6d5dfc', image: 'https://…/nube.png', meetAvatar: 'maya' },
14
+ * voice: { provider: 'openai', voiceId: 'coral', style: 'cálida y clara' },
15
+ * contact: { site: 'https://nimbus.example', email: 'nube@nimbus.example' },
16
+ * },
17
+ * // Tu cerebro: recibe el mensaje y devuelve el texto de la respuesta.
18
+ * brain: async ({ text, channel, history, identity }) => miAgente.responder(text),
19
+ * apumail: { inbox: 'nube@nimbus.example', token: process.env.NUBE_MAIL_TOKEN },
20
+ * apuchat: { identityKey: process.env.NUBE_APUCHAT_KEY },
21
+ * });
22
+ * nube.start();
23
+ *
24
+ * http.createServer((req, res) => nube.handler(req, res) || miApp(req, res));
25
+ *
26
+ * Sin `brain`, usa el LLM del proxy de 7ots (LLM_PROVIDER + claves en el entorno).
27
+ * Las claves se pasan en el código del servidor o por entorno; nunca aparecen en la tarjeta.
28
+ */
29
+
30
+ import { defineIdentity, publicIdentity, toVCard, identityPrompt, useIdentity, getIdentity, MEET_AVATARS, MEET_SCENES, OPENAI_VOICES, GROK_VOICES } from './identity.mjs';
31
+ import { startAgentChannels } from './channels/index.mjs';
32
+ import { synthesize } from './tts.mjs';
33
+
34
+ export { defineIdentity, publicIdentity, toVCard, identityPrompt, MEET_AVATARS, MEET_SCENES, OPENAI_VOICES, GROK_VOICES };
35
+
36
+ /**
37
+ * @param {object} o
38
+ * @param {object} [o.identity] ficha de identidad (ver server/identity.mjs). Sin ella: entorno + data/identity.json
39
+ * @param {Function} [o.brain] async ({ key, channel, text, lang, history, identity, system, tools, run }) => string
40
+ * @param {{step: Function}} [o.llm] o un LLM con la interfaz neutral de 7ots (sin brain ni llm: el del entorno)
41
+ * @param {object|false} [o.apumail] { inbox, token, webhookSecret, daily, api } — false lo apaga
42
+ * @param {object|false} [o.apuchat] { identityKey, allow, maxCalls, maxMinutes, maxTurns, hub } — false lo apaga
43
+ * @param {string} [o.basePath] prefijo de las rutas de handler() (defecto '/agent')
44
+ * @param {Function} [o.log]
45
+ */
46
+ export function createAgentIdentity({ identity, brain, llm, apumail, apuchat, basePath = '/agent', log } = {}) {
47
+ if (identity) useIdentity(identity);
48
+ let channels = null;
49
+ const base = basePath.replace(/\/$/, '');
50
+
51
+ const ensure = async () => {
52
+ if (channels) return channels;
53
+ let theLlm = llm;
54
+ if (!brain && !theLlm) theLlm = (await import('./llm.mjs')).createLLM();
55
+ channels = startAgentChannels({ llm: theLlm, brain, identity: getIdentity, config: { apumail, apuchat }, ...(log ? { log } : {}) });
56
+ return channels;
57
+ };
58
+
59
+ const api = {
60
+ /** Arranca los canales configurados (long-poll de apumail, mensajes y llamadas de apuchat). */
61
+ async start() {
62
+ await ensure();
63
+ return api;
64
+ },
65
+ async stop() {
66
+ await channels?.stop();
67
+ },
68
+ /** Identidad completa (incluye instrucciones; no la publiques tal cual). */
69
+ get identity() {
70
+ return getIdentity();
71
+ },
72
+ /** Cambia la identidad en caliente (nombre, voz, avatar de meet…). */
73
+ update(patch) {
74
+ return useIdentity(defineIdentity(patch, getIdentity()));
75
+ },
76
+ /** Tarjeta pública (segura para servir). */
77
+ card() {
78
+ return publicIdentity(getIdentity(), channels?.live() || {});
79
+ },
80
+ vcard(o) {
81
+ return toVCard(api.card(), o);
82
+ },
83
+ /** Texto → audio con la voz de la identidad. → { audio: Buffer, contentType } */
84
+ speak(text) {
85
+ return synthesize({ text, voice: getIdentity().voice });
86
+ },
87
+ /** Responde a un mensaje como lo haría por un canal ('email' | 'dm' | 'voice'). */
88
+ async respond(text, { from = 'sdk', channel = 'dm', lang } = {}) {
89
+ return (await ensure()).agent.respond(`${channel}:${from}`, channel, text, { lang });
90
+ },
91
+ /** Hace sonar la app de apuchat de @handle con una videollamada del agente. */
92
+ async call(handle, motivo) {
93
+ return (await ensure()).ring(handle, motivo);
94
+ },
95
+ /** Crea una videollamada de meet con el agente dentro → { call_url } (dásela solo a quien llama). */
96
+ async openCall(o) {
97
+ return (await ensure()).openCall(o);
98
+ },
99
+ status() {
100
+ return channels?.status() || { apumail: false, apuchat: false };
101
+ },
102
+ /**
103
+ * Manejador HTTP para Node (http, Express, Fastify con .raw…). Devuelve una promesa que
104
+ * resuelve true si atendió la ruta:
105
+ * GET {base}/identity · GET {base}/identity.vcf · GET /.well-known/7ots-agent.json
106
+ * POST {base}/channels/apumail (webhook de apumail)
107
+ */
108
+ async handler(req, res) {
109
+ const path = new URL(req.url, 'http://x').pathname;
110
+ if (req.method === 'GET' && (path === `${base}/identity` || path === '/.well-known/7ots-agent.json')) {
111
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
112
+ res.end(JSON.stringify(api.card()));
113
+ return true;
114
+ }
115
+ if (req.method === 'GET' && path === `${base}/identity.vcf`) {
116
+ res.writeHead(200, { 'Content-Type': 'text/vcard; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
117
+ res.end(api.vcard());
118
+ return true;
119
+ }
120
+ if (path.startsWith(`${base}/channels/`)) return (await ensure()).handle(req, res, path.slice(base.length));
121
+ return false;
122
+ },
123
+ };
124
+ return api;
125
+ }