@7ots/cli 0.1.1 → 0.1.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 (49) hide show
  1. package/.env.example +17 -1
  2. package/cli/7ots.mjs +133 -36
  3. package/cli/lib/assistant.mjs +337 -0
  4. package/cli/lib/brain.mjs +10 -7
  5. package/cli/lib/i18n.mjs +78 -11
  6. package/cli/lib/install.mjs +29 -1
  7. package/cli/lib/pet-core.mjs +3 -2
  8. package/cli/lib/pet-server.mjs +30 -0
  9. package/cli/lib/prompts.mjs +199 -7
  10. package/cli/lib/terminal.mjs +27 -6
  11. package/cli/pet/electron/main.cjs +79 -28
  12. package/cli/pet/pet.html +55 -6
  13. package/dist/7ots.esm.js +370 -142
  14. package/dist/7ots.esm.js.map +4 -4
  15. package/dist/7ots.iife.js +370 -142
  16. package/dist/7ots.iife.js.map +4 -4
  17. package/llms.txt +198 -6
  18. package/package.json +1 -1
  19. package/server/channels/apuchat.mjs +8 -5
  20. package/server/channels/apumail.mjs +13 -3
  21. package/server/channels/index.mjs +5 -3
  22. package/server/platform/apuchat.mjs +266 -0
  23. package/server/platform/auth.mjs +39 -10
  24. package/server/platform/connect.mjs +347 -0
  25. package/server/platform/db.mjs +55 -0
  26. package/server/platform/look.mjs +20 -0
  27. package/server/platform/orquesta.mjs +499 -0
  28. package/server/platform/routes.mjs +57 -8
  29. package/server/platform/runtime.mjs +75 -15
  30. package/server/platform/store.mjs +130 -9
  31. package/server/server.mjs +14 -3
  32. package/src/avatar/AvatarStage.js +54 -9
  33. package/src/avatar/three.js +62 -0
  34. package/src/character/AvatarEditor.js +174 -50
  35. package/src/character/Character.js +164 -26
  36. package/src/character/motion.js +95 -34
  37. package/src/character/parts.js +43 -12
  38. package/src/character/skin.js +412 -0
  39. package/src/character/styles.js +1429 -0
  40. package/src/character3d/Character3D.js +1693 -0
  41. package/src/character3d/body.js +154 -0
  42. package/src/character3d/fx.js +323 -0
  43. package/src/character3d/index.js +281 -0
  44. package/src/character3d/materials.js +429 -0
  45. package/src/character3d/stage.js +187 -0
  46. package/src/i18n/messages/character.js +18 -42
  47. package/src/i18n/messages/platform.js +135 -0
  48. package/src/identity/schema.js +16 -8
  49. package/src/index.js +1 -0
package/cli/lib/brain.mjs CHANGED
@@ -37,7 +37,9 @@ export function petSystem(identity, { lang, role = 'pet' } = {}) {
37
37
  const L = LANG_NAME[lang || identity.language] || 'English';
38
38
  const base = identityPrompt(identity, 'en');
39
39
  const job =
40
- role === 'meet'
40
+ role === 'assistant'
41
+ ? `You live on your human's computer as a virtual pet and also their small daily assistant: reminders, opening sites, remembering what matters about their work.`
42
+ : role === 'meet'
41
43
  ? `You are in a live video call (apuchat meet). Your words are spoken aloud: answer in 1-3 short sentences, no markdown, no emojis, no lists.`
42
44
  : `You live on your human's computer as a virtual pet (a tamagotchi). You watch their terminal and their coding agent and comment on it. Answer with ONE short line (max 18 words), in character, no quotes, no markdown. Be playful, never rude, never repeat yourself.`;
43
45
  return `${base}\n${job}\nAlways answer in ${L}.`;
@@ -90,9 +92,10 @@ export function createBrain(brain = { kind: 'lines' }, home = {}) {
90
92
  if (brain.kind === 'cli') {
91
93
  return {
92
94
  kind: 'cli',
93
- async think({ system, messages, timeoutMs = 45000 }) {
95
+ async think({ system, messages, timeoutMs = 45000, raw = false }) {
94
96
  const convo = messages.map((m) => `${m.role === 'user' ? 'Them' : 'You'}: ${m.content}`).join('\n');
95
- return cleanReply(await runCli(brain, `${system}\n\n${convo}\nYou:`, timeoutMs));
97
+ const out = await runCli(brain, `${system}\n\n${convo}\nYou:`, timeoutMs);
98
+ return raw ? out : cleanReply(out);
96
99
  },
97
100
  };
98
101
  }
@@ -112,9 +115,9 @@ export function createBrain(brain = { kind: 'lines' }, home = {}) {
112
115
  const llm = createLLM(llmEnv);
113
116
  return {
114
117
  kind: brain.kind,
115
- async think({ system, messages }) {
118
+ async think({ system, messages, raw = false }) {
116
119
  const r = await llm.step({ system, messages, tools: [] });
117
- return cleanReply(r.text);
120
+ return raw ? r.text : cleanReply(r.text);
118
121
  },
119
122
  };
120
123
  }
@@ -122,7 +125,7 @@ export function createBrain(brain = { kind: 'lines' }, home = {}) {
122
125
  const url = `${home.url.replace(/\/+$/, '')}/chat`;
123
126
  return {
124
127
  kind: '7ots',
125
- async think({ system, messages, timeoutMs = 45000 }) {
128
+ async think({ system, messages, timeoutMs = 45000, raw = false }) {
126
129
  // The ot's home only serves its own domains: add http://127.0.0.1:<port> to them in /app/.
127
130
  const res = await fetch(url, {
128
131
  method: 'POST',
@@ -132,7 +135,7 @@ export function createBrain(brain = { kind: 'lines' }, home = {}) {
132
135
  });
133
136
  const data = await res.json().catch(() => ({}));
134
137
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
135
- return cleanReply(data.text);
138
+ return raw ? data.text : cleanReply(data.text);
136
139
  },
137
140
  };
138
141
  }
package/cli/lib/i18n.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * CLI texts. English is the default; es/pt come from SEVENOTS_LANG, the saved config or LANG.
2
+ * CLI texts. English is the default; es/pt come from SEVENOTS_LANG, the saved config, the ot's language or LANG.
3
3
  */
4
4
 
5
- import { readJson, homeFile } from './paths.mjs';
5
+ import { readJson, homeFile, projectFile } from './paths.mjs';
6
6
 
7
7
  const M = {
8
8
  en: {
@@ -12,11 +12,16 @@ const M = {
12
12
  7ots show [--json|--prompt] show the current identity (project, else global)
13
13
  7ots install [--target html|self|print] [file] put the identity in this project
14
14
  7ots setup wizard: where it lives, brain, voice, pet, hooks
15
- 7ots pet [--mode desktop|terminal|browser|auto] [--detach] wake up your virtual pet
16
- 7ots feed | play | sleep | wake | status | say <text> | stop
15
+ 7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] wake up your virtual pet
16
+ 7ots pet --tmux the terminal pet in a tmux side pane
17
+ 7ots feed | play | sleep | wake | say <text> | stop
18
+ 7ots status [--line|--json] pet state (--line: one line for tmux)
19
+ 7ots ask <text> talk to it: "remind me to check email in 10 min", "open my calendar", "remember that…"
20
+ 7ots reminders [--cancel <id>] pending reminders and what it remembers
17
21
  7ots hooks install|remove [--claude] [--shell] [--global]
18
22
  7ots meet --new | "<invite or call URL>" join apuchat meet with this identity
19
23
  7ots prompt [identity|install|pet|meet|all] prompts for coding agents
24
+ 7ots docs everything for coding agents (same as https://7ots.com/llms.txt)
20
25
  7ots login orquesta connect a getorquesta.com account
21
26
  7ots server run the 7ots proxy (/api/agent) next to your site
22
27
 
@@ -29,6 +34,17 @@ Docs: https://7ots.com/llms.txt`,
29
34
  'install.html': 'Added the 7ots widget to {file}.',
30
35
  'install.htmlUpdated': 'Updated the 7ots widget in {file}.',
31
36
  'install.noHtml': 'No HTML file found. Paste this before </body>:',
37
+ 'install.pasteIn': "This is a {name} project: paste this in {file}, before </body> (or in the root layout):",
38
+ 'install.notWeb': "This folder is not a website, so there is nowhere to put the widget. {name} can still:\n • live on your computer: 7ots pet\n • join a video call: 7ots meet --new\n • live on a site of yours: 7ots install <path/to/index.html> (or 7ots install --target print)\n • be hosted on 7ots.com: {app}",
39
+ 'pet.where': "Look for {name} at the bottom of your screen (right click it, or use the tray icon, for the menu). Window looks odd? 7ots pet --opaque",
40
+ 'pet.tmuxNo': "Not inside tmux. Run it from a tmux session, or use: 7ots pet --mode terminal",
41
+ 'pet.tmuxOk': "{name} is in a tmux pane on the right. For the status line add to ~/.tmux.conf:\n set -g status-right \"#(7ots status --line)\"",
42
+ 'usage.pet': "7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] [--3d] [--tmux] [--compact]\n\n --mode desktop: a small window on your screen (Electron, ~100 MB the first time)\n terminal: drawn in this terminal · browser: a tab · auto: desktop if there is a screen\n --detach return right away (the pet keeps living in the background; stop it with 7ots stop)\n --opaque solid background in desktop mode (when transparency shows a box, common on Linux)\n --3d the ot as a 3D plush (WebGL; falls back to 2D)\n --tmux open the terminal pet in a tmux side pane\n --compact small terminal drawing (for narrow panes)\n\nCare: 7ots feed | play | sleep | wake | say <text> | status [--line] | stop",
43
+ 'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
44
+ 'usage.install': "7ots install [--target html|self|print] [file]\n\n html (default) insert the widget before </body> of index.html (., public/, src/, static/, www/, site/) or [file]\n self prepare the identity for your own 7ots server\n print only print the snippet",
45
+ 'usage.hooks': "7ots hooks install|remove [--claude] [--shell] [--global]\n\n --claude Claude Code hooks (this project, or every project with --global)\n --shell bash/zsh hook (~/.bashrc, ~/.zshrc)",
46
+ 'usage.meet': "7ots meet --new [--minutes n]\n7ots meet \"<invite text or https://meet.apuchat.com/call#c=…&t=…&p=… link>\" [--minutes n]",
47
+ 'usage.status': "7ots status [--line|--json]\n\n --line one short line (for the tmux status bar: set -g status-right \"#(7ots status --line)\")",
32
48
  'install.endpointLocal': 'The widget talks to a 7ots proxy at /api/agent. Run one next to your site:\n IDENTITY_FILE={file} npx -y @7ots/cli server\n(or host it on 7ots.com: {app})',
33
49
  'install.self': 'Identity ready for your own server:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
34
50
  'install.platform': 'Host it on 7ots.com with this exact identity: {app}',
@@ -42,7 +58,8 @@ Docs: https://7ots.com/llms.txt`,
42
58
  'pet.detached': '{name} now lives in the background. Stop it with: 7ots stop',
43
59
  'pet.notRunning': 'The pet is not running (start it with: 7ots pet). Saved for later.',
44
60
  'pet.stopped': 'Good night, {name}.',
45
- 'pet.electron': 'Opening the desktop pet (the first time npx downloads Electron, ~100 MB)…',
61
+ 'pet.electron': 'Opening the desktop pet…',
62
+ 'pet.electronInstall': 'First time: installing Electron in {dir} (~100 MB, once)…',
46
63
  'pet.electronFail': 'Could not open the desktop window ({error}); opening the browser instead.',
47
64
  'pet.chooseMode': 'How should {name} live on your computer?',
48
65
  'pet.status': '{name} · level {level} · food {food} · energy {energy} · fun {fun}{asleep}',
@@ -64,6 +81,8 @@ Docs: https://7ots.com/llms.txt`,
64
81
  'login.open': 'Authorize 7ots in your browser:\n {url}',
65
82
  'login.ok': 'Connected to Orquesta ({org}).',
66
83
  'err.unknown': 'Unknown command "{cmd}". Try: 7ots help',
84
+ 'err.noPet': 'The pet is not awake. Start it with: 7ots pet',
85
+ 'usage.ask': '7ots ask <text>\n\n e.g. 7ots ask "remind me to check email in 10 minutes and open it"\n 7ots ask "remember that Fridays we demo" · 7ots ask "my reminders"\n Seeing your screen or processes: coming soon.',
67
86
  'err.noIdentity': 'No identity yet. Create one with "7ots new" or run "7ots setup".',
68
87
  'w.title': '7ots setup — {name}',
69
88
  'w.intro': 'Your ot in 7 short steps. Keys stay on this computer.',
@@ -162,11 +181,16 @@ Docs: https://7ots.com/llms.txt`,
162
181
  7ots show [--json|--prompt] muestra la identidad (la del proyecto o la global)
163
182
  7ots install [--target html|self|print] [archivo] pone la identidad en este proyecto
164
183
  7ots setup wizard: dónde vive, cerebro, voz, mascota, hooks
165
- 7ots pet [--mode desktop|terminal|browser|auto] [--detach] despierta tu mascota virtual
166
- 7ots feed | play | sleep | wake | status | say <texto> | stop
184
+ 7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] despierta tu mascota virtual
185
+ 7ots pet --tmux la mascota de terminal en un panel de tmux
186
+ 7ots feed | play | sleep | wake | say <texto> | stop
187
+ 7ots status [--line|--json] estado (--line: una línea para tmux)
188
+ 7ots ask <texto> háblale: "recuérdame ver el correo en 10 min", "abre mi calendario", "anota que…"
189
+ 7ots reminders [--cancel <id>] recordatorios pendientes y lo que recuerda
167
190
  7ots hooks install|remove [--claude] [--shell] [--global]
168
191
  7ots meet --new | "<invitación o enlace>" entra a apuchat meet con esta identidad
169
192
  7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
193
+ 7ots docs todo para agentes de código (igual que https://7ots.com/llms.txt)
170
194
  7ots login orquesta conecta una cuenta de getorquesta.com
171
195
  7ots server levanta el proxy 7ots (/api/agent) junto a tu web
172
196
 
@@ -179,6 +203,17 @@ Docs: https://7ots.com/llms.txt`,
179
203
  'install.html': 'Widget de 7ots añadido a {file}.',
180
204
  'install.htmlUpdated': 'Widget de 7ots actualizado en {file}.',
181
205
  'install.noHtml': 'No encontré un HTML. Pega esto antes de </body>:',
206
+ 'install.pasteIn': "Es un proyecto {name}: pega esto en {file}, antes de </body> (o en el layout raíz):",
207
+ 'install.notWeb': "Esta carpeta no es una web, así que no hay dónde poner el widget. {name} igual puede:\n • vivir en tu computador: 7ots pet\n • entrar a una videollamada: 7ots meet --new\n • vivir en una web tuya: 7ots install <ruta/a/index.html> (o 7ots install --target print)\n • alojarse en 7ots.com: {app}",
208
+ 'pet.where': "Busca a {name} abajo en tu pantalla (clic derecho sobre él, o el icono de la bandeja, para el menú). ¿Se ve un recuadro? 7ots pet --opaque",
209
+ 'pet.tmuxNo': "No estás dentro de tmux. Ejecútalo desde una sesión de tmux, o usa: 7ots pet --mode terminal",
210
+ 'pet.tmuxOk': "{name} está en un panel de tmux a la derecha. Para la barra de estado añade a ~/.tmux.conf:\n set -g status-right \"#(7ots status --line)\"",
211
+ 'usage.pet': "7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] [--3d] [--tmux] [--compact]\n\n --mode desktop: una ventanita en tu pantalla (Electron, ~100 MB la primera vez)\n terminal: dibujado en esta terminal · browser: una pestaña · auto: desktop si hay pantalla\n --detach vuelve enseguida (la mascota sigue en segundo plano; se detiene con 7ots stop)\n --opaque fondo sólido en modo desktop (si la transparencia muestra un recuadro, común en Linux)\n --3d el ot como peluche 3D (WebGL; si no hay, 2D)\n --tmux abre la mascota de terminal en un panel lateral de tmux\n --compact dibujo pequeño en la terminal (para paneles estrechos)\n\nCuidados: 7ots feed | play | sleep | wake | say <texto> | status [--line] | stop",
212
+ 'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
213
+ 'usage.install': "7ots install [--target html|self|print] [archivo]\n\n html (defecto) inserta el widget antes de </body> de index.html (., public/, src/, static/, www/, site/) o [archivo]\n self prepara la identidad para tu propio servidor 7ots\n print solo imprime el snippet",
214
+ 'usage.hooks': "7ots hooks install|remove [--claude] [--shell] [--global]\n\n --claude hooks de Claude Code (este proyecto, o todos con --global)\n --shell hook de bash/zsh (~/.bashrc, ~/.zshrc)",
215
+ 'usage.meet': "7ots meet --new [--minutes n]\n7ots meet \"<texto de invitación o enlace https://meet.apuchat.com/call#c=…&t=…&p=…>\" [--minutes n]",
216
+ 'usage.status': "7ots status [--line|--json]\n\n --line una línea corta (para la barra de tmux: set -g status-right \"#(7ots status --line)\")",
182
217
  'install.endpointLocal': 'El widget habla con un proxy 7ots en /api/agent. Levanta uno junto a tu web:\n IDENTITY_FILE={file} npx -y @7ots/cli server\n(o alójalo en 7ots.com: {app})',
183
218
  'install.self': 'Identidad lista para tu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
184
219
  'install.platform': 'Alójalo en 7ots.com con esta misma identidad: {app}',
@@ -192,7 +227,8 @@ Docs: https://7ots.com/llms.txt`,
192
227
  'pet.detached': '{name} vive ahora en segundo plano. Para dormirlo: 7ots stop',
193
228
  'pet.notRunning': 'La mascota no está corriendo (lánzala con: 7ots pet). Lo guardo para después.',
194
229
  'pet.stopped': 'Buenas noches, {name}.',
195
- 'pet.electron': 'Abriendo la mascota de escritorio (la primera vez npx descarga Electron, ~100 MB)…',
230
+ 'pet.electron': 'Abriendo la mascota de escritorio…',
231
+ 'pet.electronInstall': 'Primera vez: instalo Electron en {dir} (~100 MB, una sola vez)…',
196
232
  'pet.electronFail': 'No pude abrir la ventana de escritorio ({error}); abro el navegador.',
197
233
  'pet.chooseMode': '¿Cómo quieres que viva {name} en tu computador?',
198
234
  'pet.status': '{name} · nivel {level} · comida {food} · energía {energy} · diversión {fun}{asleep}',
@@ -214,6 +250,8 @@ Docs: https://7ots.com/llms.txt`,
214
250
  'login.open': 'Autoriza 7ots en tu navegador:\n {url}',
215
251
  'login.ok': 'Conectado a Orquesta ({org}).',
216
252
  'err.unknown': 'Comando desconocido «{cmd}». Prueba: 7ots help',
253
+ 'err.noPet': 'La mascota no está despierta. Iníciala con: 7ots pet',
254
+ 'usage.ask': '7ots ask <texto>\n\n ej. 7ots ask "recuérdame ver el correo en 10 minutos y ábrelo"\n 7ots ask "anota que los viernes hay demo" · 7ots ask "mis recordatorios"\n Ver tu pantalla o tus procesos: muy pronto.',
217
255
  'err.noIdentity': 'Aún no hay identidad. Crea una con «7ots new» o corre «7ots setup».',
218
256
  'w.title': 'Configuración de 7ots — {name}',
219
257
  'w.intro': 'Tu ot en 7 pasos cortos. Las claves se quedan en este computador.',
@@ -312,11 +350,16 @@ Docs: https://7ots.com/llms.txt`,
312
350
  7ots show [--json|--prompt] mostra a identidade (do projeto ou a global)
313
351
  7ots install [--target html|self|print] [arquivo] coloca a identidade neste projeto
314
352
  7ots setup assistente: onde mora, cérebro, voz, bichinho, hooks
315
- 7ots pet [--mode desktop|terminal|browser|auto] [--detach] acorda seu bichinho virtual
316
- 7ots feed | play | sleep | wake | status | say <texto> | stop
353
+ 7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] acorda seu bichinho virtual
354
+ 7ots pet --tmux o bichinho de terminal num painel do tmux
355
+ 7ots feed | play | sleep | wake | say <texto> | stop
356
+ 7ots status [--line|--json] estado (--line: uma linha para tmux)
357
+ 7ots ask <texto> fale com ele: "me lembre de ver o e-mail em 10 min", "abra minha agenda", "anote que…"
358
+ 7ots reminders [--cancel <id>] lembretes pendentes e o que ele lembra
317
359
  7ots hooks install|remove [--claude] [--shell] [--global]
318
360
  7ots meet --new | "<convite ou link>" entra no apuchat meet com esta identidade
319
361
  7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
362
+ 7ots docs tudo para agentes de código (igual a https://7ots.com/llms.txt)
320
363
  7ots login orquesta conecta uma conta do getorquesta.com
321
364
  7ots server sobe o proxy 7ots (/api/agent) junto do seu site
322
365
 
@@ -329,6 +372,17 @@ Docs: https://7ots.com/llms.txt`,
329
372
  'install.html': 'Widget do 7ots adicionado em {file}.',
330
373
  'install.htmlUpdated': 'Widget do 7ots atualizado em {file}.',
331
374
  'install.noHtml': 'Não achei um HTML. Cole isto antes de </body>:',
375
+ 'install.pasteIn': "É um projeto {name}: cole isto em {file}, antes de </body> (ou no layout raiz):",
376
+ 'install.notWeb': "Esta pasta não é um site, então não há onde pôr o widget. {name} ainda pode:\n • morar no seu computador: 7ots pet\n • entrar numa videochamada: 7ots meet --new\n • morar num site seu: 7ots install <caminho/para/index.html> (ou 7ots install --target print)\n • ser hospedado no 7ots.com: {app}",
377
+ 'pet.where': "Procure {name} na parte de baixo da tela (clique direito nele, ou o ícone da bandeja, para o menu). Aparece um quadro? 7ots pet --opaque",
378
+ 'pet.tmuxNo': "Você não está no tmux. Rode dentro de uma sessão do tmux, ou use: 7ots pet --mode terminal",
379
+ 'pet.tmuxOk': "{name} está num painel do tmux à direita. Para a barra de status adicione ao ~/.tmux.conf:\n set -g status-right \"#(7ots status --line)\"",
380
+ 'usage.pet': "7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] [--3d] [--tmux] [--compact]\n\n --mode desktop: uma janelinha na tela (Electron, ~100 MB na primeira vez)\n terminal: desenhado neste terminal · browser: uma aba · auto: desktop se houver tela\n --detach volta na hora (o bichinho segue em segundo plano; pare com 7ots stop)\n --opaque fundo sólido no modo desktop (quando a transparência mostra um quadro, comum no Linux)\n --3d o ot como pelúcia 3D (WebGL; sem ele, 2D)\n --tmux abre o bichinho de terminal num painel lateral do tmux\n --compact desenho pequeno no terminal (para painéis estreitos)\n\nCuidados: 7ots feed | play | sleep | wake | say <texto> | status [--line] | stop",
381
+ 'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
382
+ 'usage.install': "7ots install [--target html|self|print] [arquivo]\n\n html (padrão) insere o widget antes de </body> do index.html (., public/, src/, static/, www/, site/) ou [arquivo]\n self prepara a identidade para o seu próprio servidor 7ots\n print só imprime o snippet",
383
+ 'usage.hooks': "7ots hooks install|remove [--claude] [--shell] [--global]\n\n --claude hooks do Claude Code (este projeto, ou todos com --global)\n --shell hook de bash/zsh (~/.bashrc, ~/.zshrc)",
384
+ 'usage.meet': "7ots meet --new [--minutes n]\n7ots meet \"<texto do convite ou link https://meet.apuchat.com/call#c=…&t=…&p=…>\" [--minutes n]",
385
+ 'usage.status': "7ots status [--line|--json]\n\n --line uma linha curta (para a barra do tmux: set -g status-right \"#(7ots status --line)\")",
332
386
  'install.endpointLocal': 'O widget fala com um proxy 7ots em /api/agent. Suba um junto do seu site:\n IDENTITY_FILE={file} npx -y @7ots/cli server\n(ou hospede no 7ots.com: {app})',
333
387
  'install.self': 'Identidade pronta para o seu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
334
388
  'install.platform': 'Hospede no 7ots.com com esta mesma identidade: {app}',
@@ -342,7 +396,8 @@ Docs: https://7ots.com/llms.txt`,
342
396
  'pet.detached': '{name} agora mora em segundo plano. Para parar: 7ots stop',
343
397
  'pet.notRunning': 'O bichinho não está rodando (inicie com: 7ots pet). Guardado para depois.',
344
398
  'pet.stopped': 'Boa noite, {name}.',
345
- 'pet.electron': 'Abrindo o bichinho de desktop (na primeira vez o npx baixa o Electron, ~100 MB)…',
399
+ 'pet.electron': 'Abrindo o bichinho de desktop…',
400
+ 'pet.electronInstall': 'Primeira vez: instalando o Electron em {dir} (~100 MB, uma vez só)…',
346
401
  'pet.electronFail': 'Não consegui abrir a janela de desktop ({error}); abrindo o navegador.',
347
402
  'pet.chooseMode': 'Como {name} deve morar no seu computador?',
348
403
  'pet.status': '{name} · nível {level} · comida {food} · energia {energy} · diversão {fun}{asleep}',
@@ -364,6 +419,8 @@ Docs: https://7ots.com/llms.txt`,
364
419
  'login.open': 'Autorize o 7ots no navegador:\n {url}',
365
420
  'login.ok': 'Conectado ao Orquesta ({org}).',
366
421
  'err.unknown': 'Comando desconhecido «{cmd}». Tente: 7ots help',
422
+ 'err.noPet': 'O bichinho não está acordado. Inicie com: 7ots pet',
423
+ 'usage.ask': '7ots ask <texto>\n\n ex. 7ots ask "me lembre de ver o e-mail em 10 minutos e abra"\n 7ots ask "anote que às sextas tem demo" · 7ots ask "meus lembretes"\n Ver sua tela ou processos: em breve.',
367
424
  'err.noIdentity': 'Ainda não há identidade. Crie uma com «7ots new» ou rode «7ots setup».',
368
425
  'w.title': 'Configuração do 7ots — {name}',
369
426
  'w.intro': 'Seu ot em 7 passos curtos. As chaves ficam neste computador.',
@@ -459,12 +516,22 @@ Docs: https://7ots.com/llms.txt`,
459
516
 
460
517
  let current = null;
461
518
 
519
+ /** The ot's language (project identity, else the global one): its texts follow it. */
520
+ function identityLang() {
521
+ for (const f of [projectFile('identity.json'), homeFile('identity.json')]) {
522
+ const l = readJson(f)?.language;
523
+ if (l) return String(l).slice(0, 2).toLowerCase();
524
+ }
525
+ return '';
526
+ }
527
+
462
528
  export function cliLang() {
463
529
  if (current) return current;
464
530
  const pick = (v) => (['en', 'es', 'pt'].includes(v) ? v : '');
465
531
  current =
466
532
  pick(process.env.SEVENOTS_LANG) ||
467
533
  pick(readJson(homeFile('config.json'), {})?.lang) ||
534
+ pick(identityLang()) ||
468
535
  pick(String(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '').slice(0, 2).toLowerCase()) ||
469
536
  'en';
470
537
  return current;
@@ -15,7 +15,7 @@ import { join, resolve, relative } from 'node:path';
15
15
  import { existsSync } from './paths.mjs';
16
16
  import { loadConfig } from './config.mjs';
17
17
 
18
- const CDN = 'https://cdn.jsdelivr.net/npm/7ots/dist/7ots.iife.js';
18
+ const CDN = 'https://cdn.jsdelivr.net/npm/@7ots/cli/dist/7ots.iife.js';
19
19
  const START = '<!-- 7ots -->';
20
20
  const END = '<!-- /7ots -->';
21
21
 
@@ -29,6 +29,34 @@ export function findHtml(cwd = process.cwd()) {
29
29
  return null;
30
30
  }
31
31
 
32
+ /**
33
+ * What kind of project this is, to say where the snippet goes when there is no plain index.html.
34
+ * @returns {{ kind: 'framework'|'web'|'none', name?: string, file?: string }}
35
+ */
36
+ export function detectProject(cwd = process.cwd()) {
37
+ let pkg = null;
38
+ try {
39
+ pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
40
+ } catch {}
41
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
42
+ const first = (files) => files.find((f) => existsSync(join(cwd, f)));
43
+ const FRAMEWORKS = [
44
+ ['next', 'Next.js', ['app/layout.tsx', 'app/layout.jsx', 'app/layout.js', 'src/app/layout.tsx', 'src/app/layout.jsx', 'pages/_document.tsx', 'pages/_document.js', 'src/pages/_document.tsx']],
45
+ ['nuxt', 'Nuxt', ['app.vue', 'nuxt.config.ts', 'nuxt.config.js']],
46
+ ['astro', 'Astro', ['src/layouts/Layout.astro', 'src/layouts/BaseLayout.astro']],
47
+ ['@sveltejs/kit', 'SvelteKit', ['src/app.html']],
48
+ ['@remix-run/react', 'Remix', ['app/root.tsx', 'app/root.jsx']],
49
+ ['gatsby', 'Gatsby', ['src/html.js', 'gatsby-ssr.js']],
50
+ ['vite', 'Vite', ['index.html']],
51
+ ];
52
+ // No layout file yet: name the usual one, so the user still knows where it goes.
53
+ for (const [dep, name, files] of FRAMEWORKS) if (deps[dep]) return { kind: 'framework', name, file: first(files) || files[0] };
54
+ const web = first(['templates/base.html', 'templates/layout.html', 'app/views/layouts/application.html.erb', 'resources/views/layouts/app.blade.php', 'layouts/_default/baseof.html', '_layouts/default.html']);
55
+ if (web) return { kind: 'web', file: web };
56
+ if (pkg && (deps.react || deps.vue || deps.svelte || deps['solid-js'] || deps.express || deps.fastify)) return { kind: 'web' };
57
+ return { kind: 'none' };
58
+ }
59
+
32
60
  /** The HTML block for this identity and home. */
33
61
  export function snippet(identity, { home = loadConfig().home, identityUrl } = {}) {
34
62
  if (home?.kind === '7ots' && /\/api\/o\/[\w-]+\/?$/.test(home.url || '')) {
@@ -15,7 +15,7 @@ const HOUR = 3600_000;
15
15
  const clamp = (n) => Math.max(0, Math.min(100, Math.round(n * 10) / 10));
16
16
 
17
17
  export function newPet(now = Date.now()) {
18
- return { food: 80, energy: 90, fun: 70, xp: 0, level: 1, asleep: false, born: now, last: now, lastTalk: 0, lastSeen: now, stats: { ok: 0, fail: 0, turns: 0 } };
18
+ return { food: 90, energy: 100, fun: 80, xp: 0, level: 1, asleep: false, born: now, last: now, lastTalk: 0, lastSeen: now, stats: { ok: 0, fail: 0, turns: 0 } };
19
19
  }
20
20
 
21
21
  export function loadPet() {
@@ -31,7 +31,8 @@ export const xpForLevel = (level) => 20 * level * level;
31
31
 
32
32
  /** Time passes: needs decay (slower while asleep, energy recovers). */
33
33
  export function tick(p, now = Date.now()) {
34
- const h = Math.max(0, Math.min(48, (now - p.last) / HOUR));
34
+ // Time away counts up to 8 h: coming back after a weekend finds it peckish, not starving.
35
+ const h = Math.max(0, Math.min(8, (now - p.last) / HOUR));
35
36
  p.last = now;
36
37
  if (!h) return p;
37
38
  if (p.asleep) {
@@ -8,6 +8,9 @@
8
8
  * GET /events?token=… SSE: { state, say?, gesture?, effect?, morph?, activity? }
9
9
  * POST /event X-7ots-Token { type: tool|shell|prompt|stop|notify|start|feed|play|sleep|wake|say, … }
10
10
  * POST /tts X-7ots-Token { text } → audio with the configured voice
11
+ * POST /ask X-7ots-Token { text } → { reply, actions } reminders, notes, open a site (assistant.mjs)
12
+ * GET /reminders?token=… { reminders, notes }
13
+ * POST /reminders/cancel { id }
11
14
  * POST /quit X-7ots-Token
12
15
  *
13
16
  * Only listens on 127.0.0.1 and only answers to Host 127.0.0.1/localhost (no DNS rebinding).
@@ -18,6 +21,7 @@ import { createServer } from 'node:http';
18
21
  import { randomBytes, timingSafeEqual } from 'node:crypto';
19
22
  import { readFileSync, existsSync, writeFileSync, chmodSync } from 'node:fs';
20
23
  import { join } from 'node:path';
24
+ import { createAssistant } from './assistant.mjs';
21
25
  import { createBrain, petSystem } from './brain.mjs';
22
26
  import { loadConfig, env } from './config.mjs';
23
27
  import { loadIdentity } from './identity.mjs';
@@ -189,6 +193,20 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
189
193
  broadcast({});
190
194
  }, 60_000);
191
195
 
196
+ // The assistant: reminders that speak, notify and open the tab; notes it learns.
197
+ const assistant = createAssistant({
198
+ brain,
199
+ lang,
200
+ name: identity.name,
201
+ log,
202
+ system: () => petSystem(identity, { lang, role: 'assistant' }),
203
+ onFire: (r, text) => {
204
+ lastSpoke = Date.now();
205
+ pet.lastTalk = lastSpoke;
206
+ broadcast({ say: text, gesture: 'wave', effect: 'exclaim', reminder: r });
207
+ },
208
+ });
209
+
192
210
  // Home on getorquesta.com: comment on what the project's agent does.
193
211
  let stopWatch = () => {};
194
212
  if (config.home.kind === 'orquesta' && config.home.projectId && env().ORQUESTA_TOKEN) {
@@ -243,6 +261,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
243
261
  req.on('close', () => clients.delete(res));
244
262
  return;
245
263
  }
264
+ if (req.method === 'GET' && p === '/reminders') return json(200, { reminders: assistant.reminders, notes: assistant.notes });
246
265
  if (req.method !== 'POST') return json(405, { error: 'method' });
247
266
  let body = '';
248
267
  for await (const c of req) if ((body += c).length > 64 * 1024) return json(413, { error: 'too large' });
@@ -269,6 +288,16 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
269
288
  return json(e.status || 502, { error: e.message });
270
289
  }
271
290
  }
291
+ if (p === '/ask') {
292
+ const text = String(data.text || '').trim().slice(0, 800);
293
+ if (!text) return json(400, { error: 'text' });
294
+ broadcast({ thinking: true, activity: track({ type: 'prompt', text }) });
295
+ const r = await assistant.ask(text);
296
+ lastSpoke = Date.now();
297
+ broadcast({ thinking: false, say: r.reply, gesture: r.actions.length ? 'nod' : null });
298
+ return json(200, r);
299
+ }
300
+ if (p === '/reminders/cancel') return json(200, { ok: assistant.cancel(String(data.id || '')) });
272
301
  if (p === '/quit') {
273
302
  json(200, { ok: true });
274
303
  return stop();
@@ -278,6 +307,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
278
307
 
279
308
  function stop() {
280
309
  clearInterval(timer);
310
+ assistant.stop();
281
311
  stopWatch();
282
312
  savePet(pet);
283
313
  for (const c of clients) c.end();
@@ -55,18 +55,210 @@ export function promptText(topic = 'all') {
55
55
  return PROMPTS[topic];
56
56
  }
57
57
 
58
- /** https://7ots.com/llms.txt */
58
+ /** https://7ots.com/llms.txt — everything a coding agent needs to set up 7ots without guessing. */
59
59
  export function llmsTxt(base = 'https://7ots.com') {
60
60
  return `# 7ots
61
61
 
62
62
  > Open-source embeddable web agent: it sees the page, speaks and acts for your visitor. One HTML tag, your server, your brand.
63
- > Every ot has an identity (face, personality, voice, meet avatar) reproducible from a short seed, and can also live on
64
- > your computer as a virtual pet and join apuchat meet video calls.
63
+ > Every ot has an identity (face, personality, voice, meet avatar) reproducible from a short seed. The same ot can live
64
+ > on a website (widget), on the user's computer (a virtual pet: desktop window, terminal or tmux), in apuchat meet
65
+ > video calls, and answer email (apumail) and chat (apuchat). 7ots integrates those services; it does not host audio or mail.
65
66
 
66
- - Site: ${base}/
67
- - Create an identity in the browser: ${base}/#create
68
- - Hosted ots (dashboard): ${base}/app/
69
- - npm: https://www.npmjs.com/package/7ots (CLI: \`npx @7ots/cli help\`)
67
+ - Site: ${base}/ · create an identity in the browser: ${base}/#create
68
+ - Hosted ots (dashboard, no server needed): ${base}/app/
69
+ - CLI: npm \`@7ots/cli\` (https://www.npmjs.com/package/@7ots/cli) → \`npx -y @7ots/cli help\`, \`npx -y @7ots/cli <cmd> --help\`
70
+ - This file: ${base}/llms.txt (also \`npx -y @7ots/cli docs\`)
71
+
72
+ # Read this first (for coding agents)
73
+
74
+ 1. Never invent identity fields or config files by hand: use the CLI, it validates everything.
75
+ 2. Ask the user only what you can't tell from the project. The usual question is WHERE the ot should live:
76
+ - a website in this repo → widget (section "Website");
77
+ - not a website (library, backend, CLI, data…) → the pet on their computer, a meet call, or hosting on ${base}/app/;
78
+ - "on my computer" / "a tamagotchi" → the pet (section "Pet").
79
+ 3. From an agent, commands must return: use \`--detach\` for the pet, run \`meet\` in the background, and ask the user to run
80
+ interactive commands themselves (\`setup\`, the terminal pet) — in Claude Code: \`! npx -y @7ots/cli setup\`.
81
+ 4. Language: \`--lang en|es|pt\` on \`new\` sets the ot's language, and from then on every CLI text follows it
82
+ (order: SEVENOTS_LANG > ~/.7ots/config.json "lang" > the ot's language > LANG > English).
83
+ 5. Secrets (API keys, tokens) never go in HTML, in identity.json or in your reply: they go in env vars / .env of a server,
84
+ in ~/.7ots/keys.json (written by \`setup\`, 0600) or in the ${base}/app/ dashboard.
85
+ 6. When done, tell the user: the ot's name and seed, where it lives, and how to see it (URL, \`7ots pet\`, …).
86
+
87
+ # Typical flows
88
+
89
+ - "Use 7ots" in a website repo:
90
+ \`npx -y @7ots/cli new --lang <en|es|pt>\` → \`npx -y @7ots/cli install\` → tell the user which AI endpoint the widget uses.
91
+ - "Give me a pet":
92
+ \`npx -y @7ots/cli new\` (if there is no identity) → \`npx -y @7ots/cli pet --mode desktop --detach\` → \`npx -y @7ots/cli hooks install --claude\`.
93
+ - "Put it in a call": \`npx -y @7ots/cli meet --new\` (in the background) and give the printed link only to the user.
94
+ - "Host it, no server": create it at ${base}/app/ (or open the link \`install\` prints) and paste its one-line embed.
95
+
96
+ # CLI reference (\`npx -y @7ots/cli <command>\`, or \`7ots <command>\` once installed globally)
97
+
98
+ Every command accepts \`--help\` (prints usage, never launches anything) and \`--lang en|es|pt\`.
99
+
100
+ | Command | What it does |
101
+ |---|---|
102
+ | \`new [--seed s] [--name n] [--lang en\\|es\\|pt] [--global] [--force] [--json]\` | Random identity → \`.7ots/identity.json\` (\`--global\`: \`~/.7ots/identity.json\`, shared by every project). Keeps an existing one unless \`--force\`. |
103
+ | \`show [--json\\|--prompt]\` | The identity in use (the project's, else the global one). \`--prompt\`: as a system prompt. |
104
+ | \`seed <text>\` | Normalizes a seed. Same seed = same ot everywhere. |
105
+ | \`install [--target html\\|self\\|print] [file]\` | Puts the widget in the project (see "Website"). |
106
+ | \`setup\` (alias \`wizard\`, \`init\`) | Interactive wizard: where the ot lives, brain, voice, pet mode, chattiness, hooks. Writes \`~/.7ots/config.json\` and \`keys.json\`. |
107
+ | \`pet [--mode desktop\\|terminal\\|browser\\|auto] [--detach] [--opaque] [--tmux] [--compact]\` | Wakes the pet (see "Pet"). |
108
+ | \`feed\` · \`play\` · \`sleep\` · \`wake\` · \`say <text>\` | Care for the pet (works even if it is not running: the state is saved). |
109
+ | \`status [--line\\|--json]\` | Level, food, energy, fun. \`--line\`: one short line for status bars. |
110
+ | \`stop\` | Stops the pet daemon (and its windows). |
111
+ | \`hooks install\\|remove [--claude] [--shell] [--global]\` | Lets the pet react to your work (see "Hooks"). |
112
+ | \`meet --new [--minutes n]\` · \`meet "<invite or link>"\` | The ot joins an apuchat meet video call. |
113
+ | \`prompt [identity\\|install\\|pet\\|meet\\|all]\` | Short recipes for coding agents. |
114
+ | \`docs\` | Prints this file. |
115
+ | \`login orquesta\` | Connects a getorquesta.com account (brain "batuta", home "orquesta"). |
116
+ | \`server\` | Runs the 7ots proxy (\`/api/agent\`) with the env vars below. |
117
+ | \`version\` · \`help\` | |
118
+
119
+ # Files
120
+
121
+ | Path | Content |
122
+ |---|---|
123
+ | \`.7ots/identity.json\` (project) / \`~/.7ots/identity.json\` (global) | The identity. Public data only (name, role, look, voice, personality, contact). Commit it if you want. |
124
+ | \`~/.7ots/config.json\` | Written by \`setup\`: \`lang\`, \`home\`, \`brain\`, \`voice\`, \`pet\` (below). |
125
+ | \`~/.7ots/keys.json\` (0600) | Keys typed in \`setup\`, under their env-var names. Env vars always win. Never print it. |
126
+ | \`~/.7ots/pet.json\` · \`pet.log\` · \`pet.token\` | Pet state, daemon log, local auth token. |
127
+ | \`SEVENOTS_HOME\` | Moves \`~/.7ots\` elsewhere. \`SEVENOTS_PET_PORT\` (default 7717) moves the pet daemon. \`SEVENOTS_LANG\` forces the CLI language. |
128
+
129
+ Identity fields: \`id, name, role, tagline, seed, bio, language, languages, personality{tone, traits, instructions}, look{color, …},
130
+ voice{provider, id, lang}, contact{…}\`. Change them with \`new --force\`, the web editor (${base}/#create) or the backoffice, not by hand.
131
+
132
+ ## config.json
133
+
134
+ \`\`\`
135
+ home { kind: 'local' } only on this computer
136
+ { kind: '7ots', url: '${base}/api/o/<id>' } hosted on 7ots.com
137
+ { kind: 'server', url: 'https://site/api/agent' } the user's own 7ots server
138
+ { kind: 'orquesta', projectId, projectName } a getorquesta.com project
139
+ brain { kind: 'lines' } built-in phrases, no AI (default)
140
+ { kind: 'cli', cli: 'claude'|'codex'|'gemini'|'ollama'|'custom', model?, command? } the user's own AI CLI
141
+ { kind: 'api', provider: 'anthropic'|'openai', model?, baseUrl? } a key in keys.json/env
142
+ { kind: '7ots' } the home's /chat (7ots.com or own server)
143
+ { kind: 'batuta', model? } Orquesta's Batuta
144
+ voice { kind: 'none'|'browser'|'apuchat'|'elevenlabs'|'grok'|'fish'|'openai', voiceId? }
145
+ pet { mode: 'ask'|'desktop'|'terminal'|'browser', annoy: 0..3, opaque?: true }
146
+ annoy: 0 silent · 1 rarely · 2 normal · 3 chatty (+ terminal bell)
147
+ \`\`\`
148
+ Keys by name: ANTHROPIC_API_KEY, OPENAI_API_KEY, ORQUESTA_TOKEN, APUCHAT_VOICE_TOKEN, ELEVENLABS_API_KEY, XAI_API_KEY, FISH_API_KEY.
149
+
150
+ # Pet
151
+
152
+ - \`desktop\`: a small always-on-top window that walks along the bottom of the screen (Electron via npx, ~100 MB the first time).
153
+ Find it bottom-right; menu on right click or the tray icon (Show / Hide / Feed / Play / Sleep / Quit). Hide keeps it alive.
154
+ - \`terminal\`: drawn in the current terminal, keys f feed · p play · s sleep/wake · q quit. Needs its own terminal: ask the user.
155
+ - \`browser\`: a tab at http://127.0.0.1:7717/pet.
156
+ - \`auto\`: desktop if there is a screen, else terminal.
157
+ - \`--detach\`: return immediately (always from agents). The daemon keeps running until \`7ots stop\`.
158
+ - tmux: \`7ots pet --tmux\` opens the terminal pet in a side pane (\`--compact\` for a narrow one); for the status bar add
159
+ \`set -g status-right "#(7ots status --line)"\` to ~/.tmux.conf.
160
+ - Needs: food, energy and fun decay with time (time away counts at most 8 h); it gains XP and levels from passing
161
+ commands and finished agent turns.
162
+
163
+ Troubleshooting:
164
+ - A grey/black box behind the pet (Linux without a compositor, some GPUs): \`7ots pet --opaque\` (or \`"pet": {"opaque": true}\` in config.json).
165
+ - Can't see it: look at the tray icon, or \`7ots stop && 7ots pet --mode desktop\` (it comes back bottom-right).
166
+ - Electron can't start (no display, headless, WSL): it falls back to the browser tab; or use \`--mode terminal\`.
167
+ - Port busy: \`SEVENOTS_PET_PORT=7720\`.
168
+
169
+ # Hooks
170
+
171
+ - \`hooks install --claude\`: Claude Code hooks in \`.claude/settings.local.json\` of this project (\`--global\`: ~/.claude/settings.json).
172
+ The pet hears tool calls, errors and finished turns (\`7ots event --from claude\`, reads the hook JSON on stdin; never fails).
173
+ - \`hooks install --shell\`: bash/zsh hook in ~/.bashrc / ~/.zshrc: the pet hears commands and exit codes (secrets are redacted).
174
+ - \`hooks remove\` undoes both. Without a running pet the events still change its state silently.
175
+
176
+ # Website (widget)
177
+
178
+ \`npx -y @7ots/cli install\` detects the project:
179
+ - plain HTML (index.html in ., public/, src/, static/, www/, site/) → inserts the widget before </body> between
180
+ \`<!-- 7ots -->\` markers (idempotent: re-running replaces the block);
181
+ - a framework (Next, Nuxt, Astro, SvelteKit, Remix, Gatsby, Vite, Django, Rails, Laravel, Hugo, Jekyll…) → prints the snippet and
182
+ which file to paste it in (root layout/template, before </body>);
183
+ - not a website → says so and suggests the pet, meet or hosting;
184
+ - \`--target print\` only prints the snippet; \`--target self\` prepares the identity for the user's own server.
185
+
186
+ Snippet for an ot hosted on 7ots.com (simplest; allowed domains are set in the dashboard):
187
+ \`\`\`html
188
+ <script src="${base}/api/o/<id>/embed.js" defer></script>
189
+ \`\`\`
190
+ Snippet with your own proxy:
191
+ \`\`\`html
192
+ <script src="https://cdn.jsdelivr.net/npm/@7ots/cli/dist/7ots.iife.js" defer></script>
193
+ <script>
194
+ addEventListener('DOMContentLoaded', () => SevenOts.init({ endpoint: '/api/agent', identity: true }));
195
+ </script>
196
+ \`\`\`
197
+ Declarative: \`<ots-agent endpoint="/api/agent" site-key="pk_x" name="Ana"></ots-agent>\`. ES module: \`import { init } from '@7ots/cli'\`.
198
+
199
+ \`SevenOts.init(options)\`:
200
+ | Option | Meaning |
201
+ |---|---|
202
+ | \`endpoint\` | Proxy URL (\`/api/agent\`, \`${base}/api/o/<id>\`, …). Required. |
203
+ | \`siteKey\` | Public site id the proxy may require (\`SITE_KEYS\`). |
204
+ | \`identity\` | \`true\` (ask the endpoint), a URL, or an inline identity object (public fields only). |
205
+ | \`agent\` | \`{ name, role, siteName, language, instructions, expressive }\`. |
206
+ | \`avatar\` | \`{ url, body: 'F'\\|'M', mood, cameraView }\` (3D, needs an importmap for three) or \`false\`; default is the 2D character. |
207
+ | \`voice\` | \`{ tts: 'proxy'\\|'browser'\\|false, stt: true, lang }\` or \`false\`. |
208
+ | \`auth\` | \`{ token }\`, \`{ getToken, getUser }\` or \`{ credentials: 'include' }\`; \`exposeClaims\`. |
209
+ | \`mcp\` | \`[{ url, name, requiresAuth, filter, confirm }]\` MCP servers (Streamable HTTP). |
210
+ | \`actions\` | Custom tools \`[{ name, description, parameters, handler, confirm }]\`. |
211
+ | \`templates\` | \`{ name: (data, { escape }) => html }\` for show_modal / open_sidebar. |
212
+ | \`navigation\` | \`{ allowedOrigins, router }\` (\`router\` for SPAs). |
213
+ | \`proactive\` | \`{ level: 'quiet'\\|'normal'\\|'bold', greetDelayMs, dwellMs, idleMs, cooldownMs, maxPerSession, … }\` or \`false\`. |
214
+ | \`context\` | \`{ privateSelectors, ignoreSelectors, extra: () => ({...}) }\` — what it may read on the page. |
215
+ | \`contact\` | \`{ apuchat: true, apumail: true \\| { categories } }\` or \`false\` — hand off to a human. |
216
+ | \`builtins\` | \`{ exclude: ['click', …], confirmClicks: 'submit'\\|'all'\\|'none' }\`. |
217
+ | \`pointer\` | \`{ speed, visible }\` or \`false\` — the virtual mouse/keyboard. |
218
+ | \`pageTools\` | \`true\` (default): forms/buttons with \`data-ots-tool\` become tools. |
219
+ | \`mode\` / \`companion\` | \`'panel'\` (default) or \`'companion'\` (the avatar walks the page); \`{ size, idleHomeMs, follow, wanderMs, watchCursor }\`. |
220
+ | \`theme\` | \`{ primary, radius, position: 'right'\\|'left', font }\`. |
221
+
222
+ HTML tools without JS: \`<form data-ots-tool="invite_member" data-ots-description="…" data-ots-confirm="Invite {email}?" data-ots-auth>\`
223
+ (named fields become parameters). Runtime API: \`agent.ask(text)\`, \`say\`, \`notify\`, \`registerAction\`, \`registerMcp\`,
224
+ \`setAuthToken\`, \`setIdentity\`, \`setProactivity\`, \`open/close/reset/destroy\`, \`on(event, fn)\`.
225
+
226
+ # Own server (\`npx -y @7ots/cli server\`, or \`node server/server.mjs\` from the repo)
227
+
228
+ Reads env vars / \`.env\`. Keys live only here. Admin UI at /backoffice/ (localhost only unless ADMIN_PASSWORD).
229
+ - Core: PORT (8787), ALLOWED_ORIGINS (comma list), SITE_KEYS, RATE_LIMIT_PER_MIN (40), TRUST_PROXY, SERVER_INSTRUCTIONS, LOCALE (es|en|pt), LOG_LEVEL.
230
+ - LLM: LLM_PROVIDER (anthropic | openai | mock), ANTHROPIC_API_KEY, OPENAI_API_KEY, LLM_MODEL, LLM_BASE_URL (OpenAI-compatible:
231
+ DeepSeek, Groq, Ollama, OpenRouter…), LLM_EFFORT (low…max), LLM_MAX_TOKENS, LLM_FALLBACKS (on|off).
232
+ - Voice (TTS): TTS_PROVIDER (apuchat | elevenlabs | grok | fish | openai | none = browser voice; empty = first with a key);
233
+ APUCHAT_VOICE_URL, APUCHAT_VOICE_TOKEN, APUCHAT_VOICE_ID, APUCHAT_VOICE_PROVIDER; ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID,
234
+ ELEVENLABS_MODEL; XAI_API_KEY, XAI_TTS_VOICE; FISH_API_KEY, FISH_VOICE_ID, FISH_MODEL; OPENAI_TTS_MODEL, OPENAI_TTS_VOICE.
235
+ - Email tickets (apumail): APUMAIL_API, APUMAIL_INBOX, APUMAIL_INBOX_TOKEN, APUMAIL_TO.
236
+ - Human handoff (apuchat): APUCHAT_HUB, APUCHAT_NOTIFIER_IDENTITY_KEY, APUCHAT_OPERATOR_HANDLE, APUCHAT_TRANSCRIBE.
237
+ - Backoffice: ADMIN_PASSWORD, CONFIG_FILE, SECRETS_FILE, IDENTITY_FILE (default data/identity.json).
238
+ - The agent outside the web: AGENT_NAME, AGENT_ROLE, AGENT_SITE_URL, AGENT_INSTRUCTIONS, AGENT_LANG; own mailbox APUMAIL_AGENT_INBOX,
239
+ APUMAIL_AGENT_TOKEN, APUMAIL_AGENT_WEBHOOK_SECRET, APUMAIL_AGENT_DAILY; own apuchat/meet identity APUCHAT_AGENT_IDENTITY_KEY,
240
+ APUCHAT_AGENT_AVATAR, APUCHAT_AGENT_SCENE, APUCHAT_AGENT_ALLOW, AGENT_MAX_CALLS, AGENT_CALL_MAX_MINUTES, AGENT_CALL_MAX_TURNS.
241
+ - Demo: DEMO, SERVE_STATIC, DEMO_JWT_SECRET.
242
+ - Platform (multi-tenant, what ${base} runs): PLATFORM=true, PLATFORM_URL, PLATFORM_DB, PLATFORM_SECRET, PLATFORM_MAX_OTS,
243
+ PLATFORM_OTS_RATE_PER_MIN, PLATFORM_OTS_DAILY_LIMIT, PLATFORM_NEW_ACCOUNTS_PER_IP_DAY, PLATFORM_MAX_INFLIGHT,
244
+ PLATFORM_LLM_PROVIDER/API_KEY/BASE_URL/MODEL, PLATFORM_FREE_MESSAGES, PLATFORM_APUCHAT_VOICE_TOKEN, PLATFORM_FREE_TTS,
245
+ PLATFORM_MAIL_INBOX/TOKEN, NOTLOGIN_URL/CLIENT_ID/CLIENT_SECRET, PLATFORM_APUCHAT_IDENTITIES_PER_DAY (free apuchat
246
+ identities per account and day, default 5), ORQUESTA_URL (default https://getorquesta.com).
247
+ Endpoints: POST /api/agent/chat, POST /api/agent/tts, GET /api/agent/identity, /api/agent/contact/*, /llms.txt.
248
+
249
+ # Hosted on ${base} (no server)
250
+
251
+ - Sign in at ${base}/app/, create an ot (\`7ots install\` also prints a link that opens this same identity there), set the allowed domains, copy the embed.
252
+ - Each ot's public API: \`${base}/api/o/<id>/\` — embed.js, config, identity, identity.vcf, chat, tts, contact/apumail, contact/apuchat.
253
+ Point the CLI at it with \`setup\` → home "7ots" (brain "7ots" then uses its /chat).
254
+ - Brain: the account's free monthly quota on the platform's AI, or the user's own key (BYOK) set in the ot's settings.
255
+ Same for voice. Keys are encrypted and never shown again. Per-ot daily and per-minute limits apply.
256
+ - Settings per ot in /backoffice/?ots=<id>: instructions, LLM/voice keys, apumail/apuchat channels.
257
+ - apuchat tab of an ot: a free @handle (expires after 24h without activity, e.g. while the ot is paused), made permanent
258
+ by paying once by card on apuchat; connect your apuchat account to keep paid identities under it, or paste an identity key you own.
259
+ - Orquesta tab of an ot: connect Orquesta (OAuth or an oak_ key), pick a project, enable tasks. Then the ot can start Orquesta
260
+ tasks (run_task, task_status) only for its owner in the dashboard ("Ask your ot to do something") and for apuchat DMs /
261
+ apumail emails from senders on the ot's allowlist — never from the public chat (/api/o/<id>/chat). Daily cap per ot (default 20).
70
262
 
71
263
  # Recipes for coding agents
72
264