@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.
Files changed (51) hide show
  1. package/.env.example +17 -1
  2. package/cli/7ots.mjs +144 -36
  3. package/cli/lib/assistant.mjs +347 -0
  4. package/cli/lib/brain.mjs +10 -7
  5. package/cli/lib/config.mjs +8 -1
  6. package/cli/lib/i18n.mjs +84 -11
  7. package/cli/lib/install.mjs +29 -1
  8. package/cli/lib/pet-core.mjs +3 -2
  9. package/cli/lib/pet-server.mjs +133 -13
  10. package/cli/lib/prompts.mjs +199 -7
  11. package/cli/lib/terminal.mjs +27 -6
  12. package/cli/pet/electron/main.cjs +96 -32
  13. package/cli/pet/pet.html +59 -6
  14. package/cli/pet/settings.html +430 -0
  15. package/dist/7ots.esm.js +370 -142
  16. package/dist/7ots.esm.js.map +4 -4
  17. package/dist/7ots.iife.js +370 -142
  18. package/dist/7ots.iife.js.map +4 -4
  19. package/llms.txt +198 -6
  20. package/package.json +1 -1
  21. package/server/channels/apuchat.mjs +8 -5
  22. package/server/channels/apumail.mjs +13 -3
  23. package/server/channels/index.mjs +5 -3
  24. package/server/platform/apuchat.mjs +266 -0
  25. package/server/platform/auth.mjs +39 -10
  26. package/server/platform/connect.mjs +347 -0
  27. package/server/platform/db.mjs +55 -0
  28. package/server/platform/look.mjs +20 -0
  29. package/server/platform/orquesta.mjs +499 -0
  30. package/server/platform/routes.mjs +57 -8
  31. package/server/platform/runtime.mjs +75 -15
  32. package/server/platform/store.mjs +130 -9
  33. package/server/server.mjs +14 -3
  34. package/src/avatar/AvatarStage.js +54 -9
  35. package/src/avatar/three.js +62 -0
  36. package/src/character/AvatarEditor.js +174 -50
  37. package/src/character/Character.js +164 -26
  38. package/src/character/motion.js +95 -34
  39. package/src/character/parts.js +43 -12
  40. package/src/character/skin.js +412 -0
  41. package/src/character/styles.js +1429 -0
  42. package/src/character3d/Character3D.js +1693 -0
  43. package/src/character3d/body.js +154 -0
  44. package/src/character3d/fx.js +323 -0
  45. package/src/character3d/index.js +281 -0
  46. package/src/character3d/materials.js +429 -0
  47. package/src/character3d/stage.js +187 -0
  48. package/src/i18n/messages/character.js +18 -42
  49. package/src/i18n/messages/platform.js +135 -0
  50. package/src/identity/schema.js +16 -8
  51. package/src/index.js +1 -0
@@ -0,0 +1,430 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>7ots settings</title>
7
+ <style>
8
+ :root {
9
+ --ink: #17141f; --ink-2: #4a4458; --muted: #8a8399; --bg: #f3f1f8; --panel: #ffffff; --panel-2: #f8f7fb;
10
+ --line: #e8e4f0; --accent: #6d5dfc; --accent-ink: #fff; --ok: #12a150; --warn: #e08a00; --err: #e5484d;
11
+ --shadow: 0 1px 2px rgb(20 16 40 / .06), 0 12px 40px -12px rgb(20 16 40 / .18);
12
+ --font: ui-sans-serif, -apple-system, 'Segoe UI', Inter, Roboto, system-ui, sans-serif;
13
+ --mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
14
+ color-scheme: light;
15
+ }
16
+ @media (prefers-color-scheme: dark) {
17
+ :root { --ink: #f2f0f8; --ink-2: #c9c3d6; --muted: #8d86a0; --bg: #0f0d14; --panel: #18151f; --panel-2: #1e1a27; --line: #2a2535; --shadow: 0 1px 2px rgb(0 0 0 / .4), 0 18px 50px -16px rgb(0 0 0 / .7); color-scheme: dark; }
18
+ }
19
+ * { box-sizing: border-box; }
20
+ html, body { margin: 0; font: 14px/1.5 var(--font); color: var(--ink); background: var(--bg); -webkit-font-smoothing: antialiased; }
21
+ body { padding: 28px 16px 60px; }
22
+ main { width: min(680px, 100%); margin: 0 auto; display: grid; gap: 16px; }
23
+ header { display: flex; align-items: center; gap: 10px; }
24
+ .brand { display: flex; align-items: center; gap: 8px; font-weight: 750; font-size: 20px; letter-spacing: -.02em; }
25
+ .brand i { width: 26px; height: 26px; border-radius: 8px; background: var(--ink); color: var(--panel); display: grid; place-items: center; font: 800 12px/1 var(--font); font-style: normal; }
26
+ .sp { flex: 1; }
27
+ a { color: var(--accent); text-decoration: none; }
28
+ a:hover { text-decoration: underline; }
29
+ section { background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); padding: 18px 20px; }
30
+ h2 { margin: 0 0 2px; font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
31
+ .hint { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
32
+ .now { display: inline-flex; gap: 6px; align-items: center; padding: 3px 10px; border-radius: 99px; background: var(--panel-2); border: 1px solid var(--line); font-size: 12px; color: var(--ink-2); }
33
+ .now b { color: var(--ink); }
34
+
35
+ .opts { display: grid; gap: 8px; }
36
+ .opt { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start; padding: 11px 12px; border: 1px solid var(--line); border-radius: 12px; cursor: pointer; background: var(--panel-2); }
37
+ .opt:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
38
+ .opt.off { opacity: .5; cursor: not-allowed; }
39
+ .opt input { margin: 3px 0 0; accent-color: var(--accent); }
40
+ .opt b { font-weight: 650; }
41
+ .opt > label { cursor: inherit; }
42
+ .opt small { grid-column: 2; color: var(--muted); font-size: 12.5px; }
43
+ .more { grid-column: 2; display: none; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: flex-end; }
44
+ .more .chips { align-self: center; }
45
+ .opt:has(input:checked) .more { display: flex; }
46
+ .chips { display: flex; gap: 6px; flex-wrap: wrap; }
47
+ .chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 99px; border: 1px solid var(--line); background: var(--panel); font-size: 13px; cursor: pointer; }
48
+ .chip:has(input:checked) { border-color: var(--accent); color: var(--accent); font-weight: 600; }
49
+ .chip input { display: none; }
50
+ .chip.off { opacity: .45; cursor: not-allowed; }
51
+ label.f { display: grid; gap: 3px; font-size: 12px; color: var(--muted); flex: 1 1 180px; }
52
+ input[type=text], input[type=password], select { font: 13px var(--font); color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 9px; padding: 7px 10px; width: 100%; }
53
+ input:focus-visible, select:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
54
+
55
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
56
+ button { font: 600 13px var(--font); color: var(--ink); background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 14px; cursor: pointer; }
57
+ button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
58
+ button:disabled { opacity: .55; cursor: wait; }
59
+ button.x { padding: 2px 8px; font-weight: 500; color: var(--muted); }
60
+ .msg { font-size: 13px; color: var(--muted); }
61
+ .msg.ok { color: var(--ok); } .msg.err { color: var(--err); }
62
+ .reply { margin-top: 10px; padding: 10px 12px; border-radius: 12px; background: var(--panel-2); border: 1px solid var(--line); font-size: 13.5px; display: none; }
63
+
64
+ .sw { display: grid; grid-template-columns: 1fr auto; gap: 2px 14px; padding: 11px 0; border-top: 1px solid var(--line); align-items: center; }
65
+ .sw:first-of-type { border-top: 0; padding-top: 0; }
66
+ .sw small { color: var(--muted); font-size: 12.5px; grid-column: 1; }
67
+ .tog { grid-row: 1 / span 2; grid-column: 2; position: relative; width: 40px; height: 23px; }
68
+ .tog input { opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; position: relative; z-index: 1; }
69
+ .tog span { position: absolute; inset: 0; border-radius: 99px; background: var(--line); transition: background .15s; }
70
+ .tog span::after { content: ''; position: absolute; left: 3px; top: 3px; width: 17px; height: 17px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .25); transition: transform .15s; }
71
+ .tog input:checked + span { background: var(--accent); }
72
+ .tog input:checked + span::after { transform: translateX(17px); }
73
+ .tog input:disabled { cursor: not-allowed; }
74
+ .soon { opacity: .6; }
75
+ .tag { display: inline-block; margin-left: 6px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); vertical-align: 1px; }
76
+
77
+ .slider { display: grid; gap: 6px; }
78
+ .slider input { width: 100%; accent-color: var(--accent); }
79
+ .ticks { display: flex; justify-content: space-between; color: var(--muted); font-size: 12px; }
80
+
81
+ ul.list { list-style: none; margin: 0; padding: 0; }
82
+ ul.list li { display: flex; gap: 10px; align-items: center; padding: 8px 0; border-top: 1px solid var(--line); font-size: 13.5px; }
83
+ ul.list li:first-child { border-top: 0; }
84
+ ul.list li span { flex: 1; overflow-wrap: anywhere; }
85
+ ul.list time { color: var(--muted); font-size: 12px; white-space: nowrap; }
86
+ .empty { color: var(--muted); font-size: 13px; }
87
+ footer { color: var(--muted); font-size: 12px; text-align: center; }
88
+ footer code { font: 11.5px var(--mono); }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <main>
93
+ <header>
94
+ <span class="brand"><i>7</i><span id="title"></span></span>
95
+ <span class="sp"></span>
96
+ <a id="back"></a>
97
+ </header>
98
+
99
+ <section aria-labelledby="hBrain">
100
+ <h2 id="hBrain" data-t="brainH"></h2>
101
+ <p class="hint" data-t="brainHint"></p>
102
+ <div class="opts" id="brains"></div>
103
+ <div class="row">
104
+ <button class="primary" id="saveBrain" data-t="save"></button>
105
+ <button id="test" data-t="test"></button>
106
+ <span class="now" id="now"></span>
107
+ <span class="msg" id="brainMsg" role="status"></span>
108
+ </div>
109
+ <div class="reply" id="reply"></div>
110
+ </section>
111
+
112
+ <section aria-labelledby="hAccess">
113
+ <h2 id="hAccess" data-t="accessH"></h2>
114
+ <p class="hint" data-t="accessHint"></p>
115
+ <div id="access"></div>
116
+ </section>
117
+
118
+ <section aria-labelledby="hPet">
119
+ <h2 id="hPet" data-t="petH"></h2>
120
+ <p class="hint" data-t="annoyHint"></p>
121
+ <div class="slider">
122
+ <input type="range" id="annoy" min="0" max="3" step="1" aria-labelledby="hPet">
123
+ <div class="ticks" id="ticks"></div>
124
+ </div>
125
+ <div class="row">
126
+ <span class="msg" data-t="view"></span>
127
+ <div class="chips" id="view"></div>
128
+ <span class="msg" id="petMsg" role="status"></span>
129
+ </div>
130
+ </section>
131
+
132
+ <section aria-labelledby="hNotes">
133
+ <h2 id="hNotes" data-t="notesH"></h2>
134
+ <p class="hint" data-t="notesHint"></p>
135
+ <ul class="list" id="notes"></ul>
136
+ <div class="row"><button id="forgetAll" data-t="forgetAll"></button></div>
137
+ </section>
138
+
139
+ <section aria-labelledby="hRem">
140
+ <h2 id="hRem" data-t="remH"></h2>
141
+ <p class="hint" data-t="remHint"></p>
142
+ <ul class="list" id="rems"></ul>
143
+ </section>
144
+
145
+ <footer id="foot"></footer>
146
+ </main>
147
+
148
+ <script>
149
+ const BOOT = __BOOT__;
150
+ const T = {
151
+ en: {
152
+ title: '{name} · settings', back: '← Back to the pet', save: 'Save', saved: 'Saved ✓', test: 'Try it', testing: 'Thinking…', now: 'Now:',
153
+ brainH: 'Brain', brainHint: 'Who thinks when it talks to you. Your own AI CLI is free with your subscription and runs on this computer.',
154
+ lines: 'No AI', linesD: 'Built-in phrases. Instant, but it can’t understand free text (reminders still work).',
155
+ cli: 'Local AI CLI', cliD: 'An AI command already installed here, run as a separate instance.', notInstalled: 'not installed',
156
+ api: 'Your API key', apiD: 'Anthropic or any OpenAI-compatible endpoint, with your key.',
157
+ batuta: 'Batuta (Orquesta)', batutaD: 'Orquesta’s model router, with your Orquesta token.',
158
+ home: 'The ot’s home', homeD: 'The /chat of its home on 7ots.com or your own server.', noHome: 'no home online',
159
+ model: 'Model (optional)', provider: 'Provider', baseUrl: 'Base URL (optional)', key: 'API key', keySaved: 'saved — type to replace', keyNone: 'not set', orqToken: 'Orquesta token',
160
+ accessH: 'What it can see and do', accessHint: 'Everything stays on this computer, except what goes to the brain you chose above.',
161
+ agent: 'Your coding agent and terminal', agentD: 'Your prompts, commands and tools go to the brain so it can comment. Off: it only knows “something happened”.',
162
+ memory: 'Learn about you', memoryD: 'Keeps notes (“remember that…”) and uses them when answering.',
163
+ open: 'Open sites', openD: 'Opens your mail, calendar… when you ask, or when a reminder fires.',
164
+ notify: 'Notifications', notifyD: 'A desktop notification when a reminder fires.',
165
+ soon: 'coming soon', screen: 'See your screen', screenD: 'Look at what’s open to help with it.', procs: 'Processes and apps', procsD: 'Know what’s running, and act on it (via the Orquesta agent).', inbox: 'Read mail and calendar', inboxD: 'Summaries and replies, not just opening the tab.',
166
+ petH: 'How chatty', annoyHint: 'How often it comments on what you do.', a0: 'Quiet', a1: 'A little', a2: 'Normal', a3: 'Chatty',
167
+ view: 'Look:', v2d: '2D', v3d: '3D plush', viewNext: 'Applies the next time it opens (7ots stop · 7ots pet).',
168
+ notesH: 'What it knows about you', notesHint: 'Its notes. Tell it “remember that…” to add one.', noNotes: 'Nothing yet.', forget: 'Forget', forgetAll: 'Forget everything', sure: 'Click again to forget everything',
169
+ remH: 'Reminders', remHint: 'Tell it “remind me to … in 10 minutes” or “at 15:30”.', noRems: 'No reminders.', cancel: 'Cancel',
170
+ foot: 'Open this page with <code>7ots config</code>, or ⚙️ in the pet’s menu.', err: 'Couldn’t save: {e}',
171
+ },
172
+ es: {
173
+ title: '{name} · ajustes', back: '← Volver a la mascota', save: 'Guardar', saved: 'Guardado ✓', test: 'Probar', testing: 'Pensando…', now: 'Ahora:',
174
+ brainH: 'Cerebro', brainHint: 'Quién piensa cuando te habla. Tu propio CLI de IA es gratis con tu suscripción y corre en este computador.',
175
+ lines: 'Sin IA', linesD: 'Frases incluidas. Instantáneo, pero no entiende texto libre (los recordatorios igual funcionan).',
176
+ cli: 'CLI de IA local', cliD: 'Un comando de IA ya instalado acá, como instancia aparte.', notInstalled: 'no instalado',
177
+ api: 'Tu API key', apiD: 'Anthropic o cualquier endpoint compatible con OpenAI, con tu key.',
178
+ batuta: 'Batuta (Orquesta)', batutaD: 'El router de modelos de Orquesta, con tu token de Orquesta.',
179
+ home: 'La casa del ot', homeD: 'El /chat de su casa en 7ots.com o en tu servidor.', noHome: 'sin casa en línea',
180
+ model: 'Modelo (opcional)', provider: 'Proveedor', baseUrl: 'Base URL (opcional)', key: 'API key', keySaved: 'guardada — escribe para reemplazar', keyNone: 'sin configurar', orqToken: 'Token de Orquesta',
181
+ accessH: 'Qué puede ver y hacer', accessHint: 'Todo queda en este computador, salvo lo que va al cerebro que elegiste arriba.',
182
+ agent: 'Tu agente de código y la terminal', agentD: 'Tus prompts, comandos y herramientas van al cerebro para que comente. Apagado: solo sabe “pasó algo”.',
183
+ memory: 'Aprender de ti', memoryD: 'Guarda notas (“anota que…”) y las usa al responder.',
184
+ open: 'Abrir sitios', openD: 'Abre tu correo, calendario… cuando se lo pides o cuando suena un recordatorio.',
185
+ notify: 'Notificaciones', notifyD: 'Una notificación de escritorio cuando suena un recordatorio.',
186
+ soon: 'muy pronto', screen: 'Ver tu pantalla', screenD: 'Mirar lo que tienes abierto para ayudarte.', procs: 'Procesos y apps', procsD: 'Saber qué corre y actuar (vía el agente de Orquesta).', inbox: 'Leer correo y calendario', inboxD: 'Resúmenes y respuestas, no solo abrir la pestaña.',
187
+ petH: 'Cuánto habla', annoyHint: 'Qué tan seguido comenta lo que haces.', a0: 'Callado', a1: 'Poco', a2: 'Normal', a3: 'Charlatán',
188
+ view: 'Aspecto:', v2d: '2D', v3d: 'Peluche 3D', viewNext: 'Se aplica la próxima vez que se abra (7ots stop · 7ots pet).',
189
+ notesH: 'Lo que sabe de ti', notesHint: 'Sus notas. Dile “anota que…” para agregar una.', noNotes: 'Nada todavía.', forget: 'Olvidar', forgetAll: 'Olvidar todo', sure: 'Haz clic otra vez para olvidar todo',
190
+ remH: 'Recordatorios', remHint: 'Dile “recuérdame … en 10 minutos” o “a las 15:30”.', noRems: 'Sin recordatorios.', cancel: 'Cancelar',
191
+ foot: 'Abre esta página con <code>7ots config</code>, o ⚙️ en el menú de la mascota.', err: 'No se pudo guardar: {e}',
192
+ },
193
+ pt: {
194
+ title: '{name} · ajustes', back: '← Voltar ao bichinho', save: 'Salvar', saved: 'Salvo ✓', test: 'Testar', testing: 'Pensando…', now: 'Agora:',
195
+ brainH: 'Cérebro', brainHint: 'Quem pensa quando ele fala com você. Seu próprio CLI de IA é grátis com sua assinatura e roda neste computador.',
196
+ lines: 'Sem IA', linesD: 'Frases embutidas. Instantâneo, mas não entende texto livre (lembretes funcionam).',
197
+ cli: 'CLI de IA local', cliD: 'Um comando de IA já instalado aqui, como instância separada.', notInstalled: 'não instalado',
198
+ api: 'Sua API key', apiD: 'Anthropic ou qualquer endpoint compatível com OpenAI, com sua key.',
199
+ batuta: 'Batuta (Orquesta)', batutaD: 'O roteador de modelos da Orquesta, com seu token.',
200
+ home: 'A casa do ot', homeD: 'O /chat da casa dele no 7ots.com ou no seu servidor.', noHome: 'sem casa online',
201
+ model: 'Modelo (opcional)', provider: 'Provedor', baseUrl: 'Base URL (opcional)', key: 'API key', keySaved: 'salva — digite para trocar', keyNone: 'não configurada', orqToken: 'Token da Orquesta',
202
+ accessH: 'O que ele pode ver e fazer', accessHint: 'Tudo fica neste computador, menos o que vai para o cérebro escolhido acima.',
203
+ agent: 'Seu agente de código e o terminal', agentD: 'Seus prompts, comandos e ferramentas vão para o cérebro comentar. Desligado: só sabe “algo aconteceu”.',
204
+ memory: 'Aprender sobre você', memoryD: 'Guarda notas (“anote que…”) e usa ao responder.',
205
+ open: 'Abrir sites', openD: 'Abre seu e-mail, agenda… quando você pede ou quando um lembrete toca.',
206
+ notify: 'Notificações', notifyD: 'Uma notificação na área de trabalho quando um lembrete toca.',
207
+ soon: 'em breve', screen: 'Ver sua tela', screenD: 'Olhar o que está aberto para ajudar.', procs: 'Processos e apps', procsD: 'Saber o que roda e agir (via o agente da Orquesta).', inbox: 'Ler e-mail e agenda', inboxD: 'Resumos e respostas, não só abrir a aba.',
208
+ petH: 'Quanto fala', annoyHint: 'Com que frequência comenta o que você faz.', a0: 'Calado', a1: 'Pouco', a2: 'Normal', a3: 'Tagarela',
209
+ view: 'Visual:', v2d: '2D', v3d: 'Pelúcia 3D', viewNext: 'Vale na próxima vez que abrir (7ots stop · 7ots pet).',
210
+ notesH: 'O que ele sabe de você', notesHint: 'As notas dele. Diga “anote que…” para adicionar.', noNotes: 'Nada ainda.', forget: 'Esquecer', forgetAll: 'Esquecer tudo', sure: 'Clique de novo para esquecer tudo',
211
+ remH: 'Lembretes', remHint: 'Diga “me lembre de … em 10 minutos” ou “às 15:30”.', noRems: 'Sem lembretes.', cancel: 'Cancelar',
212
+ foot: 'Abra esta página com <code>7ots config</code>, ou ⚙️ no menu do bichinho.', err: 'Não deu para salvar: {e}',
213
+ },
214
+ };
215
+ // The browser's language first: the ot may speak English while its human reads Spanish.
216
+ const nav = (navigator.language || '').slice(0, 2);
217
+ const lang = T[nav] ? nav : T[BOOT.lang] ? BOOT.lang : 'en';
218
+ document.documentElement.lang = lang;
219
+ const t = (k, v = {}) => (T[lang][k] ?? T.en[k] ?? k).replace(/\{(\w+)\}/g, (_, n) => v[n] ?? '');
220
+ const $ = (s) => document.querySelector(s);
221
+ const el = (tag, props = {}, ...kids) => {
222
+ const n = Object.assign(document.createElement(tag), props);
223
+ n.append(...kids.filter((k) => k != null));
224
+ return n;
225
+ };
226
+ const api = (path, body) =>
227
+ fetch(path, { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': BOOT.token }, body: body ? JSON.stringify(body) : undefined }).then(async (r) => {
228
+ const d = await r.json().catch(() => ({}));
229
+ if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
230
+ return d;
231
+ });
232
+ const flash = (node, text, cls = 'ok') => {
233
+ node.className = `msg ${cls}`;
234
+ node.textContent = text;
235
+ clearTimeout(node._t);
236
+ if (cls === 'ok') node._t = setTimeout(() => (node.textContent = ''), 2500);
237
+ };
238
+
239
+ for (const n of document.querySelectorAll('[data-t]')) n.textContent = t(n.dataset.t);
240
+ $('#back').textContent = t('back');
241
+ $('#back').href = `/pet?token=${encodeURIComponent(BOOT.token)}`;
242
+ $('#foot').innerHTML = t('foot'); // own strings only
243
+ $('#ticks').append(...[0, 1, 2, 3].map((i) => el('span', { textContent: t(`a${i}`) })));
244
+
245
+ let S = null;
246
+
247
+ // ── brain ──
248
+ const CLI_MODELS = { claude: ['claude-haiku-4-5-20251001', 'claude-sonnet-5-5', 'claude-opus-5-5'], ollama: ['llama3.2', 'qwen2.5', 'gemma3'] };
249
+ function keyField(name, label) {
250
+ const has = S.keys[name];
251
+ return el('label', { className: 'f' }, `${label} · ${has ? '✓' : t('keyNone')}`, el('input', { type: 'password', name, placeholder: has ? t('keySaved') : '', autocomplete: 'off' }));
252
+ }
253
+ function renderBrains() {
254
+ const b = S.config.brain;
255
+ const box = $('#brains');
256
+ box.replaceChildren();
257
+ const opt = (kind, title, desc, more, off = '') => {
258
+ const radio = el('input', { type: 'radio', name: 'kind', value: kind, checked: b.kind === kind, disabled: !!off });
259
+ radio.id = `k-${kind}`;
260
+ // A div, not a label: the extra fields inside have labels of their own (no nested labels).
261
+ const card = el('div', { className: `opt${off ? ' off' : ''}` }, radio, el('label', { htmlFor: radio.id }, el('b', { textContent: title + (off ? ` — ${off}` : '') })), el('small', { textContent: desc }), more ? el('div', { className: 'more' }, ...more) : null);
262
+ card.addEventListener('click', (e) => {
263
+ if (!off && !e.target.closest('.more') && !radio.checked) radio.checked = true;
264
+ });
265
+ return card;
266
+ };
267
+ // CLI: one chip per known CLI, uninstalled ones greyed.
268
+ const chips = el('div', { className: 'chips' }, ...S.allClis.map((c) => {
269
+ const ok = S.clis.includes(c);
270
+ return el('label', { className: `chip${ok ? '' : ' off'}`, title: ok ? '' : t('notInstalled') }, el('input', { type: 'radio', name: 'cli', value: c, disabled: !ok, checked: b.kind === 'cli' ? b.cli === c : ok && c === S.clis[0] }), c);
271
+ }));
272
+ const cliModel = el('input', { type: 'text', name: 'cliModel', value: b.kind === 'cli' ? b.model || '' : '', placeholder: 'claude-haiku-4-5-20251001', autocomplete: 'off' });
273
+ cliModel.setAttribute('list', 'models');
274
+ const models = el('datalist', { id: 'models' });
275
+ const fillModels = () => {
276
+ const c = box.querySelector('input[name=cli]:checked')?.value;
277
+ models.replaceChildren(...(CLI_MODELS[c] || []).map((m) => el('option', { value: m })));
278
+ cliModel.placeholder = (CLI_MODELS[c] || [''])[0];
279
+ };
280
+ chips.addEventListener('change', fillModels);
281
+ box.append(
282
+ opt('lines', t('lines'), t('linesD')),
283
+ opt('cli', t('cli'), t('cliD'), [chips, el('label', { className: 'f' }, t('model'), cliModel, models)], S.clis.length ? '' : t('notInstalled')),
284
+ opt('api', t('api'), t('apiD'), [
285
+ el('label', { className: 'f' }, t('provider'), el('select', { name: 'provider' }, ...['anthropic', 'openai'].map((p) => el('option', { value: p, textContent: p === 'openai' ? 'OpenAI-compatible' : 'Anthropic', selected: b.provider === p })))),
286
+ el('label', { className: 'f' }, t('model'), el('input', { type: 'text', name: 'apiModel', value: b.kind === 'api' ? b.model || '' : '', autocomplete: 'off' })),
287
+ el('label', { className: 'f' }, t('baseUrl'), el('input', { type: 'text', name: 'baseUrl', value: b.baseUrl || '', placeholder: 'https://…', autocomplete: 'off' })),
288
+ keyField('ANTHROPIC_API_KEY', 'Anthropic ' + t('key')),
289
+ keyField('OPENAI_API_KEY', 'OpenAI ' + t('key')),
290
+ ]),
291
+ opt('batuta', t('batuta'), t('batutaD'), [keyField('ORQUESTA_TOKEN', t('orqToken'))]),
292
+ opt('7ots', t('home'), t('homeD'), null, S.home.url ? '' : t('noHome')),
293
+ );
294
+ fillModels();
295
+ $('#now').replaceChildren(t('now'), ' ', el('b', { textContent: S.brain === 'lines' ? t('lines') : S.brain }));
296
+ }
297
+ function brainForm() {
298
+ const box = $('#brains');
299
+ const v = (n) => box.querySelector(`[name=${n}]`)?.value?.trim() || '';
300
+ const kind = box.querySelector('input[name=kind]:checked')?.value || 'lines';
301
+ const brain =
302
+ kind === 'cli' ? { kind, cli: box.querySelector('input[name=cli]:checked')?.value, model: v('cliModel') }
303
+ : kind === 'api' ? { kind, provider: v('provider'), model: v('apiModel'), baseUrl: v('baseUrl') }
304
+ : { kind };
305
+ const keys = {};
306
+ for (const n of ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ORQUESTA_TOKEN']) if (v(n)) keys[n] = v(n);
307
+ return { brain, ...(Object.keys(keys).length ? { keys } : {}) };
308
+ }
309
+ async function saveBrain() {
310
+ const btn = $('#saveBrain');
311
+ btn.disabled = true;
312
+ try {
313
+ S = await api('/config', brainForm());
314
+ renderBrains();
315
+ flash($('#brainMsg'), t('saved'));
316
+ return true;
317
+ } catch (e) {
318
+ flash($('#brainMsg'), t('err', { e: e.message }), 'err');
319
+ return false;
320
+ } finally {
321
+ btn.disabled = false;
322
+ }
323
+ }
324
+ $('#saveBrain').addEventListener('click', saveBrain);
325
+ $('#test').addEventListener('click', async () => {
326
+ const btn = $('#test');
327
+ if (!(await saveBrain())) return;
328
+ btn.disabled = true;
329
+ btn.textContent = t('testing');
330
+ const out = $('#reply');
331
+ try {
332
+ const r = await api('/config/test', {});
333
+ out.style.display = 'block';
334
+ out.textContent = r.ok ? `“${r.text}” · ${(r.ms / 1000).toFixed(1)} s` : `✗ ${r.error}`;
335
+ out.style.color = r.ok ? '' : 'var(--err)';
336
+ } finally {
337
+ btn.disabled = false;
338
+ btn.textContent = t('test');
339
+ }
340
+ });
341
+
342
+ // ── access ──
343
+ function renderAccess() {
344
+ const box = $('#access');
345
+ box.replaceChildren();
346
+ const sw = (key, on, disabled, soon) => {
347
+ const input = el('input', { type: 'checkbox', checked: on, disabled });
348
+ input.setAttribute('role', 'switch');
349
+ input.setAttribute('aria-label', t(key));
350
+ if (!disabled) input.addEventListener('change', () => save({ access: { [key]: input.checked } }, $('#accessMsg')));
351
+ return el('div', { className: `sw${soon ? ' soon' : ''}` }, el('b', {}, t(key), soon ? el('span', { className: 'tag', textContent: t('soon') }) : null), el('small', { textContent: t(`${key}D`) }), el('span', { className: 'tog' }, input, el('span')));
352
+ };
353
+ for (const k of ['agent', 'memory', 'open', 'notify']) box.append(sw(k, S.config.access[k] !== false, false, false));
354
+ for (const k of ['screen', 'procs', 'inbox']) box.append(sw(k, false, true, true));
355
+ box.append(el('span', { className: 'msg', id: 'accessMsg', role: 'status' }));
356
+ }
357
+
358
+ // ── chattiness + look ──
359
+ function renderPet() {
360
+ $('#annoy').value = S.config.pet.annoy ?? 2;
361
+ const v = S.config.pet.view || '2d';
362
+ $('#view').replaceChildren(...['2d', '3d'].map((k) => {
363
+ const r = el('input', { type: 'radio', name: 'view', value: k, checked: v === k });
364
+ r.addEventListener('change', () => save({ pet: { view: k } }, $('#petMsg'), t('viewNext')));
365
+ return el('label', { className: 'chip' }, r, t(`v${k}`));
366
+ }));
367
+ }
368
+ $('#annoy').addEventListener('change', (e) => save({ pet: { annoy: Number(e.target.value) } }, $('#petMsg')));
369
+
370
+ async function save(body, msg, okText = t('saved')) {
371
+ try {
372
+ S = await api('/config', body);
373
+ flash(msg, okText);
374
+ } catch (e) {
375
+ flash(msg, t('err', { e: e.message }), 'err');
376
+ }
377
+ }
378
+
379
+ // ── notes + reminders ──
380
+ const fmt = (ms) => new Date(ms).toLocaleString(lang, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
381
+ function renderLists() {
382
+ const notes = S.notes.slice().reverse();
383
+ $('#notes').replaceChildren(...(notes.length ? notes.map((n) => {
384
+ const x = el('button', { className: 'x', textContent: t('forget') });
385
+ x.addEventListener('click', async () => {
386
+ await api('/notes/forget', { at: n.at });
387
+ refresh();
388
+ });
389
+ return el('li', {}, el('span', { textContent: n.text }), n.at ? el('time', { textContent: fmt(n.at) }) : null, x);
390
+ }) : [el('li', { className: 'empty', textContent: t('noNotes') })]));
391
+ $('#forgetAll').hidden = !notes.length;
392
+ $('#rems').replaceChildren(...(S.reminders.length ? S.reminders.map((r) => {
393
+ const x = el('button', { className: 'x', textContent: t('cancel') });
394
+ x.addEventListener('click', async () => {
395
+ await api('/reminders/cancel', { id: r.id });
396
+ refresh();
397
+ });
398
+ return el('li', {}, el('time', { textContent: fmt(r.at) }), el('span', { textContent: r.text + (r.open ? ` → ${new URL(r.open).host}` : '') }), x);
399
+ }) : [el('li', { className: 'empty', textContent: t('noRems') })]));
400
+ }
401
+ let armed = false;
402
+ $('#forgetAll').addEventListener('click', async (e) => {
403
+ if (!armed) {
404
+ armed = true;
405
+ e.target.textContent = t('sure');
406
+ setTimeout(() => ((armed = false), (e.target.textContent = t('forgetAll'))), 4000);
407
+ return;
408
+ }
409
+ armed = false;
410
+ e.target.textContent = t('forgetAll');
411
+ await api('/notes/forget', { at: 'all' });
412
+ refresh();
413
+ });
414
+
415
+ async function refresh(all = false) {
416
+ S = await api('/config');
417
+ if (all) {
418
+ document.title = t('title', { name: S.name });
419
+ $('#title').textContent = t('title', { name: S.name });
420
+ renderBrains();
421
+ renderAccess();
422
+ renderPet();
423
+ }
424
+ renderLists();
425
+ }
426
+ refresh(true);
427
+ setInterval(() => document.visibilityState === 'visible' && refresh(), 15000); // reminders fire, notes get added
428
+ </script>
429
+ </body>
430
+ </html>