@7ots/cli 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -1
- package/cli/7ots.mjs +144 -36
- package/cli/lib/assistant.mjs +347 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/config.mjs +8 -1
- package/cli/lib/i18n.mjs +84 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +133 -13
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +96 -32
- package/cli/pet/pet.html +59 -6
- package/cli/pet/settings.html +430 -0
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- 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 === '
|
|
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
|
-
|
|
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/config.mjs
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
* { kind: '7ots' } the home's /chat (7ots or own server)
|
|
14
14
|
* { kind: 'batuta', model? } Orquesta's Batuta LLM
|
|
15
15
|
* voice how it speaks: { kind: 'none'|'browser'|'apuchat'|'elevenlabs'|'grok'|'fish'|'openai', voiceId? }
|
|
16
|
-
* pet { mode: 'ask'|'desktop'|'terminal'|'browser', annoy: 0..3 }
|
|
16
|
+
* pet { mode: 'ask'|'desktop'|'terminal'|'browser', annoy: 0..3, view?: '2d'|'3d' }
|
|
17
|
+
* access what the pet may see and do (the settings page, /settings):
|
|
18
|
+
* { agent: true, the details of your coding agent / terminal (prompts, commands, tools) go to the brain
|
|
19
|
+
* memory: true, it keeps notes about you and uses them when answering
|
|
20
|
+
* open: true, it may open sites in your browser (reminders, "open my mail")
|
|
21
|
+
* notify: true } desktop notifications when a reminder fires
|
|
17
22
|
*
|
|
18
23
|
* ~/.7ots/keys.json (0600) keeps the keys typed in the wizard under their usual env-var names.
|
|
19
24
|
* Variables already set in the environment always win over the file.
|
|
@@ -30,6 +35,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
|
|
|
30
35
|
brain: { kind: 'lines' },
|
|
31
36
|
voice: { kind: 'browser' },
|
|
32
37
|
pet: { mode: 'ask', annoy: 2 },
|
|
38
|
+
access: { agent: true, memory: true, open: true, notify: true },
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
export function loadConfig() {
|
|
@@ -41,6 +47,7 @@ export function loadConfig() {
|
|
|
41
47
|
brain: { ...CONFIG_DEFAULTS.brain, ...c.brain },
|
|
42
48
|
voice: { ...CONFIG_DEFAULTS.voice, ...c.voice },
|
|
43
49
|
pet: { ...CONFIG_DEFAULTS.pet, ...c.pet },
|
|
50
|
+
access: { ...CONFIG_DEFAULTS.access, ...c.access },
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
53
|
|
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,17 @@ 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
|
|
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
|
|
21
|
+
7ots config [--print] settings page: brain, what it can see, notes
|
|
17
22
|
7ots hooks install|remove [--claude] [--shell] [--global]
|
|
18
23
|
7ots meet --new | "<invite or call URL>" join apuchat meet with this identity
|
|
19
24
|
7ots prompt [identity|install|pet|meet|all] prompts for coding agents
|
|
25
|
+
7ots docs everything for coding agents (same as https://7ots.com/llms.txt)
|
|
20
26
|
7ots login orquesta connect a getorquesta.com account
|
|
21
27
|
7ots server run the 7ots proxy (/api/agent) next to your site
|
|
22
28
|
|
|
@@ -29,6 +35,17 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
29
35
|
'install.html': 'Added the 7ots widget to {file}.',
|
|
30
36
|
'install.htmlUpdated': 'Updated the 7ots widget in {file}.',
|
|
31
37
|
'install.noHtml': 'No HTML file found. Paste this before </body>:',
|
|
38
|
+
'install.pasteIn': "This is a {name} project: paste this in {file}, before </body> (or in the root layout):",
|
|
39
|
+
'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}",
|
|
40
|
+
'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",
|
|
41
|
+
'pet.tmuxNo': "Not inside tmux. Run it from a tmux session, or use: 7ots pet --mode terminal",
|
|
42
|
+
'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)\"",
|
|
43
|
+
'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",
|
|
44
|
+
'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
|
|
45
|
+
'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",
|
|
46
|
+
'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)",
|
|
47
|
+
'usage.meet': "7ots meet --new [--minutes n]\n7ots meet \"<invite text or https://meet.apuchat.com/call#c=…&t=…&p=… link>\" [--minutes n]",
|
|
48
|
+
'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
49
|
'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
50
|
'install.self': 'Identity ready for your own server:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
|
|
34
51
|
'install.platform': 'Host it on 7ots.com with this exact identity: {app}',
|
|
@@ -42,7 +59,8 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
42
59
|
'pet.detached': '{name} now lives in the background. Stop it with: 7ots stop',
|
|
43
60
|
'pet.notRunning': 'The pet is not running (start it with: 7ots pet). Saved for later.',
|
|
44
61
|
'pet.stopped': 'Good night, {name}.',
|
|
45
|
-
'pet.electron': 'Opening the desktop pet
|
|
62
|
+
'pet.electron': 'Opening the desktop pet…',
|
|
63
|
+
'pet.electronInstall': 'First time: installing Electron in {dir} (~100 MB, once)…',
|
|
46
64
|
'pet.electronFail': 'Could not open the desktop window ({error}); opening the browser instead.',
|
|
47
65
|
'pet.chooseMode': 'How should {name} live on your computer?',
|
|
48
66
|
'pet.status': '{name} · level {level} · food {food} · energy {energy} · fun {fun}{asleep}',
|
|
@@ -64,6 +82,9 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
64
82
|
'login.open': 'Authorize 7ots in your browser:\n {url}',
|
|
65
83
|
'login.ok': 'Connected to Orquesta ({org}).',
|
|
66
84
|
'err.unknown': 'Unknown command "{cmd}". Try: 7ots help',
|
|
85
|
+
'err.noPet': 'The pet is not awake. Start it with: 7ots pet',
|
|
86
|
+
'pet.settings': 'Opening the settings in your browser.',
|
|
87
|
+
'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
88
|
'err.noIdentity': 'No identity yet. Create one with "7ots new" or run "7ots setup".',
|
|
68
89
|
'w.title': '7ots setup — {name}',
|
|
69
90
|
'w.intro': 'Your ot in 7 short steps. Keys stay on this computer.',
|
|
@@ -162,11 +183,17 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
162
183
|
7ots show [--json|--prompt] muestra la identidad (la del proyecto o la global)
|
|
163
184
|
7ots install [--target html|self|print] [archivo] pone la identidad en este proyecto
|
|
164
185
|
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
|
|
186
|
+
7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] despierta tu mascota virtual
|
|
187
|
+
7ots pet --tmux la mascota de terminal en un panel de tmux
|
|
188
|
+
7ots feed | play | sleep | wake | say <texto> | stop
|
|
189
|
+
7ots status [--line|--json] estado (--line: una línea para tmux)
|
|
190
|
+
7ots ask <texto> háblale: "recuérdame ver el correo en 10 min", "abre mi calendario", "anota que…"
|
|
191
|
+
7ots reminders [--cancel <id>] recordatorios pendientes y lo que recuerda
|
|
192
|
+
7ots config [--print] ajustes: cerebro, qué puede ver, notas
|
|
167
193
|
7ots hooks install|remove [--claude] [--shell] [--global]
|
|
168
194
|
7ots meet --new | "<invitación o enlace>" entra a apuchat meet con esta identidad
|
|
169
195
|
7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
|
|
196
|
+
7ots docs todo para agentes de código (igual que https://7ots.com/llms.txt)
|
|
170
197
|
7ots login orquesta conecta una cuenta de getorquesta.com
|
|
171
198
|
7ots server levanta el proxy 7ots (/api/agent) junto a tu web
|
|
172
199
|
|
|
@@ -179,6 +206,17 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
179
206
|
'install.html': 'Widget de 7ots añadido a {file}.',
|
|
180
207
|
'install.htmlUpdated': 'Widget de 7ots actualizado en {file}.',
|
|
181
208
|
'install.noHtml': 'No encontré un HTML. Pega esto antes de </body>:',
|
|
209
|
+
'install.pasteIn': "Es un proyecto {name}: pega esto en {file}, antes de </body> (o en el layout raíz):",
|
|
210
|
+
'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}",
|
|
211
|
+
'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",
|
|
212
|
+
'pet.tmuxNo': "No estás dentro de tmux. Ejecútalo desde una sesión de tmux, o usa: 7ots pet --mode terminal",
|
|
213
|
+
'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)\"",
|
|
214
|
+
'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",
|
|
215
|
+
'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
|
|
216
|
+
'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",
|
|
217
|
+
'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)",
|
|
218
|
+
'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]",
|
|
219
|
+
'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
220
|
'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
221
|
'install.self': 'Identidad lista para tu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
|
|
184
222
|
'install.platform': 'Alójalo en 7ots.com con esta misma identidad: {app}',
|
|
@@ -192,7 +230,8 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
192
230
|
'pet.detached': '{name} vive ahora en segundo plano. Para dormirlo: 7ots stop',
|
|
193
231
|
'pet.notRunning': 'La mascota no está corriendo (lánzala con: 7ots pet). Lo guardo para después.',
|
|
194
232
|
'pet.stopped': 'Buenas noches, {name}.',
|
|
195
|
-
'pet.electron': 'Abriendo la mascota de escritorio
|
|
233
|
+
'pet.electron': 'Abriendo la mascota de escritorio…',
|
|
234
|
+
'pet.electronInstall': 'Primera vez: instalo Electron en {dir} (~100 MB, una sola vez)…',
|
|
196
235
|
'pet.electronFail': 'No pude abrir la ventana de escritorio ({error}); abro el navegador.',
|
|
197
236
|
'pet.chooseMode': '¿Cómo quieres que viva {name} en tu computador?',
|
|
198
237
|
'pet.status': '{name} · nivel {level} · comida {food} · energía {energy} · diversión {fun}{asleep}',
|
|
@@ -214,6 +253,9 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
214
253
|
'login.open': 'Autoriza 7ots en tu navegador:\n {url}',
|
|
215
254
|
'login.ok': 'Conectado a Orquesta ({org}).',
|
|
216
255
|
'err.unknown': 'Comando desconocido «{cmd}». Prueba: 7ots help',
|
|
256
|
+
'err.noPet': 'La mascota no está despierta. Iníciala con: 7ots pet',
|
|
257
|
+
'pet.settings': 'Abriendo los ajustes en tu navegador.',
|
|
258
|
+
'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
259
|
'err.noIdentity': 'Aún no hay identidad. Crea una con «7ots new» o corre «7ots setup».',
|
|
218
260
|
'w.title': 'Configuración de 7ots — {name}',
|
|
219
261
|
'w.intro': 'Tu ot en 7 pasos cortos. Las claves se quedan en este computador.',
|
|
@@ -312,11 +354,17 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
312
354
|
7ots show [--json|--prompt] mostra a identidade (do projeto ou a global)
|
|
313
355
|
7ots install [--target html|self|print] [arquivo] coloca a identidade neste projeto
|
|
314
356
|
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
|
|
357
|
+
7ots pet [--mode desktop|terminal|browser|auto] [--detach] [--opaque] acorda seu bichinho virtual
|
|
358
|
+
7ots pet --tmux o bichinho de terminal num painel do tmux
|
|
359
|
+
7ots feed | play | sleep | wake | say <texto> | stop
|
|
360
|
+
7ots status [--line|--json] estado (--line: uma linha para tmux)
|
|
361
|
+
7ots ask <texto> fale com ele: "me lembre de ver o e-mail em 10 min", "abra minha agenda", "anote que…"
|
|
362
|
+
7ots reminders [--cancel <id>] lembretes pendentes e o que ele lembra
|
|
363
|
+
7ots config [--print] ajustes: cérebro, o que pode ver, notas
|
|
317
364
|
7ots hooks install|remove [--claude] [--shell] [--global]
|
|
318
365
|
7ots meet --new | "<convite ou link>" entra no apuchat meet com esta identidade
|
|
319
366
|
7ots prompt [identity|install|pet|meet|all] prompts para agentes de código
|
|
367
|
+
7ots docs tudo para agentes de código (igual a https://7ots.com/llms.txt)
|
|
320
368
|
7ots login orquesta conecta uma conta do getorquesta.com
|
|
321
369
|
7ots server sobe o proxy 7ots (/api/agent) junto do seu site
|
|
322
370
|
|
|
@@ -329,6 +377,17 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
329
377
|
'install.html': 'Widget do 7ots adicionado em {file}.',
|
|
330
378
|
'install.htmlUpdated': 'Widget do 7ots atualizado em {file}.',
|
|
331
379
|
'install.noHtml': 'Não achei um HTML. Cole isto antes de </body>:',
|
|
380
|
+
'install.pasteIn': "É um projeto {name}: cole isto em {file}, antes de </body> (ou no layout raiz):",
|
|
381
|
+
'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}",
|
|
382
|
+
'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",
|
|
383
|
+
'pet.tmuxNo': "Você não está no tmux. Rode dentro de uma sessão do tmux, ou use: 7ots pet --mode terminal",
|
|
384
|
+
'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)\"",
|
|
385
|
+
'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",
|
|
386
|
+
'usage.new': "7ots new [--seed s] [--name n] [--lang en|es|pt] [--global] [--force] [--json]",
|
|
387
|
+
'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",
|
|
388
|
+
'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)",
|
|
389
|
+
'usage.meet': "7ots meet --new [--minutes n]\n7ots meet \"<texto do convite ou link https://meet.apuchat.com/call#c=…&t=…&p=…>\" [--minutes n]",
|
|
390
|
+
'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
391
|
'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
392
|
'install.self': 'Identidade pronta para o seu servidor:\n IDENTITY_FILE={file} npx -y @7ots/cli server',
|
|
334
393
|
'install.platform': 'Hospede no 7ots.com com esta mesma identidade: {app}',
|
|
@@ -342,7 +401,8 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
342
401
|
'pet.detached': '{name} agora mora em segundo plano. Para parar: 7ots stop',
|
|
343
402
|
'pet.notRunning': 'O bichinho não está rodando (inicie com: 7ots pet). Guardado para depois.',
|
|
344
403
|
'pet.stopped': 'Boa noite, {name}.',
|
|
345
|
-
'pet.electron': 'Abrindo o bichinho de desktop
|
|
404
|
+
'pet.electron': 'Abrindo o bichinho de desktop…',
|
|
405
|
+
'pet.electronInstall': 'Primeira vez: instalando o Electron em {dir} (~100 MB, uma vez só)…',
|
|
346
406
|
'pet.electronFail': 'Não consegui abrir a janela de desktop ({error}); abrindo o navegador.',
|
|
347
407
|
'pet.chooseMode': 'Como {name} deve morar no seu computador?',
|
|
348
408
|
'pet.status': '{name} · nível {level} · comida {food} · energia {energy} · diversão {fun}{asleep}',
|
|
@@ -364,6 +424,9 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
364
424
|
'login.open': 'Autorize o 7ots no navegador:\n {url}',
|
|
365
425
|
'login.ok': 'Conectado ao Orquesta ({org}).',
|
|
366
426
|
'err.unknown': 'Comando desconhecido «{cmd}». Tente: 7ots help',
|
|
427
|
+
'err.noPet': 'O bichinho não está acordado. Inicie com: 7ots pet',
|
|
428
|
+
'pet.settings': 'Abrindo os ajustes no seu navegador.',
|
|
429
|
+
'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
430
|
'err.noIdentity': 'Ainda não há identidade. Crie uma com «7ots new» ou rode «7ots setup».',
|
|
368
431
|
'w.title': 'Configuração do 7ots — {name}',
|
|
369
432
|
'w.intro': 'Seu ot em 7 passos curtos. As chaves ficam neste computador.',
|
|
@@ -459,12 +522,22 @@ Docs: https://7ots.com/llms.txt`,
|
|
|
459
522
|
|
|
460
523
|
let current = null;
|
|
461
524
|
|
|
525
|
+
/** The ot's language (project identity, else the global one): its texts follow it. */
|
|
526
|
+
function identityLang() {
|
|
527
|
+
for (const f of [projectFile('identity.json'), homeFile('identity.json')]) {
|
|
528
|
+
const l = readJson(f)?.language;
|
|
529
|
+
if (l) return String(l).slice(0, 2).toLowerCase();
|
|
530
|
+
}
|
|
531
|
+
return '';
|
|
532
|
+
}
|
|
533
|
+
|
|
462
534
|
export function cliLang() {
|
|
463
535
|
if (current) return current;
|
|
464
536
|
const pick = (v) => (['en', 'es', 'pt'].includes(v) ? v : '');
|
|
465
537
|
current =
|
|
466
538
|
pick(process.env.SEVENOTS_LANG) ||
|
|
467
539
|
pick(readJson(homeFile('config.json'), {})?.lang) ||
|
|
540
|
+
pick(identityLang()) ||
|
|
468
541
|
pick(String(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '').slice(0, 2).toLowerCase()) ||
|
|
469
542
|
'en';
|
|
470
543
|
return current;
|
package/cli/lib/install.mjs
CHANGED
|
@@ -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
|
|
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 || '')) {
|
package/cli/lib/pet-core.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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) {
|
package/cli/lib/pet-server.mjs
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
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 }
|
|
14
|
+
* GET /settings?token=… the settings page: brain, what it may see and do, notes, reminders
|
|
15
|
+
* GET /config?token=… { config: { brain, pet, access }, clis, keys: { NAME: bool }, … }
|
|
16
|
+
* POST /config { brain?, pet?, access?, keys? } → saved to ~/.7ots and applied live
|
|
17
|
+
* POST /config/test one line from the current brain → { ok, text, ms }
|
|
18
|
+
* POST /notes/forget { at | 'all' }
|
|
11
19
|
* POST /quit X-7ots-Token
|
|
12
20
|
*
|
|
13
21
|
* Only listens on 127.0.0.1 and only answers to Host 127.0.0.1/localhost (no DNS rebinding).
|
|
@@ -18,8 +26,9 @@ import { createServer } from 'node:http';
|
|
|
18
26
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
19
27
|
import { readFileSync, existsSync, writeFileSync, chmodSync } from 'node:fs';
|
|
20
28
|
import { join } from 'node:path';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
29
|
+
import { createAssistant } from './assistant.mjs';
|
|
30
|
+
import { AI_CLIS, createBrain, detectClis, petSystem } from './brain.mjs';
|
|
31
|
+
import { env, loadConfig, saveConfig, saveKey } from './config.mjs';
|
|
23
32
|
import { loadIdentity } from './identity.mjs';
|
|
24
33
|
import { line } from './lines.mjs';
|
|
25
34
|
import { ensureHome, homeFile, PET_PORT, PKG_ROOT } from './paths.mjs';
|
|
@@ -63,6 +72,7 @@ export async function postPet(path, body, port = PET_PORT) {
|
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
const SETTABLE_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ORQUESTA_TOKEN'];
|
|
66
76
|
const brainLabel = (b = {}) => (b.kind === 'cli' ? `${b.cli}${b.model ? ` · ${b.model}` : ''}` : b.kind === 'api' ? `${b.provider || 'api'}${b.model ? ` · ${b.model}` : ''}` : b.kind || 'lines');
|
|
67
77
|
|
|
68
78
|
const GAP = { 0: Infinity, 1: 10 * 60_000, 2: 90_000, 3: 20_000 }; // minimum silence between spontaneous lines
|
|
@@ -71,12 +81,13 @@ const IDLE = { 0: Infinity, 1: 60 * 60_000, 2: 20 * 60_000, 3: 5 * 60_000 }; //
|
|
|
71
81
|
|
|
72
82
|
export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
73
83
|
const token = petToken();
|
|
74
|
-
|
|
75
|
-
const
|
|
84
|
+
let config = loadConfig();
|
|
85
|
+
const annoyOf = (c) => Math.max(0, Math.min(3, Number(c.pet.annoy ?? 2)));
|
|
86
|
+
let annoy = annoyOf(config);
|
|
76
87
|
const { identity } = loadIdentity();
|
|
77
88
|
if (!identity) throw Object.assign(new Error('no identity'), { code: 'NO_IDENTITY' });
|
|
78
89
|
const lang = identity.language || 'en';
|
|
79
|
-
|
|
90
|
+
let brain = createBrain(config.brain, config.home); // swapped live from the settings page
|
|
80
91
|
const pet = tick(loadPet());
|
|
81
92
|
const clients = new Set();
|
|
82
93
|
const history = []; // last lines (so the AI doesn't repeat itself)
|
|
@@ -121,11 +132,11 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
const SITUATION = {
|
|
124
|
-
tool: (e) => `Your human's coding agent just used the tool ${e.tool}${e.detail ? ` (${e.detail})` : ''}.`,
|
|
125
|
-
toolFail: (e) => `Your human's coding agent tried the tool ${e.tool} and it FAILED${e.detail ? `: ${e.detail}` : ''}.`,
|
|
126
|
-
ok: (e) => `In the terminal, the command \`${e.cmd
|
|
127
|
-
fail: (e) => `In the terminal, the command \`${e.cmd
|
|
128
|
-
prompt: (e) => `Your human just asked their coding agent
|
|
135
|
+
tool: (e) => `Your human's coding agent just used the tool ${e.tool || '?'}${e.detail ? ` (${e.detail})` : ''}.`,
|
|
136
|
+
toolFail: (e) => `Your human's coding agent tried the tool ${e.tool || '?'} and it FAILED${e.detail ? `: ${e.detail}` : ''}.`,
|
|
137
|
+
ok: (e) => `In the terminal, ${e.cmd ? `the command \`${e.cmd}\`` : 'a command'} just succeeded.`,
|
|
138
|
+
fail: (e) => `In the terminal, ${e.cmd ? `the command \`${e.cmd}\`` : 'a command'} just failed with exit code ${e.code}.`,
|
|
139
|
+
prompt: (e) => `Your human just asked their coding agent${e.text ? `: "${String(e.text).slice(0, 300)}"` : ' something'}.`,
|
|
129
140
|
stop: () => `Your human's coding agent just finished its turn. Encourage them to check the result.`,
|
|
130
141
|
waiting: (e) => `Your human's coding agent is waiting for them (permission or an answer)${e.message ? `: ${e.message}` : ''}. Get their attention.`,
|
|
131
142
|
hello: () => `You just woke up on your human's desktop. Say hi.`,
|
|
@@ -171,7 +182,9 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
171
182
|
if (!kind) return broadcast({ gesture: r.gesture });
|
|
172
183
|
const routine = !r.important && !['fail', 'toolFail'].includes(kind);
|
|
173
184
|
if (routine && Math.random() >= CHATTY[annoy]) return broadcast({ gesture: r.gesture });
|
|
174
|
-
|
|
185
|
+
// access.agent off: the brain hears that something happened, not what (no prompts, commands or details).
|
|
186
|
+
const seen = config.access.agent ? ev : { type: ev.type, tool: ev.tool, ok: ev.ok, code: ev.code };
|
|
187
|
+
const sit = SITUATION[kind]?.(seen) || '';
|
|
175
188
|
speak(kind, { cmd: ev.cmd || '', tool: ev.tool || '' }, sit, { force: r.important && ev.type !== 'tool', gesture: r.gesture });
|
|
176
189
|
}
|
|
177
190
|
|
|
@@ -189,6 +202,21 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
189
202
|
broadcast({});
|
|
190
203
|
}, 60_000);
|
|
191
204
|
|
|
205
|
+
// The assistant: reminders that speak, notify and open the tab; notes it learns.
|
|
206
|
+
const assistant = createAssistant({
|
|
207
|
+
brain: { get kind() { return brain.kind; }, think: (o) => brain.think(o) },
|
|
208
|
+
access: () => config.access,
|
|
209
|
+
lang,
|
|
210
|
+
name: identity.name,
|
|
211
|
+
log,
|
|
212
|
+
system: () => petSystem(identity, { lang, role: 'assistant' }),
|
|
213
|
+
onFire: (r, text) => {
|
|
214
|
+
lastSpoke = Date.now();
|
|
215
|
+
pet.lastTalk = lastSpoke;
|
|
216
|
+
broadcast({ say: text, gesture: 'wave', effect: 'exclaim', reminder: r });
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
192
220
|
// Home on getorquesta.com: comment on what the project's agent does.
|
|
193
221
|
let stopWatch = () => {};
|
|
194
222
|
if (config.home.kind === 'orquesta' && config.home.projectId && env().ORQUESTA_TOKEN) {
|
|
@@ -212,8 +240,8 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
212
240
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
213
241
|
};
|
|
214
242
|
|
|
215
|
-
const page = () =>
|
|
216
|
-
readFileSync(join(PKG_ROOT, 'cli', 'pet',
|
|
243
|
+
const page = (file = 'pet.html') =>
|
|
244
|
+
readFileSync(join(PKG_ROOT, 'cli', 'pet', file), 'utf8')
|
|
217
245
|
.replace('__BOOT__', JSON.stringify({ token, identity, voice: config.voice, lang, annoy, brain: brainLabel(config.brain), activity }).replace(/</g, '\\u003c'));
|
|
218
246
|
|
|
219
247
|
const server = createServer(async (req, res) => {
|
|
@@ -235,6 +263,10 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
235
263
|
if (req.method === 'GET' && p === '/pet') {
|
|
236
264
|
return res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }).end(page());
|
|
237
265
|
}
|
|
266
|
+
if (req.method === 'GET' && p === '/settings') {
|
|
267
|
+
return res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer', 'X-Frame-Options': 'DENY' }).end(page('settings.html'));
|
|
268
|
+
}
|
|
269
|
+
if (req.method === 'GET' && p === '/config') return json(200, settingsView());
|
|
238
270
|
if (req.method === 'GET' && p === '/state') return json(200, { identity, state: view(pet), config: { voice: config.voice, pet: config.pet, brain: config.brain.kind } });
|
|
239
271
|
if (req.method === 'GET' && p === '/events') {
|
|
240
272
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
@@ -243,6 +275,7 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
243
275
|
req.on('close', () => clients.delete(res));
|
|
244
276
|
return;
|
|
245
277
|
}
|
|
278
|
+
if (req.method === 'GET' && p === '/reminders') return json(200, { reminders: assistant.reminders, notes: assistant.notes });
|
|
246
279
|
if (req.method !== 'POST') return json(405, { error: 'method' });
|
|
247
280
|
let body = '';
|
|
248
281
|
for await (const c of req) if ((body += c).length > 64 * 1024) return json(413, { error: 'too large' });
|
|
@@ -269,6 +302,33 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
269
302
|
return json(e.status || 502, { error: e.message });
|
|
270
303
|
}
|
|
271
304
|
}
|
|
305
|
+
if (p === '/ask') {
|
|
306
|
+
const text = String(data.text || '').trim().slice(0, 800);
|
|
307
|
+
if (!text) return json(400, { error: 'text' });
|
|
308
|
+
broadcast({ thinking: true, activity: track({ type: 'prompt', text }) });
|
|
309
|
+
const r = await assistant.ask(text);
|
|
310
|
+
lastSpoke = Date.now();
|
|
311
|
+
broadcast({ thinking: false, say: r.reply, gesture: r.actions.length ? 'nod' : null });
|
|
312
|
+
return json(200, r);
|
|
313
|
+
}
|
|
314
|
+
if (p === '/config') {
|
|
315
|
+
const bad = applySettings(data);
|
|
316
|
+
return bad ? json(400, { error: bad }) : json(200, settingsView());
|
|
317
|
+
}
|
|
318
|
+
if (p === '/config/test') {
|
|
319
|
+
if (brain.kind === 'lines') return json(200, { ok: true, text: line('hello', lang, { name: identity.name, level: pet.level }), ms: 0 });
|
|
320
|
+
const t0 = Date.now();
|
|
321
|
+
try {
|
|
322
|
+
const text = await brain.think({ system: petSystem(identity, { lang }), messages: [{ role: 'user', content: 'Your human is checking that your brain works. Say hi in one short line.' }], timeoutMs: 45000 });
|
|
323
|
+
if (!text) throw new Error('empty answer');
|
|
324
|
+
broadcast({ say: text, gesture: 'wave' });
|
|
325
|
+
return json(200, { ok: true, text, ms: Date.now() - t0 });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return json(200, { ok: false, error: e.message, ms: Date.now() - t0 });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (p === '/notes/forget') return json(200, { ok: assistant.forget(data.at === 'all' ? 'all' : String(data.at ?? '')) });
|
|
331
|
+
if (p === '/reminders/cancel') return json(200, { ok: assistant.cancel(String(data.id || '')) });
|
|
272
332
|
if (p === '/quit') {
|
|
273
333
|
json(200, { ok: true });
|
|
274
334
|
return stop();
|
|
@@ -276,8 +336,68 @@ export function startPetServer({ port = PET_PORT, log = () => {} } = {}) {
|
|
|
276
336
|
return json(404, { error: 'not found' });
|
|
277
337
|
});
|
|
278
338
|
|
|
339
|
+
/** What the settings page shows. Keys only as present/absent, never their value. */
|
|
340
|
+
function settingsView() {
|
|
341
|
+
const keys = env();
|
|
342
|
+
return {
|
|
343
|
+
name: identity.name,
|
|
344
|
+
lang,
|
|
345
|
+
config: { brain: config.brain, pet: config.pet, access: config.access },
|
|
346
|
+
brain: brainLabel(config.brain),
|
|
347
|
+
clis: detectClis(),
|
|
348
|
+
allClis: Object.keys(AI_CLIS),
|
|
349
|
+
keys: Object.fromEntries(SETTABLE_KEYS.map((k) => [k, Boolean(keys[k])])),
|
|
350
|
+
home: { kind: config.home.kind, url: Boolean(config.home.url) },
|
|
351
|
+
reminders: assistant.reminders,
|
|
352
|
+
notes: assistant.notes,
|
|
353
|
+
files: { config: homeFile('config.json'), memory: homeFile('memory.json'), reminders: homeFile('reminders.json') },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Validates and saves a settings change, then applies it without restarting. Returns an error or null. */
|
|
358
|
+
function applySettings(d) {
|
|
359
|
+
const next = loadConfig();
|
|
360
|
+
if (d.brain) {
|
|
361
|
+
const b = d.brain;
|
|
362
|
+
const model = String(b.model || '').trim();
|
|
363
|
+
if (model && !/^[\w.:/@-]{1,80}$/.test(model)) return 'model';
|
|
364
|
+
if (b.kind === 'lines') next.brain = { kind: 'lines' };
|
|
365
|
+
else if (b.kind === 'cli') {
|
|
366
|
+
if (!AI_CLIS[b.cli]) return 'cli'; // 'custom' (a shell command) stays CLI-only: 7ots setup
|
|
367
|
+
next.brain = { kind: 'cli', cli: b.cli, ...(model ? { model } : {}) };
|
|
368
|
+
} else if (b.kind === 'api') {
|
|
369
|
+
if (!['anthropic', 'openai'].includes(b.provider)) return 'provider';
|
|
370
|
+
const baseUrl = String(b.baseUrl || '').trim();
|
|
371
|
+
if (baseUrl && !/^https?:\/\/[^\s"'<>]{1,200}$/i.test(baseUrl)) return 'baseUrl';
|
|
372
|
+
next.brain = { kind: 'api', provider: b.provider, ...(model ? { model } : {}), ...(baseUrl ? { baseUrl } : {}) };
|
|
373
|
+
} else if (b.kind === 'batuta') next.brain = { kind: 'batuta', ...(model ? { model } : {}) };
|
|
374
|
+
else if (b.kind === '7ots') {
|
|
375
|
+
if (!next.home.url) return 'home';
|
|
376
|
+
next.brain = { kind: '7ots' };
|
|
377
|
+
} else return 'kind';
|
|
378
|
+
}
|
|
379
|
+
if (d.pet) {
|
|
380
|
+
if (d.pet.annoy !== undefined) next.pet.annoy = Math.max(0, Math.min(3, Math.round(Number(d.pet.annoy)) || 0));
|
|
381
|
+
if (['2d', '3d'].includes(d.pet.view)) next.pet.view = d.pet.view;
|
|
382
|
+
}
|
|
383
|
+
if (d.access) for (const k of Object.keys(next.access)) if (typeof d.access[k] === 'boolean') next.access[k] = d.access[k];
|
|
384
|
+
if (d.keys) {
|
|
385
|
+
for (const [k, v] of Object.entries(d.keys)) {
|
|
386
|
+
if (!SETTABLE_KEYS.includes(k) || typeof v !== 'string' || v.length > 400) return 'key';
|
|
387
|
+
saveKey(k, v.trim()); // '' removes it
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
saveConfig(next);
|
|
391
|
+
config = loadConfig();
|
|
392
|
+
annoy = annoyOf(config);
|
|
393
|
+
brain = createBrain(config.brain, config.home);
|
|
394
|
+
log(`settings: brain ${brainLabel(config.brain)}`);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
279
398
|
function stop() {
|
|
280
399
|
clearInterval(timer);
|
|
400
|
+
assistant.stop();
|
|
281
401
|
stopWatch();
|
|
282
402
|
savePet(pet);
|
|
283
403
|
for (const c of clients) c.end();
|