@7ots/cli 0.1.0 → 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 (50) hide show
  1. package/.env.example +17 -1
  2. package/cli/7ots.mjs +136 -36
  3. package/cli/lib/assistant.mjs +337 -0
  4. package/cli/lib/brain.mjs +10 -7
  5. package/cli/lib/i18n.mjs +87 -17
  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 +200 -8
  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 +199 -7
  18. package/package.json +1 -2
  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/skills/7ots/SKILL.md +1 -1
  33. package/src/avatar/AvatarStage.js +54 -9
  34. package/src/avatar/three.js +62 -0
  35. package/src/character/AvatarEditor.js +174 -50
  36. package/src/character/Character.js +164 -26
  37. package/src/character/motion.js +95 -34
  38. package/src/character/parts.js +43 -12
  39. package/src/character/skin.js +412 -0
  40. package/src/character/styles.js +1429 -0
  41. package/src/character3d/Character3D.js +1693 -0
  42. package/src/character3d/body.js +154 -0
  43. package/src/character3d/fx.js +323 -0
  44. package/src/character3d/index.js +281 -0
  45. package/src/character3d/materials.js +429 -0
  46. package/src/character3d/stage.js +187 -0
  47. package/src/i18n/messages/character.js +18 -42
  48. package/src/i18n/messages/platform.js +135 -0
  49. package/src/identity/schema.js +16 -8
  50. 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,12 +12,18 @@ 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
26
+ 7ots server run the 7ots proxy (/api/agent) next to your site
21
27
 
22
28
  Identities are reproducible: the same seed gives the same ot everywhere (web, CLI, pet, meet).
23
29
  Docs: https://7ots.com/llms.txt`,
@@ -28,8 +34,19 @@ Docs: https://7ots.com/llms.txt`,
28
34
  'install.html': 'Added the 7ots widget to {file}.',
29
35
  'install.htmlUpdated': 'Updated the 7ots widget in {file}.',
30
36
  'install.noHtml': 'No HTML file found. Paste this before </body>:',
31
- 'install.endpointLocal': 'The widget talks to a 7ots proxy at /api/agent. Run one next to your site:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server\n(or host it on 7ots.com: {app})',
32
- 'install.self': 'Identity ready for your own server:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server',
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)\")",
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})',
49
+ 'install.self': 'Identity ready for your own server:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
33
50
  'install.platform': 'Host it on 7ots.com with this exact identity: {app}',
34
51
  'pet.food': 'Food',
35
52
  'pet.energy': 'Energy',
@@ -41,7 +58,8 @@ Docs: https://7ots.com/llms.txt`,
41
58
  'pet.detached': '{name} now lives in the background. Stop it with: 7ots stop',
42
59
  'pet.notRunning': 'The pet is not running (start it with: 7ots pet). Saved for later.',
43
60
  'pet.stopped': 'Good night, {name}.',
44
- '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)…',
45
63
  'pet.electronFail': 'Could not open the desktop window ({error}); opening the browser instead.',
46
64
  'pet.chooseMode': 'How should {name} live on your computer?',
47
65
  'pet.status': '{name} · level {level} · food {food} · energy {energy} · fun {fun}{asleep}',
@@ -63,6 +81,8 @@ Docs: https://7ots.com/llms.txt`,
63
81
  'login.open': 'Authorize 7ots in your browser:\n {url}',
64
82
  'login.ok': 'Connected to Orquesta ({org}).',
65
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.',
66
86
  'err.noIdentity': 'No identity yet. Create one with "7ots new" or run "7ots setup".',
67
87
  'w.title': '7ots setup — {name}',
68
88
  'w.intro': 'Your ot in 7 short steps. Keys stay on this computer.',
@@ -161,12 +181,18 @@ Docs: https://7ots.com/llms.txt`,
161
181
  7ots show [--json|--prompt] muestra la identidad (la del proyecto o la global)
162
182
  7ots install [--target html|self|print] [archivo] pone la identidad en este proyecto
163
183
  7ots setup wizard: dónde vive, cerebro, voz, mascota, hooks
164
- 7ots pet [--mode desktop|terminal|browser|auto] [--detach] despierta tu mascota virtual
165
- 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
166
190
  7ots hooks install|remove [--claude] [--shell] [--global]
167
191
  7ots meet --new | "<invitación o enlace>" entra a apuchat meet con esta identidad
168
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)
169
194
  7ots login orquesta conecta una cuenta de getorquesta.com
195
+ 7ots server levanta el proxy 7ots (/api/agent) junto a tu web
170
196
 
171
197
  Las identidades son reproducibles: la misma semilla da el mismo ot en todas partes (web, CLI, mascota, meet).
172
198
  Docs: https://7ots.com/llms.txt`,
@@ -177,8 +203,19 @@ Docs: https://7ots.com/llms.txt`,
177
203
  'install.html': 'Widget de 7ots añadido a {file}.',
178
204
  'install.htmlUpdated': 'Widget de 7ots actualizado en {file}.',
179
205
  'install.noHtml': 'No encontré un HTML. Pega esto antes de </body>:',
180
- 'install.endpointLocal': 'El widget habla con un proxy 7ots en /api/agent. Levanta uno junto a tu web:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server\n(o alójalo en 7ots.com: {app})',
181
- 'install.self': 'Identidad lista para tu servidor:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server',
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)\")",
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})',
218
+ 'install.self': 'Identidad lista para tu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
182
219
  'install.platform': 'Alójalo en 7ots.com con esta misma identidad: {app}',
183
220
  'pet.food': 'Comida',
184
221
  'pet.energy': 'Energía',
@@ -190,7 +227,8 @@ Docs: https://7ots.com/llms.txt`,
190
227
  'pet.detached': '{name} vive ahora en segundo plano. Para dormirlo: 7ots stop',
191
228
  'pet.notRunning': 'La mascota no está corriendo (lánzala con: 7ots pet). Lo guardo para después.',
192
229
  'pet.stopped': 'Buenas noches, {name}.',
193
- '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)…',
194
232
  'pet.electronFail': 'No pude abrir la ventana de escritorio ({error}); abro el navegador.',
195
233
  'pet.chooseMode': '¿Cómo quieres que viva {name} en tu computador?',
196
234
  'pet.status': '{name} · nivel {level} · comida {food} · energía {energy} · diversión {fun}{asleep}',
@@ -212,6 +250,8 @@ Docs: https://7ots.com/llms.txt`,
212
250
  'login.open': 'Autoriza 7ots en tu navegador:\n {url}',
213
251
  'login.ok': 'Conectado a Orquesta ({org}).',
214
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.',
215
255
  'err.noIdentity': 'Aún no hay identidad. Crea una con «7ots new» o corre «7ots setup».',
216
256
  'w.title': 'Configuración de 7ots — {name}',
217
257
  'w.intro': 'Tu ot en 7 pasos cortos. Las claves se quedan en este computador.',
@@ -310,12 +350,18 @@ Docs: https://7ots.com/llms.txt`,
310
350
  7ots show [--json|--prompt] mostra a identidade (do projeto ou a global)
311
351
  7ots install [--target html|self|print] [arquivo] coloca a identidade neste projeto
312
352
  7ots setup assistente: onde mora, cérebro, voz, bichinho, hooks
313
- 7ots pet [--mode desktop|terminal|browser|auto] [--detach] acorda seu bichinho virtual
314
- 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
315
359
  7ots hooks install|remove [--claude] [--shell] [--global]
316
360
  7ots meet --new | "<convite ou link>" entra no apuchat meet com esta identidade
317
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)
318
363
  7ots login orquesta conecta uma conta do getorquesta.com
364
+ 7ots server sobe o proxy 7ots (/api/agent) junto do seu site
319
365
 
320
366
  As identidades são reproduzíveis: a mesma semente dá o mesmo ot em todo lugar (web, CLI, bichinho, meet).
321
367
  Docs: https://7ots.com/llms.txt`,
@@ -326,8 +372,19 @@ Docs: https://7ots.com/llms.txt`,
326
372
  'install.html': 'Widget do 7ots adicionado em {file}.',
327
373
  'install.htmlUpdated': 'Widget do 7ots atualizado em {file}.',
328
374
  'install.noHtml': 'Não achei um HTML. Cole isto antes de </body>:',
329
- 'install.endpointLocal': 'O widget fala com um proxy 7ots em /api/agent. Suba um junto do seu site:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server\n(ou hospede no 7ots.com: {app})',
330
- 'install.self': 'Identidade pronta para o seu servidor:\n IDENTITY_FILE={file} npx -p 7ots 7ots-server',
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)\")",
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})',
387
+ 'install.self': 'Identidade pronta para o seu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
331
388
  'install.platform': 'Hospede no 7ots.com com esta mesma identidade: {app}',
332
389
  'pet.food': 'Comida',
333
390
  'pet.energy': 'Energia',
@@ -339,7 +396,8 @@ Docs: https://7ots.com/llms.txt`,
339
396
  'pet.detached': '{name} agora mora em segundo plano. Para parar: 7ots stop',
340
397
  'pet.notRunning': 'O bichinho não está rodando (inicie com: 7ots pet). Guardado para depois.',
341
398
  'pet.stopped': 'Boa noite, {name}.',
342
- '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ó)…',
343
401
  'pet.electronFail': 'Não consegui abrir a janela de desktop ({error}); abrindo o navegador.',
344
402
  'pet.chooseMode': 'Como {name} deve morar no seu computador?',
345
403
  'pet.status': '{name} · nível {level} · comida {food} · energia {energy} · diversão {fun}{asleep}',
@@ -361,6 +419,8 @@ Docs: https://7ots.com/llms.txt`,
361
419
  'login.open': 'Autorize o 7ots no navegador:\n {url}',
362
420
  'login.ok': 'Conectado ao Orquesta ({org}).',
363
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.',
364
424
  'err.noIdentity': 'Ainda não há identidade. Crie uma com «7ots new» ou rode «7ots setup».',
365
425
  'w.title': 'Configuração do 7ots — {name}',
366
426
  'w.intro': 'Seu ot em 7 passos curtos. As chaves ficam neste computador.',
@@ -456,12 +516,22 @@ Docs: https://7ots.com/llms.txt`,
456
516
 
457
517
  let current = null;
458
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
+
459
528
  export function cliLang() {
460
529
  if (current) return current;
461
530
  const pick = (v) => (['en', 'es', 'pt'].includes(v) ? v : '');
462
531
  current =
463
532
  pick(process.env.SEVENOTS_LANG) ||
464
533
  pick(readJson(homeFile('config.json'), {})?.lang) ||
534
+ pick(identityLang()) ||
465
535
  pick(String(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '').slice(0, 2).toLowerCase()) ||
466
536
  'en';
467
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();